From c5a53846e91ff98541d7ab22eccbf97373ce50ec Mon Sep 17 00:00:00 2001 From: Alex Lebedko Date: Fri, 29 Mar 2024 16:06:46 +0700 Subject: [PATCH 001/115] liquidity pools list module --- fearless.xcodeproj/project.pbxproj | 68 ++++++++++++++++++ .../colorColdGreen.colorset/Contents.json | 6 +- fearless/Common/View/TokenPairIconsView.swift | 51 ++++++++++++++ .../ViewModel/TokenPairsIconViewModel.swift | 6 ++ .../LiquidityPoolsListAssembly.swift | 24 +++++++ .../LiquidityPoolsListInteractor.swift | 15 ++++ .../LiquidityPoolsListPresenter.swift | 45 ++++++++++++ .../LiquidityPoolsListProtocols.swift | 19 +++++ .../LiquidityPoolsListRouter.swift | 3 + .../LiquidityPoolsListViewController.swift | 49 +++++++++++++ .../LiquidityPoolsListViewLayout.swift | 12 ++++ .../View/LiquidityPoolListCell.swift | 69 +++++++++++++++++++ .../LiquidityPoolsListTests.swift | 16 +++++ 13 files changed, 380 insertions(+), 3 deletions(-) create mode 100644 fearless/Common/View/TokenPairIconsView.swift create mode 100644 fearless/Common/ViewModel/TokenPairsIconViewModel.swift create mode 100644 fearless/Modules/LiquidityPoolsList/LiquidityPoolsListAssembly.swift create mode 100644 fearless/Modules/LiquidityPoolsList/LiquidityPoolsListInteractor.swift create mode 100644 fearless/Modules/LiquidityPoolsList/LiquidityPoolsListPresenter.swift create mode 100644 fearless/Modules/LiquidityPoolsList/LiquidityPoolsListProtocols.swift create mode 100644 fearless/Modules/LiquidityPoolsList/LiquidityPoolsListRouter.swift create mode 100644 fearless/Modules/LiquidityPoolsList/LiquidityPoolsListViewController.swift create mode 100644 fearless/Modules/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift create mode 100644 fearless/Modules/LiquidityPoolsList/View/LiquidityPoolListCell.swift create mode 100644 fearlessTests/Modules/LiquidityPoolsList/LiquidityPoolsListTests.swift diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index d781364a20..bfcf3d5324 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 002561414AF1F8F3B4B65538 /* WalletTransactionDetailsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7416F3AFA5F4D1130B1C410 /* WalletTransactionDetailsWireframe.swift */; }; 0077B6854FDCA7ECCD557B09 /* StakingPoolCreateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D651E438F86F37CC07D6D3F /* StakingPoolCreateViewController.swift */; }; 02FA6FDF7212F1F8D056BC18 /* SwapTransactionDetailAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = C597BAB74DF23914B68FDC39 /* SwapTransactionDetailAssembly.swift */; }; + 04724AE7FCB7364D3AE0248F /* LiquidityPoolsListInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7045FDDE48C27A858389B302 /* LiquidityPoolsListInteractor.swift */; }; 04A17615051F4A1AE0E63BF8 /* PolkaswapSwapConfirmationViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F67BB672040911E4A165446 /* PolkaswapSwapConfirmationViewLayout.swift */; }; 04F9CF04588676D79EFB8570 /* ClaimCrowdloanRewardsAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBAA28C551344F1457D76DD5 /* ClaimCrowdloanRewardsAssembly.swift */; }; 054C4BCDEC29ED5F74A36E8B /* ExportMnemonicPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61EBE466BDCF77E65FDCDF81 /* ExportMnemonicPresenter.swift */; }; @@ -208,6 +209,7 @@ 07FBC9E228BE24E900ED65B4 /* MissingAccountFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07FBC9E128BE24E900ED65B4 /* MissingAccountFetcher.swift */; }; 07FBC9E628BE29C900ED65B4 /* ChainIssuesCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07FBC9E528BE29C900ED65B4 /* ChainIssuesCenter.swift */; }; 07FD95C027F4384900F07064 /* UIFont+dynamicSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07FD95BF27F4384900F07064 /* UIFont+dynamicSize.swift */; }; + 085DBF0F0FE7856B5C52815E /* LiquidityPoolsListAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6627C06C19A177744E32BA35 /* LiquidityPoolsListAssembly.swift */; }; 093C10C88C0A209158EA75D1 /* UsernameSetupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5EF68BE0E29D2305CB7337 /* UsernameSetupTests.swift */; }; 0A4820F04EC9DA9DD515EC3A /* MainNftContainerRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1CA1EF5BF1151E0DFB298C /* MainNftContainerRouter.swift */; }; 0AAFEFA17F249F4BEF051F6B /* ControllerAccountConfirmationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54FB887490A8B33890B4E0E4 /* ControllerAccountConfirmationPresenter.swift */; }; @@ -333,6 +335,7 @@ 41040A200CF5E77C291128DA /* SwapTransactionDetailInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3EB4CF4E3E4B486D16BDE5C /* SwapTransactionDetailInteractor.swift */; }; 41831354E275074C7BBC4C28 /* NodeSelectionViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BDDF1911884C819F63DCA80 /* NodeSelectionViewFactory.swift */; }; 41B29C1C9239BB2DCB7903A7 /* SelectValidatorsStartViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C391292F22D16427C77CD9 /* SelectValidatorsStartViewFactory.swift */; }; + 423DD1059C62CFEF51A0CFCF /* LiquidityPoolsListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23BF570839C35B8FA1F7CAF6 /* LiquidityPoolsListTests.swift */; }; 42B79A8D0D9540C1D97D991C /* AccountConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 599FEDBD7E8B665F1A93BA70 /* AccountConfirmWireframe.swift */; }; 436D8B9651C88360A6D72E90 /* ChainSelectionWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = C829A981119FEA0EAE4E96E9 /* ChainSelectionWireframe.swift */; }; 4448B591D4A193DBC9E2E3BF /* AccountCreateInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A7B39A61DB0D2F0F1B1DBA1 /* AccountCreateInteractor.swift */; }; @@ -376,6 +379,7 @@ 59745D3C9602745E1417D2F6 /* ChainSelectionInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83AB0AD3A7CECD061611F60C /* ChainSelectionInteractor.swift */; }; 5980BDE494C9E473E5959C71 /* NftCollectionProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD845193EDFC3A1D0BC73719 /* NftCollectionProtocols.swift */; }; 59838EECC194BB0E6E0AEAA2 /* PolkaswapAdjustmentPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B5D683E7DE3533CA418BD21 /* PolkaswapAdjustmentPresenter.swift */; }; + 5A403C06012CE3DD5AA0415C /* LiquidityPoolsListRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BB81B27EBBDD4D246767C4A /* LiquidityPoolsListRouter.swift */; }; 5C796EF8ED29F564B5D1126B /* CrowdloanContributionConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F75722D2F921FD1C2D4105D /* CrowdloanContributionConfirmViewController.swift */; }; 5D0665A581B9F8DFDBD0CF7B /* WalletChainAccountDashboardWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885A9E2D3619FEFC5ED0C093 /* WalletChainAccountDashboardWireframe.swift */; }; 5DDD2206DF795CF205610455 /* AccountExportPasswordPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31226053044986BC828AA912 /* AccountExportPasswordPresenter.swift */; }; @@ -902,6 +906,7 @@ 847C966325536455002D288F /* ExportRestoreJsonViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847C966225536455002D288F /* ExportRestoreJsonViewFactory.swift */; }; 847CD46626416D0900E1542F /* SlashesOperationFactoryStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847CD46526416D0900E1542F /* SlashesOperationFactoryStub.swift */; }; 847DD8DC26034B99003DE053 /* LocalizableViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847DD8DB26034B99003DE053 /* LocalizableViewProtocol.swift */; }; + 8485446546C43C075408F3B7 /* LiquidityPoolsListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = E137263F032552C463560C91 /* LiquidityPoolsListProtocols.swift */; }; 84873AFF26028E2B000A83EE /* StakingStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84873AFE26028E2B000A83EE /* StakingStateMachine.swift */; }; 84873B0426029B75000A83EE /* StakingEstimationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84873B0326029B75000A83EE /* StakingEstimationViewModel.swift */; }; 84873B0926029CBD000A83EE /* StakingStateViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84873B0826029CBD000A83EE /* StakingStateViewModelFactory.swift */; }; @@ -1393,6 +1398,7 @@ 94B0F0C84AF74B3CD7223C3A /* AccountConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7306D50F278F6CC90DC88F27 /* AccountConfirmPresenter.swift */; }; 94EB0971EDA635A626CA8B72 /* StakingPoolCreateInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D9DD27C76FE239728ED5F8 /* StakingPoolCreateInteractor.swift */; }; 9565BEB636E6D386B0C0FBE5 /* StakingPayoutConfirmationViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BE0492B98AB9C1540846B39 /* StakingPayoutConfirmationViewFactory.swift */; }; + 95F3EB368D1DF5503DF40D4F /* LiquidityPoolsListViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D491B2698446FA356D6D800B /* LiquidityPoolsListViewLayout.swift */; }; 9659B32D4622C8D9679298DF /* ChainSelectionViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15D1C78B2844216802DA000 /* ChainSelectionViewFactory.swift */; }; 96B2C3B29C0EA1A068ED5FB1 /* WalletSendConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF834BF779244B8AF7746B04 /* WalletSendConfirmProtocols.swift */; }; 96EBE5F57C97C7A7A525E864 /* SwapTransactionDetailProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC265B5F209B038633AE0E3F /* SwapTransactionDetailProtocols.swift */; }; @@ -1416,6 +1422,7 @@ 9F0457A013858A7ADEB41234 /* NftSendConfirmTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE7D061906CF67230BF5393A /* NftSendConfirmTests.swift */; }; 9F4A48B1BE3A1110A0CF9F36 /* ReferralCrowdloanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DDDB2B35CD3299F50613141 /* ReferralCrowdloanViewController.swift */; }; 9F664DA212363F5F2C2B530B /* SelectMarketRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C05A688EA7379572BBCE545 /* SelectMarketRouter.swift */; }; + A06CF2CB8E8AEC2E67B6A496 /* LiquidityPoolsListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65D8653B07D92DFE321CEF1E /* LiquidityPoolsListPresenter.swift */; }; A090FF206B56A0E465C62072 /* CrowdloanListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86F7A369E31DCB9ABD556EE9 /* CrowdloanListPresenter.swift */; }; A14F444FA808457C16EF826C /* WalletOptionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193908913A5B241C82672D8A /* WalletOptionViewController.swift */; }; A29F1452BF0AEB885E6460E2 /* SelectMarketPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E07F60661001B2C505D9C8B /* SelectMarketPresenter.swift */; }; @@ -1600,6 +1607,7 @@ C3C7D60B36C778DA0A307BCC /* AddCustomNodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CD8A37F219A0BCC0C6063E /* AddCustomNodeViewController.swift */; }; C3D915637D26E807B85957CF /* NodeSelectionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABDA8952766DE724CD078D6 /* NodeSelectionPresenter.swift */; }; C4427244A22EA7BA7F7C9E9F /* StakingRedeemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403D8D690B564EDC04996945 /* StakingRedeemTests.swift */; }; + C45A034C51D3FE788E304291 /* LiquidityPoolsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 101F1829F8CB980B39A7F14A /* LiquidityPoolsListViewController.swift */; }; C46EEF6A9A9A601694E72DB1 /* StakingMainWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F09665083031502F9693F8 /* StakingMainWireframe.swift */; }; C481665C170F4D9523DC73AF /* WarningAlertViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55EEBFA4A54B3AAF9F52855 /* WarningAlertViewLayout.swift */; }; C4A4D40A08DAB4A71C21C1A8 /* StakingRedeemInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560C48D7A83F51F001622D71 /* StakingRedeemInteractor.swift */; }; @@ -2770,6 +2778,9 @@ FAB707622BB317D300A1131C /* CrossChainViewLoadingCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB707612BB317D300A1131C /* CrossChainViewLoadingCollector.swift */; }; FAB707652BB3C06900A1131C /* AssetsAccountRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB707642BB3C06900A1131C /* AssetsAccountRequest.swift */; }; FAB707672BB3C1A400A1131C /* AssetAccountInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB707662BB3C1A400A1131C /* AssetAccountInfo.swift */; }; + FAB707C52BB6792700A1131C /* TokenPairIconsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB707C42BB6792700A1131C /* TokenPairIconsView.swift */; }; + FAB707C72BB67F7000A1131C /* TokenPairsIconViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB707C62BB67F7000A1131C /* TokenPairsIconViewModel.swift */; }; + FAB707C92BB68C4000A1131C /* LiquidityPoolListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB707C82BB68C4000A1131C /* LiquidityPoolListCell.swift */; }; FAB8B96E29F23FCB002E5F04 /* ChainAccountBalanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB8B96D29F23FCA002E5F04 /* ChainAccountBalanceViewModel.swift */; }; FABA161B2B0C941B001AF2F0 /* MultiassetV11MigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FABA161A2B0C941B001AF2F0 /* MultiassetV11MigrationPolicy.swift */; }; FABA161D2B0C94CA001AF2F0 /* UserDataModelV10toV11.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = FABA161C2B0C94C9001AF2F0 /* UserDataModelV10toV11.xcmappingmodel */; }; @@ -3313,6 +3324,7 @@ 0E07F60661001B2C505D9C8B /* SelectMarketPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectMarketPresenter.swift; sourceTree = ""; }; 0F48467D97F9D06F83F70894 /* Pods-fearlessAll-fearless.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-fearlessAll-fearless.dev.xcconfig"; path = "Target Support Files/Pods-fearlessAll-fearless/Pods-fearlessAll-fearless.dev.xcconfig"; sourceTree = ""; }; 0F67BB672040911E4A165446 /* PolkaswapSwapConfirmationViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PolkaswapSwapConfirmationViewLayout.swift; sourceTree = ""; }; + 101F1829F8CB980B39A7F14A /* LiquidityPoolsListViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsListViewController.swift; sourceTree = ""; }; 1211B723BB656770C4EA5BC2 /* WalletTransactionDetailsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletTransactionDetailsViewFactory.swift; sourceTree = ""; }; 12441689D2AF47D508D16CCF /* WalletSendConfirmViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletSendConfirmViewFactory.swift; sourceTree = ""; }; 1366336078BCA34EFB4C6FF9 /* CrowdloanContributionConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionConfirmInteractor.swift; sourceTree = ""; }; @@ -3349,6 +3361,7 @@ 2377F8FB07B47637346249F5 /* StakingRewardDetailsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRewardDetailsViewFactory.swift; sourceTree = ""; }; 23A74BDB54D503FA2BFBEF35 /* StakingUnbondSetupProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondSetupProtocols.swift; sourceTree = ""; }; 23BC71941B91D3E372CDB11C /* CrowdloanContributionSetupViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionSetupViewLayout.swift; sourceTree = ""; }; + 23BF570839C35B8FA1F7CAF6 /* LiquidityPoolsListTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsListTests.swift; sourceTree = ""; }; 23CF7E56EC624BDB60290387 /* CreateContactPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateContactPresenter.swift; sourceTree = ""; }; 25B80FDDB5C3032A0BBBD826 /* NftCollectionPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftCollectionPresenter.swift; sourceTree = ""; }; 25FF82C2FD912021A1F20876 /* PolkaswapAdjustmentViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PolkaswapAdjustmentViewLayout.swift; sourceTree = ""; }; @@ -3502,6 +3515,8 @@ 63CCCC63389A70294F816143 /* NodeSelectionProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NodeSelectionProtocols.swift; sourceTree = ""; }; 63F4BE52D0625CD8C21D2460 /* CrowdloanListViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanListViewLayout.swift; sourceTree = ""; }; 65AD15693E21C869DE1FDD17 /* UsernameSetupWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UsernameSetupWireframe.swift; sourceTree = ""; }; + 65D8653B07D92DFE321CEF1E /* LiquidityPoolsListPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsListPresenter.swift; sourceTree = ""; }; + 6627C06C19A177744E32BA35 /* LiquidityPoolsListAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsListAssembly.swift; sourceTree = ""; }; 66FFB904E5A83F2EFBCCBBF8 /* StakingPoolInfoTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPoolInfoTests.swift; sourceTree = ""; }; 67B1037AEC97DBEAF9FD50C1 /* AssetNetworksPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetNetworksPresenter.swift; sourceTree = ""; }; 67B3E1906EEBE32E71E82BB6 /* NodeSelectionViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NodeSelectionViewLayout.swift; sourceTree = ""; }; @@ -3521,6 +3536,7 @@ 6DE4840EBB9892A5E35FB443 /* AccountExportPasswordViewController.xib */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file.xib; path = AccountExportPasswordViewController.xib; sourceTree = ""; }; 6EDB8BAE1FAE3C7502E9245E /* NftDetailsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftDetailsViewLayout.swift; sourceTree = ""; }; 6FA0310E7E7EA9A985602CCA /* ChainAccountListTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChainAccountListTests.swift; sourceTree = ""; }; + 7045FDDE48C27A858389B302 /* LiquidityPoolsListInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsListInteractor.swift; sourceTree = ""; }; 71285CF636B32ACD8EB5519E /* ReferralCrowdloanViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferralCrowdloanViewFactory.swift; sourceTree = ""; }; 7306D50F278F6CC90DC88F27 /* AccountConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountConfirmPresenter.swift; sourceTree = ""; }; 73078ED3B2642FEAF348DB2A /* WalletOptionProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletOptionProtocols.swift; sourceTree = ""; }; @@ -4517,6 +4533,7 @@ 9A9EBD3B7AEA5EF594DFEB49 /* PolkaswapAdjustmentProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PolkaswapAdjustmentProtocols.swift; sourceTree = ""; }; 9AEA7ECB8434DF494D2B25B9 /* ChainAccountTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChainAccountTests.swift; sourceTree = ""; }; 9B5626189788682A84D4E9D7 /* SelectValidatorsConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectValidatorsConfirmPresenter.swift; sourceTree = ""; }; + 9BB81B27EBBDD4D246767C4A /* LiquidityPoolsListRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsListRouter.swift; sourceTree = ""; }; 9C01DCD4DA014E8FB50B9F11 /* CrowdloanContributionSetupTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionSetupTests.swift; sourceTree = ""; }; 9C05A688EA7379572BBCE545 /* SelectMarketRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectMarketRouter.swift; sourceTree = ""; }; A14CA4551FCC2EBD078E2242 /* AccountConfirmViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountConfirmViewFactory.swift; sourceTree = ""; }; @@ -4841,6 +4858,7 @@ D39D54DC9992CF9CB6699AA3 /* StakingAmountViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingAmountViewFactory.swift; sourceTree = ""; }; D45B7031E0809CED062C83F8 /* StakingUnbondConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondConfirmPresenter.swift; sourceTree = ""; }; D482753E0D75C5F5E0617998 /* CreateContactInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateContactInteractor.swift; sourceTree = ""; }; + D491B2698446FA356D6D800B /* LiquidityPoolsListViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsListViewLayout.swift; sourceTree = ""; }; D4C391292F22D16427C77CD9 /* SelectValidatorsStartViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectValidatorsStartViewFactory.swift; sourceTree = ""; }; D55EEBFA4A54B3AAF9F52855 /* WarningAlertViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WarningAlertViewLayout.swift; sourceTree = ""; }; D5B7937620F4339EE948EC25 /* AddCustomNodePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddCustomNodePresenter.swift; sourceTree = ""; }; @@ -4867,6 +4885,7 @@ DFBFF377F35B254AB3141100 /* AssetNetworksViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetNetworksViewLayout.swift; sourceTree = ""; }; E0A0CC21B0CD2A47B9B28841 /* NftSendViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftSendViewController.swift; sourceTree = ""; }; E11575D8B4F64C2E805372A5 /* AccountExportPasswordViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountExportPasswordViewFactory.swift; sourceTree = ""; }; + E137263F032552C463560C91 /* LiquidityPoolsListProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsListProtocols.swift; sourceTree = ""; }; E18B94164B49123E62FA60B7 /* AccountManagementViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountManagementViewFactory.swift; sourceTree = ""; }; E1E60EF37AC0A7646ED8FE64 /* AccountImportViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountImportViewFactory.swift; sourceTree = ""; }; E32C2DC4CC106A3509BE651D /* ExportMnemonicTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExportMnemonicTests.swift; sourceTree = ""; }; @@ -5880,6 +5899,9 @@ FAB707612BB317D300A1131C /* CrossChainViewLoadingCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossChainViewLoadingCollector.swift; sourceTree = ""; }; FAB707642BB3C06900A1131C /* AssetsAccountRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsAccountRequest.swift; sourceTree = ""; }; FAB707662BB3C1A400A1131C /* AssetAccountInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetAccountInfo.swift; sourceTree = ""; }; + FAB707C42BB6792700A1131C /* TokenPairIconsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenPairIconsView.swift; sourceTree = ""; }; + FAB707C62BB67F7000A1131C /* TokenPairsIconViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenPairsIconViewModel.swift; sourceTree = ""; }; + FAB707C82BB68C4000A1131C /* LiquidityPoolListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidityPoolListCell.swift; sourceTree = ""; }; FAB8B96D29F23FCA002E5F04 /* ChainAccountBalanceViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChainAccountBalanceViewModel.swift; sourceTree = ""; }; FABA161A2B0C941B001AF2F0 /* MultiassetV11MigrationPolicy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiassetV11MigrationPolicy.swift; sourceTree = ""; }; FABA161C2B0C94C9001AF2F0 /* UserDataModelV10toV11.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = UserDataModelV10toV11.xcmappingmodel; sourceTree = ""; }; @@ -6977,6 +6999,21 @@ path = ClaimCrowdloanRewards; sourceTree = ""; }; + 0D31C32ADE0D58DD072D6AB2 /* LiquidityPoolsList */ = { + isa = PBXGroup; + children = ( + FAB707C32BB678CB00A1131C /* View */, + E137263F032552C463560C91 /* LiquidityPoolsListProtocols.swift */, + 9BB81B27EBBDD4D246767C4A /* LiquidityPoolsListRouter.swift */, + 65D8653B07D92DFE321CEF1E /* LiquidityPoolsListPresenter.swift */, + 7045FDDE48C27A858389B302 /* LiquidityPoolsListInteractor.swift */, + 101F1829F8CB980B39A7F14A /* LiquidityPoolsListViewController.swift */, + D491B2698446FA356D6D800B /* LiquidityPoolsListViewLayout.swift */, + 6627C06C19A177744E32BA35 /* LiquidityPoolsListAssembly.swift */, + ); + path = LiquidityPoolsList; + sourceTree = ""; + }; 0D3E297DAEAAF14373239578 /* NetworkInfo */ = { isa = PBXGroup; children = ( @@ -7646,6 +7683,7 @@ E1A8025898B97966D25AF46D /* NftSendConfirm */, 85BB4B6268BB3E4F39F414D1 /* AssetNetworks */, D734A66128ED3C8653098101 /* ClaimCrowdloanRewards */, + ACA6E90849E6A0CC4C775867 /* LiquidityPoolsList */, ); path = Modules; sourceTree = ""; @@ -9207,6 +9245,7 @@ 45C94A390068322611CA7C02 /* SwapTransactionDetail */, DA46895F73B0FB1064146E47 /* AssetNetworks */, 0AEF32A92BEEDB9B5A60A433 /* ClaimCrowdloanRewards */, + 0D31C32ADE0D58DD072D6AB2 /* LiquidityPoolsList */, ); path = Modules; sourceTree = ""; @@ -9729,6 +9768,7 @@ 076D9D58294C461E002762E3 /* PolkaswapDoubleSymbolView.swift */, FA584C772AB2BCD500F6F020 /* UniversalMediaView.swift */, C6DC2D592B1458CC00BAA4DB /* CollectionViewSectionHeader.swift */, + FAB707C42BB6792700A1131C /* TokenPairIconsView.swift */, ); path = View; sourceTree = ""; @@ -9963,6 +10003,7 @@ FA38C9A0275FD6B9005C5577 /* BundleImageViewModel.swift */, FA38C9C02761E68B005C5577 /* AccountViewModel.swift */, FA86443F276843E100956D8E /* ViewModelObserver.swift */, + FAB707C62BB67F7000A1131C /* TokenPairsIconViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -11011,6 +11052,14 @@ path = StakingUnbondConfirm; sourceTree = ""; }; + ACA6E90849E6A0CC4C775867 /* LiquidityPoolsList */ = { + isa = PBXGroup; + children = ( + 23BF570839C35B8FA1F7CAF6 /* LiquidityPoolsListTests.swift */, + ); + path = LiquidityPoolsList; + sourceTree = ""; + }; AE1000EF2667981A004753B7 /* ChangeTargets */ = { isa = PBXGroup; children = ( @@ -14735,6 +14784,14 @@ path = AssetsPallet; sourceTree = ""; }; + FAB707C32BB678CB00A1131C /* View */ = { + isa = PBXGroup; + children = ( + FAB707C82BB68C4000A1131C /* LiquidityPoolListCell.swift */, + ); + path = View; + sourceTree = ""; + }; FABA162E2B0C9510001AF2F0 /* NetworkManagment */ = { isa = PBXGroup; children = ( @@ -16858,6 +16915,7 @@ FA93A2F52834AB750021330F /* ValidatorInfoFlow.swift in Sources */, 8430AADC26022C58005B1066 /* NoStashState.swift in Sources */, 84F4A9182550331D000CF0A3 /* ExportOption.swift in Sources */, + FAB707C92BB68C4000A1131C /* LiquidityPoolListCell.swift in Sources */, FA2FC80C28B3807C00CC0A42 /* StakingPoolJoinConfirmInteractor.swift in Sources */, 84FD3DB12540C09800A234E3 /* TransactionHistoryMergeManager.swift in Sources */, 84038FEC26FFBA4D00C73F3F /* PriceLocalStorageSubscriber.swift in Sources */, @@ -17820,6 +17878,7 @@ FA62625B2AC2E35A005D3D95 /* MultiSelectNetworksViewModel.swift in Sources */, FAB707672BB3C1A400A1131C /* AssetAccountInfo.swift in Sources */, FAD429182A86567F001D6A16 /* BackupSelectWalletTableCell.swift in Sources */, + FAB707C72BB67F7000A1131C /* TokenPairsIconViewModel.swift in Sources */, FA34EEDD2B98723C0042E73E /* OnboardingPageCell.swift in Sources */, FAE9EBA7288ABBFC009390B6 /* AnalyticsRewardsRelaychainViewModelState.swift in Sources */, 07F2B76328ACDA7800280C38 /* RuntimeHotBootSnapshotFactory.swift in Sources */, @@ -18218,6 +18277,7 @@ 849ABE772628103200011A2A /* ControllersListReducer.swift in Sources */, FA99425628053C8800D771E5 /* SelectableExportAccountTableCell.swift in Sources */, 9565BEB636E6D386B0C0FBE5 /* StakingPayoutConfirmationViewFactory.swift in Sources */, + FAB707C52BB6792700A1131C /* TokenPairIconsView.swift in Sources */, FA37AE202858838A001DCA96 /* ParachainStakingScheduledRequest.swift in Sources */, FA6262512AC2E35A005D3D95 /* WalletConnectActiveSessionsTableCell.swift in Sources */, FAA086D82848AB8600CC2F33 /* YourRewardDestinationViewModel.swift in Sources */, @@ -18806,6 +18866,13 @@ 7E3FB57A93AFAE39CF3030C8 /* ClaimCrowdloanRewardsViewController.swift in Sources */, C80CF43B1F8307A48D03741D /* ClaimCrowdloanRewardsViewLayout.swift in Sources */, 04F9CF04588676D79EFB8570 /* ClaimCrowdloanRewardsAssembly.swift in Sources */, + 8485446546C43C075408F3B7 /* LiquidityPoolsListProtocols.swift in Sources */, + 5A403C06012CE3DD5AA0415C /* LiquidityPoolsListRouter.swift in Sources */, + A06CF2CB8E8AEC2E67B6A496 /* LiquidityPoolsListPresenter.swift in Sources */, + 04724AE7FCB7364D3AE0248F /* LiquidityPoolsListInteractor.swift in Sources */, + C45A034C51D3FE788E304291 /* LiquidityPoolsListViewController.swift in Sources */, + 95F3EB368D1DF5503DF40D4F /* LiquidityPoolsListViewLayout.swift in Sources */, + 085DBF0F0FE7856B5C52815E /* LiquidityPoolsListAssembly.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -18976,6 +19043,7 @@ 9F0457A013858A7ADEB41234 /* NftSendConfirmTests.swift in Sources */, 3E72CF4FCDFE9ED965124D18 /* AssetNetworksTests.swift in Sources */, 99DCBCC3298620721B213012 /* ClaimCrowdloanRewardsTests.swift in Sources */, + 423DD1059C62CFEF51A0CFCF /* LiquidityPoolsListTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/fearless/Assets.xcassets/colors/colorColdGreen.colorset/Contents.json b/fearless/Assets.xcassets/colors/colorColdGreen.colorset/Contents.json index 90bfbb4389..9e966c9717 100644 --- a/fearless/Assets.xcassets/colors/colorColdGreen.colorset/Contents.json +++ b/fearless/Assets.xcassets/colors/colorColdGreen.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "161", - "green" : "200", - "red" : "9" + "blue" : "0xA1", + "green" : "0xC8", + "red" : "0x09" } }, "idiom" : "universal" diff --git a/fearless/Common/View/TokenPairIconsView.swift b/fearless/Common/View/TokenPairIconsView.swift new file mode 100644 index 0000000000..14846d18e3 --- /dev/null +++ b/fearless/Common/View/TokenPairIconsView.swift @@ -0,0 +1,51 @@ +import UIKit + +class TokenPairIconsView: UIView { + let firstTokenIconView = UIImageView() + let secondTokenIconView = UIImageView() + + override init(frame: CGRect) { + super.init(frame: frame) + + drawSubviews() + setupLayout() + } + + override func layoutSubviews() { + super.layoutSubviews() + + setupLayout() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + + drawSubviews() + setupLayout() + } + + func bind(viewModel: TokenPairsIconViewModel) { + let iconSize = CGSize(width: frame.size.height / 1.5, height: frame.size.height / 1.5) + + viewModel.firstTokenIconViewModel.loadImage(on: firstTokenIconView, targetSize: iconSize, animated: false) + viewModel.secondTokenIconViewModel.loadImage(on: secondTokenIconView, targetSize: iconSize, animated: false) + } + + private func drawSubviews() { + addSubview(firstTokenIconView) + addSubview(secondTokenIconView) + } + + private func setupLayout() { + let iconSize = frame.size.height / 1.5 + let horizontalOffset: CGFloat = -(iconSize / 3.0) + let verticalOffset: CGFloat = -(iconSize / 2.0) + firstTokenIconView.frame = CGRect(x: 0.0, y: 0.0, width: iconSize, height: iconSize) + secondTokenIconView.frame = CGRect( + x: CGRectGetMaxX(firstTokenIconView.frame) + horizontalOffset, + y: CGRectGetMaxY(firstTokenIconView.frame) + verticalOffset, + width: iconSize, + height: iconSize + ) + } +} diff --git a/fearless/Common/ViewModel/TokenPairsIconViewModel.swift b/fearless/Common/ViewModel/TokenPairsIconViewModel.swift new file mode 100644 index 0000000000..dead1b2406 --- /dev/null +++ b/fearless/Common/ViewModel/TokenPairsIconViewModel.swift @@ -0,0 +1,6 @@ +import Foundation + +struct TokenPairsIconViewModel { + let firstTokenIconViewModel: RemoteImageViewModel + let secondTokenIconViewModel: RemoteImageViewModel +} diff --git a/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListAssembly.swift b/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListAssembly.swift new file mode 100644 index 0000000000..4d9a0a722f --- /dev/null +++ b/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListAssembly.swift @@ -0,0 +1,24 @@ +import UIKit +import SoraFoundation + +final class LiquidityPoolsListAssembly { + static func configureModule() -> LiquidityPoolsListModuleCreationResult? { + let localizationManager = LocalizationManager.shared + + let interactor = LiquidityPoolsListInteractor() + let router = LiquidityPoolsListRouter() + + let presenter = LiquidityPoolsListPresenter( + interactor: interactor, + router: router, + localizationManager: localizationManager + ) + + let view = LiquidityPoolsListViewController( + output: presenter, + localizationManager: localizationManager + ) + + return (view, presenter) + } +} diff --git a/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListInteractor.swift b/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListInteractor.swift new file mode 100644 index 0000000000..2656d5fa45 --- /dev/null +++ b/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListInteractor.swift @@ -0,0 +1,15 @@ +import UIKit + +final class LiquidityPoolsListInteractor { + // MARK: - Private properties + + private weak var output: LiquidityPoolsListInteractorOutput? +} + +// MARK: - LiquidityPoolsListInteractorInput + +extension LiquidityPoolsListInteractor: LiquidityPoolsListInteractorInput { + func setup(with output: LiquidityPoolsListInteractorOutput) { + self.output = output + } +} diff --git a/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListPresenter.swift b/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListPresenter.swift new file mode 100644 index 0000000000..84bb5304d3 --- /dev/null +++ b/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListPresenter.swift @@ -0,0 +1,45 @@ +import Foundation +import SoraFoundation + +final class LiquidityPoolsListPresenter { + // MARK: Private properties + + private weak var view: LiquidityPoolsListViewInput? + private let router: LiquidityPoolsListRouterInput + private let interactor: LiquidityPoolsListInteractorInput + + // MARK: - Constructors + + init( + interactor: LiquidityPoolsListInteractorInput, + router: LiquidityPoolsListRouterInput, + localizationManager: LocalizationManagerProtocol + ) { + self.interactor = interactor + self.router = router + self.localizationManager = localizationManager + } + + // MARK: - Private methods +} + +// MARK: - LiquidityPoolsListViewOutput + +extension LiquidityPoolsListPresenter: LiquidityPoolsListViewOutput { + func didLoad(view: LiquidityPoolsListViewInput) { + self.view = view + interactor.setup(with: self) + } +} + +// MARK: - LiquidityPoolsListInteractorOutput + +extension LiquidityPoolsListPresenter: LiquidityPoolsListInteractorOutput {} + +// MARK: - Localizable + +extension LiquidityPoolsListPresenter: Localizable { + func applyLocalization() {} +} + +extension LiquidityPoolsListPresenter: LiquidityPoolsListModuleInput {} diff --git a/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListProtocols.swift b/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListProtocols.swift new file mode 100644 index 0000000000..5b3c88f29d --- /dev/null +++ b/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListProtocols.swift @@ -0,0 +1,19 @@ +typealias LiquidityPoolsListModuleCreationResult = (view: LiquidityPoolsListViewInput, input: LiquidityPoolsListModuleInput) + +protocol LiquidityPoolsListViewInput: ControllerBackedProtocol {} + +protocol LiquidityPoolsListViewOutput: AnyObject { + func didLoad(view: LiquidityPoolsListViewInput) +} + +protocol LiquidityPoolsListInteractorInput: AnyObject { + func setup(with output: LiquidityPoolsListInteractorOutput) +} + +protocol LiquidityPoolsListInteractorOutput: AnyObject {} + +protocol LiquidityPoolsListRouterInput: AnyObject {} + +protocol LiquidityPoolsListModuleInput: AnyObject {} + +protocol LiquidityPoolsListModuleOutput: AnyObject {} diff --git a/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListRouter.swift b/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListRouter.swift new file mode 100644 index 0000000000..7910ac4971 --- /dev/null +++ b/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListRouter.swift @@ -0,0 +1,3 @@ +import Foundation + +final class LiquidityPoolsListRouter: LiquidityPoolsListRouterInput {} diff --git a/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListViewController.swift b/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListViewController.swift new file mode 100644 index 0000000000..ec301df3ca --- /dev/null +++ b/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListViewController.swift @@ -0,0 +1,49 @@ +import UIKit +import SoraFoundation + +final class LiquidityPoolsListViewController: UIViewController, ViewHolder { + typealias RootViewType = LiquidityPoolsListViewLayout + + // MARK: Private properties + + private let output: LiquidityPoolsListViewOutput + + // MARK: - Constructor + + init( + output: LiquidityPoolsListViewOutput, + localizationManager: LocalizationManagerProtocol? + ) { + self.output = output + super.init(nibName: nil, bundle: nil) + self.localizationManager = localizationManager + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Life cycle + + override func loadView() { + view = LiquidityPoolsListViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + output.didLoad(view: self) + } + + // MARK: - Private methods +} + +// MARK: - LiquidityPoolsListViewInput + +extension LiquidityPoolsListViewController: LiquidityPoolsListViewInput {} + +// MARK: - Localizable + +extension LiquidityPoolsListViewController: Localizable { + func applyLocalization() {} +} diff --git a/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift b/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift new file mode 100644 index 0000000000..9f874449a9 --- /dev/null +++ b/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift @@ -0,0 +1,12 @@ +import UIKit + +final class LiquidityPoolsListViewLayout: UIView { + override init(frame: CGRect) { + super.init(frame: frame) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/fearless/Modules/LiquidityPoolsList/View/LiquidityPoolListCell.swift b/fearless/Modules/LiquidityPoolsList/View/LiquidityPoolListCell.swift new file mode 100644 index 0000000000..00a54068f1 --- /dev/null +++ b/fearless/Modules/LiquidityPoolsList/View/LiquidityPoolListCell.swift @@ -0,0 +1,69 @@ +import UIKit + +class LiquidityPoolListCell: UITableViewCell { + let tokenPairIconsView = TokenPairIconsView() + let tokenPairNameLabel: UILabel = { + let label = UILabel() + label.font = .capsTitle + label.textColor = .white + return label + }() + + let rewardTokenNameLabel: UILabel = { + let label = UILabel() + label.font = .p2Paragraph + label.textColor = R.color.colorWhite50() + return label + }() + + let apyLabel: UILabel = { + let label = UILabel() + label.font = .capsTitle + label.textColor = R.color.colorPink() + return label + }() + + let stakingStatusLabel: UILabel = { + let label = UILabel() + label.font = .p2Paragraph + label.textColor = R.color.colorColdGreen() + return label + }() + + let reservesLabel: UILabel = { + let label = UILabel() + label.font = .p2Paragraph + label.textColor = R.color.colorWhite50() + return label + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + drawSubviews() + setupConstraints() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + + drawSubviews() + setupConstraints() + } + + private func drawSubviews() { + addSubview(tokenPairIconsView) + addSubview(tokenPairNameLabel) + addSubview(rewardTokenNameLabel) + addSubview(apyLabel) + addSubview(stakingStatusLabel) + addSubview(reservesLabel) + } + + private func setupConstraints() { + tokenPairIconsView.snp.makeConstraints { make in + make.leading.equalToSuperview().inset(16) + make.top.bottom.equalToSuperview().inset(4) + } + } +} diff --git a/fearlessTests/Modules/LiquidityPoolsList/LiquidityPoolsListTests.swift b/fearlessTests/Modules/LiquidityPoolsList/LiquidityPoolsListTests.swift new file mode 100644 index 0000000000..b4202ea635 --- /dev/null +++ b/fearlessTests/Modules/LiquidityPoolsList/LiquidityPoolsListTests.swift @@ -0,0 +1,16 @@ +import XCTest + +class LiquidityPoolsListTests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + XCTFail("Did you forget to add tests?") + } +} From 40fe4aedeb563068cc74a89f310a80126a919996 Mon Sep 17 00:00:00 2001 From: Alex Lebedko Date: Sat, 30 Mar 2024 10:02:07 +0700 Subject: [PATCH 002/115] lp list cell ui --- fearless.xcodeproj/project.pbxproj | 12 ++++++++ .../View/LiquidityPoolListCell.swift | 29 +++++++++++++++++++ .../LiquidityPoolListCellModel.swift | 1 + 3 files changed, 42 insertions(+) create mode 100644 fearless/Modules/LiquidityPoolsList/ViewModel/LiquidityPoolListCellModel.swift diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index bfcf3d5324..dcba4ed50a 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -2858,6 +2858,7 @@ FACD42BD2A5BE91E009975AA /* PortionRewardCalculatorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACD42BB2A5BE91D009975AA /* PortionRewardCalculatorService.swift */; }; FACD42BF2A5BE93D009975AA /* polkadot-9370metadata in Resources */ = {isa = PBXBuildFile; fileRef = FACD42BE2A5BE93D009975AA /* polkadot-9370metadata */; }; FACD42C12A5C10BB009975AA /* TransferService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACD42C02A5C10BB009975AA /* TransferService.swift */; }; + FACE6C232BB6C37900643CEF /* LiquidityPoolListCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACE6C222BB6C37900643CEF /* LiquidityPoolListCellModel.swift */; }; FAD0068027EA252400C97E09 /* AboutViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD0067E27EA252400C97E09 /* AboutViewModelFactory.swift */; }; FAD0068127EA252400C97E09 /* AboutViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD0067F27EA252400C97E09 /* AboutViewState.swift */; }; FAD0068427EA255900C97E09 /* AboutTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD0068227EA255900C97E09 /* AboutTableViewCell.swift */; }; @@ -5990,6 +5991,7 @@ FACD42BB2A5BE91D009975AA /* PortionRewardCalculatorService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PortionRewardCalculatorService.swift; sourceTree = ""; }; FACD42BE2A5BE93D009975AA /* polkadot-9370metadata */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "polkadot-9370metadata"; sourceTree = ""; }; FACD42C02A5C10BB009975AA /* TransferService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransferService.swift; sourceTree = ""; }; + FACE6C222BB6C37900643CEF /* LiquidityPoolListCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidityPoolListCellModel.swift; sourceTree = ""; }; FAD0067E27EA252400C97E09 /* AboutViewModelFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutViewModelFactory.swift; sourceTree = ""; }; FAD0067F27EA252400C97E09 /* AboutViewState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutViewState.swift; sourceTree = ""; }; FAD0068227EA255900C97E09 /* AboutTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutTableViewCell.swift; sourceTree = ""; }; @@ -7002,6 +7004,7 @@ 0D31C32ADE0D58DD072D6AB2 /* LiquidityPoolsList */ = { isa = PBXGroup; children = ( + FACE6C212BB6C36B00643CEF /* ViewModel */, FAB707C32BB678CB00A1131C /* View */, E137263F032552C463560C91 /* LiquidityPoolsListProtocols.swift */, 9BB81B27EBBDD4D246767C4A /* LiquidityPoolsListRouter.swift */, @@ -14956,6 +14959,14 @@ path = Model; sourceTree = ""; }; + FACE6C212BB6C36B00643CEF /* ViewModel */ = { + isa = PBXGroup; + children = ( + FACE6C222BB6C37900643CEF /* LiquidityPoolListCellModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; FAD4289B2A865635001D6A16 /* BackupRiskWarnings */ = { isa = PBXGroup; children = ( @@ -17260,6 +17271,7 @@ FA072C25277C0FA900731718 /* ApplicationSettingsPresentable.swift in Sources */, F462B35C260C86880005AB01 /* ViewHolder.swift in Sources */, 84C515FB26D84F8C000DBA45 /* AccountImportWrapper.swift in Sources */, + FACE6C232BB6C37900643CEF /* LiquidityPoolListCellModel.swift in Sources */, 849014DE24AA8F60008F705E /* MainTabBarInteractor.swift in Sources */, 849ABE7226280F3800011A2A /* ControllersReducer.swift in Sources */, FAD4290E2A86567F001D6A16 /* BannersViewLayout.swift in Sources */, diff --git a/fearless/Modules/LiquidityPoolsList/View/LiquidityPoolListCell.swift b/fearless/Modules/LiquidityPoolsList/View/LiquidityPoolListCell.swift index 00a54068f1..2040448e39 100644 --- a/fearless/Modules/LiquidityPoolsList/View/LiquidityPoolListCell.swift +++ b/fearless/Modules/LiquidityPoolsList/View/LiquidityPoolListCell.swift @@ -65,5 +65,34 @@ class LiquidityPoolListCell: UITableViewCell { make.leading.equalToSuperview().inset(16) make.top.bottom.equalToSuperview().inset(4) } + + tokenPairNameLabel.snp.makeConstraints { make in + make.leading.equalTo(tokenPairIconsView.snp.trailing).offset(8) + make.top.equalToSuperview().inset(4) + } + + rewardTokenNameLabel.snp.makeConstraints { make in + make.leading.equalTo(tokenPairIconsView.snp.trailing).offset(8) + make.bottom.equalToSuperview().inset(4) + make.top.equalTo(tokenPairNameLabel.snp.bottom).offset(4) + } + + apyLabel.snp.makeConstraints { make in + make.trailing.equalToSuperview().inset(16) + make.leading.equalTo(tokenPairNameLabel.snp.trailing).offset(8) + make.centerY.equalTo(tokenPairNameLabel.snp.centerY) + } + + stakingStatusLabel.snp.makeConstraints { make in + make.trailing.equalToSuperview().inset(16) + make.leading.equalTo(rewardTokenNameLabel.snp.trailing).offset(8) + make.centerY.equalTo(rewardTokenNameLabel.snp.centerY) + } + + reservesLabel.snp.makeConstraints { make in + make.trailing.equalToSuperview().inset(16) + make.leading.equalTo(rewardTokenNameLabel.snp.trailing).offset(8) + make.centerY.equalTo(rewardTokenNameLabel.snp.centerY) + } } } diff --git a/fearless/Modules/LiquidityPoolsList/ViewModel/LiquidityPoolListCellModel.swift b/fearless/Modules/LiquidityPoolsList/ViewModel/LiquidityPoolListCellModel.swift new file mode 100644 index 0000000000..fecc4ab449 --- /dev/null +++ b/fearless/Modules/LiquidityPoolsList/ViewModel/LiquidityPoolListCellModel.swift @@ -0,0 +1 @@ +import Foundation From 1d33a163c149cffc645a3f9fe71d2d78381a5c77 Mon Sep 17 00:00:00 2001 From: Alex Lebedko Date: Tue, 2 Apr 2024 12:52:03 +0700 Subject: [PATCH 003/115] tableview added --- fearless.xcodeproj/project.pbxproj | 4 ++ .../LiquidityPoolsListViewController.swift | 35 +++++++++- .../LiquidityPoolsListViewLayout.swift | 69 +++++++++++++++++++ .../View/LiquidityPoolListCell.swift | 12 ++++ .../LiquidityPoolListCellModel.swift | 9 +++ .../LiquidityPoolListViewModel.swift | 7 ++ fearless/en.lproj/Localizable.strings | 1 + fearless/id.lproj/Localizable.strings | 1 + fearless/ja.lproj/Localizable.strings | 1 + fearless/pt-PT.lproj/Localizable.strings | 1 + fearless/ru.lproj/Localizable.strings | 1 + fearless/tr.lproj/Localizable.strings | 1 + fearless/vi.lproj/Localizable.strings | 1 + fearless/zh-Hans.lproj/Localizable.strings | 1 + 14 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 fearless/Modules/LiquidityPoolsList/ViewModel/LiquidityPoolListViewModel.swift diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index dcba4ed50a..d94a4de0e4 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -2859,6 +2859,7 @@ FACD42BF2A5BE93D009975AA /* polkadot-9370metadata in Resources */ = {isa = PBXBuildFile; fileRef = FACD42BE2A5BE93D009975AA /* polkadot-9370metadata */; }; FACD42C12A5C10BB009975AA /* TransferService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACD42C02A5C10BB009975AA /* TransferService.swift */; }; FACE6C232BB6C37900643CEF /* LiquidityPoolListCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACE6C222BB6C37900643CEF /* LiquidityPoolListCellModel.swift */; }; + FACE6C652BBBBC8C00643CEF /* LiquidityPoolListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACE6C642BBBBC8C00643CEF /* LiquidityPoolListViewModel.swift */; }; FAD0068027EA252400C97E09 /* AboutViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD0067E27EA252400C97E09 /* AboutViewModelFactory.swift */; }; FAD0068127EA252400C97E09 /* AboutViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD0067F27EA252400C97E09 /* AboutViewState.swift */; }; FAD0068427EA255900C97E09 /* AboutTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD0068227EA255900C97E09 /* AboutTableViewCell.swift */; }; @@ -5992,6 +5993,7 @@ FACD42BE2A5BE93D009975AA /* polkadot-9370metadata */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "polkadot-9370metadata"; sourceTree = ""; }; FACD42C02A5C10BB009975AA /* TransferService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransferService.swift; sourceTree = ""; }; FACE6C222BB6C37900643CEF /* LiquidityPoolListCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidityPoolListCellModel.swift; sourceTree = ""; }; + FACE6C642BBBBC8C00643CEF /* LiquidityPoolListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidityPoolListViewModel.swift; sourceTree = ""; }; FAD0067E27EA252400C97E09 /* AboutViewModelFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutViewModelFactory.swift; sourceTree = ""; }; FAD0067F27EA252400C97E09 /* AboutViewState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutViewState.swift; sourceTree = ""; }; FAD0068227EA255900C97E09 /* AboutTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutTableViewCell.swift; sourceTree = ""; }; @@ -14963,6 +14965,7 @@ isa = PBXGroup; children = ( FACE6C222BB6C37900643CEF /* LiquidityPoolListCellModel.swift */, + FACE6C642BBBBC8C00643CEF /* LiquidityPoolListViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -18733,6 +18736,7 @@ 982BB3FA25BA6AD5443B24C6 /* NetworkIssuesNotificationPresenter.swift in Sources */, BF3753B93B3E94BE1ECB9E81 /* NetworkIssuesNotificationInteractor.swift in Sources */, 445F1F1FB3ECC47D8DD2FBEA /* NetworkIssuesNotificationViewController.swift in Sources */, + FACE6C652BBBBC8C00643CEF /* LiquidityPoolListViewModel.swift in Sources */, 737F71CCDF39E7A400EBB7C0 /* NetworkIssuesNotificationViewLayout.swift in Sources */, 38BFEDD4B9F31EF2532962BD /* NetworkIssuesNotificationAssembly.swift in Sources */, A4FE32D50E4B7CB5B53E0067 /* StakingPoolInfoProtocols.swift in Sources */, diff --git a/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListViewController.swift b/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListViewController.swift index ec301df3ca..7f2d9461f9 100644 --- a/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListViewController.swift +++ b/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListViewController.swift @@ -6,6 +6,7 @@ final class LiquidityPoolsListViewController: UIViewController, ViewHolder { // MARK: Private properties + private var cellModels: [LiquidityPoolListCellModel] = [] private let output: LiquidityPoolsListViewOutput // MARK: - Constructor @@ -33,17 +34,47 @@ final class LiquidityPoolsListViewController: UIViewController, ViewHolder { override func viewDidLoad() { super.viewDidLoad() output.didLoad(view: self) + + rootView.tableView.delegate = self + rootView.tableView.dataSource = self + rootView.tableView.registerClassForCell(LiquidityPoolListCell.self) } // MARK: - Private methods } +extension LiquidityPoolsListViewController: UITableViewDataSource, UITableViewDelegate { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + cellModels.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + return tableView.dequeueReusableCellWithType(LiquidityPoolListCell.self) ?? UITableViewCell() + } + + func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + guard let lpCell = cell as? LiquidityPoolListCell else { + return + } + + lpCell.bind(viewModel: cellModels[indexPath.row]) + } +} + // MARK: - LiquidityPoolsListViewInput -extension LiquidityPoolsListViewController: LiquidityPoolsListViewInput {} +extension LiquidityPoolsListViewController: LiquidityPoolsListViewInput { + func bind(viewModel: LiquidityPoolListViewModel) { + cellModels = viewModel.poolViewModels + + rootView.tableView.reloadData() + } +} // MARK: - Localizable extension LiquidityPoolsListViewController: Localizable { - func applyLocalization() {} + func applyLocalization() { + rootView.locale = selectedLocale + } } diff --git a/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift b/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift index 9f874449a9..6a7c8bd6e9 100644 --- a/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift +++ b/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift @@ -1,12 +1,81 @@ import UIKit +import SoraUI final class LiquidityPoolsListViewLayout: UIView { + let topBar: BorderedContainerView = { + let view = BorderedContainerView() + view.borderType = .bottom + view.fillColor = R.color.colorWhite8() + return view + }() + + let titleLabel: UILabel = { + let label = UILabel() + label.font = .h5Title + label.textColor = .white + return label + }() + + let moreButton: UIButton = { + let button = UIButton(type: .custom) + button.titleLabel.font = .capsTitle + button.setTitleColor(.white, for: .normal) + button.backgroundColor = R.color.colorWhite8() + return button + }() + + let tableView: UITableView = { + let tableView = UITableView() + return tableView + }() + + var locale: Locale = .current { + didSet { + applyLocalization() + } + } + override init(frame: CGRect) { super.init(frame: frame) + + drawSubviews() + setupConstraints() } @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") + + drawSubviews() + setupConstraints() + } + + func bind(viewModel: LiquidityPoolListViewModel) { + titleLabel.text = viewModel.titleLabelText + moreButton.isHidden = !viewModel.moreButtonVisible + } + + private func drawSubviews() { + addSubview(topBar) + addSubview(tableView) + + topBar.addSubview(titleLabel) + topBar.addSubview(moreButton) + } + + private func setupConstraints() { + topBar.snp.makeConstraints { make in + make.height.equalTo(42) + make.leading.trailing.top.equalToSuperview() + } + + tableView.snp.makeConstraints { make in + make.top.equalTo(topBar.snp.bottom).offset(8) + make.leading.trailing.bottom.equalToSuperview() + + } + } + + private func applyLocalization() { } } diff --git a/fearless/Modules/LiquidityPoolsList/View/LiquidityPoolListCell.swift b/fearless/Modules/LiquidityPoolsList/View/LiquidityPoolListCell.swift index 2040448e39..2143faecd6 100644 --- a/fearless/Modules/LiquidityPoolsList/View/LiquidityPoolListCell.swift +++ b/fearless/Modules/LiquidityPoolsList/View/LiquidityPoolListCell.swift @@ -50,6 +50,18 @@ class LiquidityPoolListCell: UITableViewCell { drawSubviews() setupConstraints() } + + func bind(viewModel: LiquidityPoolListCellModel) { + tokenPairIconsView.bind(viewModel: viewModel.tokenPairIconsVieWModel) + tokenPairNameLabel.text = viewModel.tokenPairNameLabelText + rewardTokenNameLabel.text = viewModel.rewardTokenNameLabelText + apyLabel.text = viewModel.apyLabelText + stakingStatusLabel.text = viewModel.stakingStatusLabelText + reservesLabel.text = viewModel.reservesLabelText + + stakingStatusLabel.isHidden = viewModel.stakingStatusLabelText == nil + reservesLabel.isHidden = viewModel.reservesLabelText == nil + } private func drawSubviews() { addSubview(tokenPairIconsView) diff --git a/fearless/Modules/LiquidityPoolsList/ViewModel/LiquidityPoolListCellModel.swift b/fearless/Modules/LiquidityPoolsList/ViewModel/LiquidityPoolListCellModel.swift index fecc4ab449..5876df7f94 100644 --- a/fearless/Modules/LiquidityPoolsList/ViewModel/LiquidityPoolListCellModel.swift +++ b/fearless/Modules/LiquidityPoolsList/ViewModel/LiquidityPoolListCellModel.swift @@ -1 +1,10 @@ import Foundation + +struct LiquidityPoolListCellModel { + let tokenPairIconsVieWModel: TokenPairsIconViewModel + let tokenPairNameLabelText: String + let rewardTokenNameLabelText: String + let apyLabelText: String + let stakingStatusLabelText: String? + let reservesLabelText: String? +} diff --git a/fearless/Modules/LiquidityPoolsList/ViewModel/LiquidityPoolListViewModel.swift b/fearless/Modules/LiquidityPoolsList/ViewModel/LiquidityPoolListViewModel.swift new file mode 100644 index 0000000000..e108ca5834 --- /dev/null +++ b/fearless/Modules/LiquidityPoolsList/ViewModel/LiquidityPoolListViewModel.swift @@ -0,0 +1,7 @@ +import Foundation + +struct LiquidityPoolListViewModel { + let poolViewModels: [LiquidityPoolListCellModel] + let titleLabelText: String + let moreButtonVisible: Bool +} diff --git a/fearless/en.lproj/Localizable.strings b/fearless/en.lproj/Localizable.strings index a42fe67446..21cd85002c 100644 --- a/fearless/en.lproj/Localizable.strings +++ b/fearless/en.lproj/Localizable.strings @@ -1268,3 +1268,4 @@ To find out more, contact %@"; "verify.phone.number.send.code" = "SEND CODE"; "balance.locks.blocked.row.title" = "Blocked"; "wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%s) on the destination account will be less than the minimal balance (%s). Please increase the amount by %s"; +"common.more" = "more"; diff --git a/fearless/id.lproj/Localizable.strings b/fearless/id.lproj/Localizable.strings index 8d1fede563..70c5aad6dd 100644 --- a/fearless/id.lproj/Localizable.strings +++ b/fearless/id.lproj/Localizable.strings @@ -1252,3 +1252,4 @@ To find out more, contact %@"; "verify.phone.number.send.code" = "SEND CODE"; "balance.locks.blocked.row.title" = "Blocked"; "wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%s) on the destination account will be less than the minimal balance (%s). Please increase the amount by %s"; +"common.more" = "more"; diff --git a/fearless/ja.lproj/Localizable.strings b/fearless/ja.lproj/Localizable.strings index 838d299386..65ff406944 100644 --- a/fearless/ja.lproj/Localizable.strings +++ b/fearless/ja.lproj/Localizable.strings @@ -1137,3 +1137,4 @@ "сurrencies.stub.text" = "通貨"; "balance.locks.blocked.row.title" = "Blocked"; "wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%s) on the destination account will be less than the minimal balance (%s). Please increase the amount by %s"; +"common.more" = "more"; diff --git a/fearless/pt-PT.lproj/Localizable.strings b/fearless/pt-PT.lproj/Localizable.strings index 4e2d9067d9..7dd5bdabc7 100644 --- a/fearless/pt-PT.lproj/Localizable.strings +++ b/fearless/pt-PT.lproj/Localizable.strings @@ -1273,3 +1273,4 @@ To find out more, contact %@"; "verify.phone.number.send.code" = "ENVIAR CÓDIGO"; "balance.locks.blocked.row.title" = "Blocked"; "wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%s) on the destination account will be less than the minimal balance (%s). Please increase the amount by %s"; +"common.more" = "more"; diff --git a/fearless/ru.lproj/Localizable.strings b/fearless/ru.lproj/Localizable.strings index cd886d81b5..e0a665238c 100644 --- a/fearless/ru.lproj/Localizable.strings +++ b/fearless/ru.lproj/Localizable.strings @@ -1266,3 +1266,4 @@ Euro cash"; "verify.phone.number.send.code" = "ОТПРАВИТЬ КОД"; "balance.locks.blocked.row.title" = "Заблокировано"; "wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%s) on the destination account will be less than the minimal balance (%s). Please increase the amount by %s"; +"common.more" = "more"; diff --git a/fearless/tr.lproj/Localizable.strings b/fearless/tr.lproj/Localizable.strings index fa895907dc..756d1a913b 100644 --- a/fearless/tr.lproj/Localizable.strings +++ b/fearless/tr.lproj/Localizable.strings @@ -1141,3 +1141,4 @@ ait olduğundan emin olun."; "сurrencies.stub.text" = "Para birimleri"; "balance.locks.blocked.row.title" = "Blocked"; "wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%s) on the destination account will be less than the minimal balance (%s). Please increase the amount by %s"; +"common.more" = "more"; diff --git a/fearless/vi.lproj/Localizable.strings b/fearless/vi.lproj/Localizable.strings index b7e3eb4a71..1b6139450e 100644 --- a/fearless/vi.lproj/Localizable.strings +++ b/fearless/vi.lproj/Localizable.strings @@ -1266,3 +1266,4 @@ Giữ, stake hoặc cung cấp thanh khoản cho XOR trị giá ít nhất €%@ "verify.phone.number.send.code" = "GỬI MÃ"; "balance.locks.blocked.row.title" = "Blocked"; "wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%s) on the destination account will be less than the minimal balance (%s). Please increase the amount by %s"; +"common.more" = "more"; diff --git a/fearless/zh-Hans.lproj/Localizable.strings b/fearless/zh-Hans.lproj/Localizable.strings index 043efc3456..c58e5bfa04 100644 --- a/fearless/zh-Hans.lproj/Localizable.strings +++ b/fearless/zh-Hans.lproj/Localizable.strings @@ -1264,3 +1264,4 @@ "verify.phone.number.send.code" = "发送验证码"; "balance.locks.blocked.row.title" = "Blocked"; "wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%s) on the destination account will be less than the minimal balance (%s). Please increase the amount by %s"; +"common.more" = "more"; From 3c551ef416a486884c833a2034204c60fc5326d7 Mon Sep 17 00:00:00 2001 From: Alex Lebedko Date: Tue, 16 Apr 2024 16:08:40 +0700 Subject: [PATCH 004/115] chainregistry spm --- .../Services/CrowdloanService.swift | 1 + .../EquilibriumTotalWalletService.swift | 1 + .../Services/NetworkInfoFetching.swift | 1 + .../WalletConnectPolkadorSigner.swift | 1 + .../StorageKeyDataExtractor.swift | 1 + .../Sources/StorageProviderSource.swift | 1 + .../Sources/WebSocketProviderSource.swift | 1 + .../WalletLocalSubscriptionFactory.swift | 2 + .../Operation/StorageDecodingOperation.swift | 1 + .../StorageKeyEncodingOperation.swift | 4 +- .../Operation/StorageRequestFactory.swift | 1 + .../Protocols/RuntimeConstantFetching.swift | 1 + .../ChainRegistry/ChainRegistry.swift | 65 ++++++++++++ .../RuntimeHotBootSnapshotFactory.swift | 3 +- .../RuntimeProviderPool/RuntimeProvider.swift | 99 +++++++++++++------ .../RuntimeProviderFactory.swift | 1 + .../RuntimeProviderPool.swift | 1 + .../RuntimeProviderPool/RuntimeSnapshot.swift | 24 ++--- .../RuntimeSnapshotOperationFactory.swift | 3 +- .../RuntimeSpecVersion.swift | 84 ++++++++-------- ...edControllerStashAccountCheckService.swift | 2 + .../ExtrinsicEraOperationFactory.swift | 1 + .../ExtrinsicOperationFactory.swift | 4 +- .../ExtrinsicService/ExtrinsicService.swift | 2 + .../ImmortalEraOperationFactory.swift | 1 + .../MortalEraOperationFactory.swift | 1 + .../PayoutRewardsService+Fetch.swift | 1 + .../PayoutRewardsService.swift | 1 + .../PoolStakingAccountSubscription.swift | 1 + .../RemoteSubscriptionRequests.swift | 1 + .../BaseStakingAccountResolver.swift | 1 + .../StakingAccountResolverV13.swift | 1 + .../StakingAccountResolverV14.swift | 1 + .../BaseStakingAccountSubscription.swift | 1 + .../RuntimeCoderFactory.swift | 70 ++++++------- .../RuntimeRegistryServiceProtocol.swift | 44 ++++----- .../Common/Services/ServiceCoordinator.swift | 59 ++++++++++- .../StorageKeySuffixMapper.swift | 1 + .../ExtrinsicProcessing.swift | 1 + .../TransactionSubscription.swift | 1 + .../Storage/StorageFacadeProtocol.swift | 13 +++ .../Storage/SubstrateDataStorageFacade.swift | 13 +++ .../Storage/UserDataStorageFacade.swift | 13 +++ .../Common/Substrate/Types/AccountInfo.swift | 7 ++ .../Substrate/Types/StorageCodingPath.swift | 3 + .../PrefixStorageResponseValueExtractor.swift | 1 + .../SoraSubsquidHistoryOperationFactory.swift | 1 + .../SubqueryHistoryOperationFactory.swift | 1 + .../PolkaswapOperationFactoryProtocol.swift | 1 + .../StakingPoolOperationFactory.swift | 1 + .../BalanceInfoDependencyContainer.swift | 1 + .../BalanceInfo/BalanceInfoInteractor.swift | 1 + .../CrowdloanContributionInteractor.swift | 1 + ...owdloanContributionConfirmInteractor.swift | 1 + .../CrowdloanListInteractor.swift | 1 + .../Operation/CrowdloanOperationFactory.swift | 1 + .../Send/SendDependencyContainer.swift | 10 +- .../AnalyticsValidatorsInteractor.swift | 1 + .../ControllerAccountInteractor.swift | 1 + ...trollerAccountConfirmationInteractor.swift | 1 + .../Staking/Model/ChainStakingSettings.swift | 1 + .../EraCountdownOperationFactory.swift | 2 + .../Operations/IdentityOperationFactory.swift | 1 + .../Operations/SlashesOperationFactory.swift | 1 + .../Operations/StakingDurationFetching.swift | 1 + .../StakingDurationOperationFactory.swift | 1 + ...ParachainStakingInfoOperationFactory.swift | 1 + ...elaychainStakingInfoOperationFactory.swift | 1 + .../StakingOperationFactory.swift | 1 + .../ParachainCollatorOperationFactory.swift | 2 +- .../RelaychainValidatorOperationFactory.swift | 1 + ...ctValidatorsConfirmParachainStrategy.swift | 1 + .../SelectValidatorsConfirmPoolStrategy.swift | 1 + ...lidatorsConfirmPoolInitiatedStrategy.swift | 1 + ...orsConfirmRelaychainExistingStrategy.swift | 1 + ...rsConfirmRelaychainInitiatedStrategy.swift | 1 + .../SelectValidatorsConfirmViewFactory.swift | 1 + ...lectValidatorsStartParachainStrategy.swift | 1 + .../SelectValidatorsStartPoolStrategy.swift | 1 + ...ectValidatorsStartRelaychainStrategy.swift | 1 + .../Pool/YourValidatorListPoolStrategy.swift | 1 + .../YourValidatorListRelaychainStrategy.swift | 1 + .../EraValidatorService+Fetch.swift | 1 + .../PortionRewardCalculatorService.swift | 1 + .../StakingAmountParachainStrategy.swift | 1 + .../StakingAmountRelaychainStrategy.swift | 1 + .../StakingAmountInteractor.swift | 1 + .../StakingBalanceRelaychainStrategy.swift | 1 + .../StakingBondMoreParachainStrategy.swift | 1 + .../Pool/StakingBondMorePoolStrategy.swift | 1 + .../StakingBondMoreRelaychainStrategy.swift | 1 + ...ondMoreConfirmationParachainStrategy.swift | 1 + ...kingBondMoreConfirmationPoolStrategy.swift | 1 + ...ndMoreConfirmationRelaychainStrategy.swift | 1 + .../StakingMain/StakingMainInteractor.swift | 1 + ...takingPayoutConfirmationPoolStrategy.swift | 1 + ...PayoutConfirmationrelaychainStrategy.swift | 1 + ...gRebondConfirmationParachainStrategy.swift | 1 + ...RebondConfirmationRelaychainStrategy.swift | 1 + .../StakingRebondSetupInteractor.swift | 1 + .../StakingRedeemParachainStrategy.swift | 1 + .../Flow/Pool/StakingRedeemPoolStrategy.swift | 1 + .../StakingRedeemRelaychainStrategy.swift | 1 + ...gRedeemConfirmationParachainStrategy.swift | 1 + ...akingRedeemConfirmation]PoolStrategy.swift | 1 + ...RedeemConfirmationRelaychainStrategy.swift | 1 + .../StakingRewardDestConfirmInteractor.swift | 1 + .../StakingRewardDestSetupInteractor.swift | 1 + .../StakingRewardPayoutsInteractor.swift | 1 + ...takingUnbondConfirmParachainStrategy.swift | 1 + .../StakingUnbondConfirmPoolStrategy.swift | 1 + ...akingUnbondConfirmRelaychainStrategy.swift | 1 + .../StakingUnbondSetupParachainStrategy.swift | 1 + .../Pool/StakingUnbondSetupPoolStrategy.swift | 1 + ...StakingUnbondSetupRelaychainStrategy.swift | 1 + .../StakingPoolJoinConfirmInteractor.swift | 1 + .../StakingPoolStartInteractor.swift | 1 + .../StakingPoolInfoInteractor.swift | 1 + .../StakingPoolManagementInteractor.swift | 1 + 119 files changed, 469 insertions(+), 156 deletions(-) diff --git a/fearless/ApplicationLayer/Services/CrowdloanService.swift b/fearless/ApplicationLayer/Services/CrowdloanService.swift index dd5c6b7120..bc48385919 100644 --- a/fearless/ApplicationLayer/Services/CrowdloanService.swift +++ b/fearless/ApplicationLayer/Services/CrowdloanService.swift @@ -2,6 +2,7 @@ import Foundation import SSFUtils import SSFModels import RobinHood +import SSFRuntimeCodingService protocol CrowdloanService { func fetchContributions(accountId: AccountId) async throws -> CrowdloanContributionDict diff --git a/fearless/ApplicationLayer/Services/Equilibrium/EquilibriumTotalWalletService.swift b/fearless/ApplicationLayer/Services/Equilibrium/EquilibriumTotalWalletService.swift index a7bd20c5fb..bfdeb96317 100644 --- a/fearless/ApplicationLayer/Services/Equilibrium/EquilibriumTotalWalletService.swift +++ b/fearless/ApplicationLayer/Services/Equilibrium/EquilibriumTotalWalletService.swift @@ -3,6 +3,7 @@ import SSFUtils import RobinHood import BigInt import SSFModels +import SSFRuntimeCodingService protocol EquilibriumTotalBalanceServiceProtocol { var accountInfos: [ChainAssetKey: AccountInfo?] { get } diff --git a/fearless/ApplicationLayer/Services/NetworkInfoFetching.swift b/fearless/ApplicationLayer/Services/NetworkInfoFetching.swift index 6a318a32e3..40fcd74ec7 100644 --- a/fearless/ApplicationLayer/Services/NetworkInfoFetching.swift +++ b/fearless/ApplicationLayer/Services/NetworkInfoFetching.swift @@ -1,6 +1,7 @@ import Foundation import SSFUtils import RobinHood +import SSFRuntimeCodingService protocol NetworkInfoFetching { func fetchCurrentBlock( diff --git a/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectPolkadorSigner.swift b/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectPolkadorSigner.swift index ac2a5f1d13..55156cb618 100644 --- a/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectPolkadorSigner.swift +++ b/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectPolkadorSigner.swift @@ -3,6 +3,7 @@ import Commons import SSFSigner import SSFUtils import SSFModels +import SSFRuntimeCodingService final class WalletConnectPolkadorSigner: WalletConnectPayloadSigner { enum SignType { diff --git a/fearless/ApplicationLayer/StorageKeyDataExtractor.swift b/fearless/ApplicationLayer/StorageKeyDataExtractor.swift index a5d2791c5d..a0e383203d 100644 --- a/fearless/ApplicationLayer/StorageKeyDataExtractor.swift +++ b/fearless/ApplicationLayer/StorageKeyDataExtractor.swift @@ -1,5 +1,6 @@ import Foundation import SSFUtils +import SSFRuntimeCodingService final class StorageKeyDataExtractor { private let runtimeService: RuntimeCodingServiceProtocol diff --git a/fearless/Common/DataProvider/Sources/StorageProviderSource.swift b/fearless/Common/DataProvider/Sources/StorageProviderSource.swift index 7fda0ab24c..8126d0c6d5 100644 --- a/fearless/Common/DataProvider/Sources/StorageProviderSource.swift +++ b/fearless/Common/DataProvider/Sources/StorageProviderSource.swift @@ -1,6 +1,7 @@ import Foundation import RobinHood import SSFUtils +import SSFRuntimeCodingService final class StorageProviderSource: DataProviderSourceProtocol { enum LastSeen: Equatable { diff --git a/fearless/Common/DataProvider/Sources/WebSocketProviderSource.swift b/fearless/Common/DataProvider/Sources/WebSocketProviderSource.swift index ee7cb94853..e49900ecae 100644 --- a/fearless/Common/DataProvider/Sources/WebSocketProviderSource.swift +++ b/fearless/Common/DataProvider/Sources/WebSocketProviderSource.swift @@ -1,6 +1,7 @@ import Foundation import RobinHood import SSFUtils +import SSFRuntimeCodingService typealias WebSocketProviderKeyClosure = (@escaping () throws -> RuntimeCoderFactoryProtocol) throws -> BaseOperation<[Data]> diff --git a/fearless/Common/DataProvider/WalletLocalSubscriptionFactory.swift b/fearless/Common/DataProvider/WalletLocalSubscriptionFactory.swift index 5669198542..2919766c3d 100644 --- a/fearless/Common/DataProvider/WalletLocalSubscriptionFactory.swift +++ b/fearless/Common/DataProvider/WalletLocalSubscriptionFactory.swift @@ -1,6 +1,8 @@ import Foundation import RobinHood import SSFModels +import SSFAccountManagmentStorage +import SSFRuntimeCodingService protocol WalletLocalSubscriptionFactoryProtocol { var operationManager: OperationManagerProtocol { get } diff --git a/fearless/Common/Operation/StorageDecodingOperation.swift b/fearless/Common/Operation/StorageDecodingOperation.swift index cb7695a5de..b76fade666 100644 --- a/fearless/Common/Operation/StorageDecodingOperation.swift +++ b/fearless/Common/Operation/StorageDecodingOperation.swift @@ -1,6 +1,7 @@ import Foundation import SSFUtils import RobinHood +import SSFRuntimeCodingService enum StorageDecodingOperationError: Error { case missingRequiredParams diff --git a/fearless/Common/Operation/StorageKeyEncodingOperation.swift b/fearless/Common/Operation/StorageKeyEncodingOperation.swift index b99ff5e65f..2601ad8961 100644 --- a/fearless/Common/Operation/StorageKeyEncodingOperation.swift +++ b/fearless/Common/Operation/StorageKeyEncodingOperation.swift @@ -1,12 +1,14 @@ import Foundation import SSFUtils import RobinHood +import SSFStorageQueryKit +import SSFRuntimeCodingService protocol NMapKeyParamProtocol { func encode(encoder: DynamicScaleEncoding, type: String) throws -> Data } -struct NMapKeyParam: NMapKeyParamProtocol { +struct NMapKeyParam: NMapKeyParamProtocol, SSFStorageQueryKit.NMapKeyParamProtocol { var value: T func encode(encoder: DynamicScaleEncoding, type: String) throws -> Data { diff --git a/fearless/Common/Operation/StorageRequestFactory.swift b/fearless/Common/Operation/StorageRequestFactory.swift index 294e3f5ad7..79cccd9216 100644 --- a/fearless/Common/Operation/StorageRequestFactory.swift +++ b/fearless/Common/Operation/StorageRequestFactory.swift @@ -1,6 +1,7 @@ import Foundation import RobinHood import SSFUtils +import SSFRuntimeCodingService struct StorageResponse { let key: Data diff --git a/fearless/Common/Protocols/RuntimeConstantFetching.swift b/fearless/Common/Protocols/RuntimeConstantFetching.swift index 90d2418dca..9b6902fdd9 100644 --- a/fearless/Common/Protocols/RuntimeConstantFetching.swift +++ b/fearless/Common/Protocols/RuntimeConstantFetching.swift @@ -1,6 +1,7 @@ import Foundation import RobinHood import SSFUtils +import SSFRuntimeCodingService protocol RuntimeConstantFetching { func fetchConstant( diff --git a/fearless/Common/Services/ChainRegistry/ChainRegistry.swift b/fearless/Common/Services/ChainRegistry/ChainRegistry.swift index 4f7241d9b4..1f7dfbae8d 100644 --- a/fearless/Common/Services/ChainRegistry/ChainRegistry.swift +++ b/fearless/Common/Services/ChainRegistry/ChainRegistry.swift @@ -3,6 +3,9 @@ import RobinHood import SSFUtils import SSFModels import Web3 +import SSFChainRegistry +import SSFRuntimeCodingService +import SSFChainConnection protocol ChainRegistryProtocol: AnyObject { var availableChainIds: Set? { get } @@ -416,3 +419,65 @@ extension ChainRegistry: EventVisitorProtocol { chainsTypesMap = event.versioningMap } } + +extension ChainRegistry: SSFChainRegistry.ChainRegistryProtocol { + func getRuntimeProvider( + chainId: SSFModels.ChainModel.Id, + usedRuntimePaths _: [String: [String]], + runtimeItem _: SSFModels.RuntimeMetadataItemProtocol? + ) async throws -> SSFRuntimeCodingService.RuntimeProviderProtocol { + let runtimeProvider = readLock.concurrentlyRead { runtimeProviderPool.getRuntimeProvider(for: chainId) } + guard let runtimeProvider else { + throw ChainRegistryError.runtimeMetadaUnavailable + } + return runtimeProvider + } + +// func getRuntimeProvider(for chainId: SSFModels.ChainModel.Id) -> SSFRuntimeCodingService.RuntimeProviderProtocol? { +// let runtimeProvider = readLock.concurrentlyRead { runtimeProviderPool.getRuntimeProvider(for: chainId) } +// return runtimeProvider +// } + + func getSubstrateConnection(for chain: SSFModels.ChainModel) throws -> SSFChainConnection.SubstrateConnection { + let connection = getConnection(for: chain.chainId) + guard let connection else { + throw ChainRegistryError.connectionUnavailable + } + + return connection + } + + func getEthereumConnection(for chain: SSFModels.ChainModel) throws -> SSFChainConnection.Web3EthConnection { + let connection = getEthereumConnection(for: chain.chainId) + guard let connection else { + throw ChainRegistryError.connectionUnavailable + } + return connection + } + + func getChain(for chainId: SSFModels.ChainModel.Id) async throws -> SSFModels.ChainModel { + let chain = readLock.concurrentlyRead { chains.first(where: { $0.chainId == chainId }) } + + guard let chain else { + throw ChainRegistryError.connectionUnavailable + } + + return chain + } + + func getChains() async throws -> [SSFModels.ChainModel] { + availableChains + } + + func getReadySnapshot( + chainId: SSFModels.ChainModel.Id, + usedRuntimePaths _: [String: [String]], + runtimeItem _: SSFModels.RuntimeMetadataItemProtocol? + ) async throws -> SSFRuntimeCodingService.RuntimeSnapshot { + let runtimeService = try await getRuntimeProvider(chainId: chainId, usedRuntimePaths: [:], runtimeItem: nil) + guard let runtimeSnapshot = runtimeService.snapshot else { + throw ChainRegistryError.runtimeMetadaUnavailable + } + return runtimeSnapshot + } +} diff --git a/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeHotBootSnapshotFactory.swift b/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeHotBootSnapshotFactory.swift index 537d702318..fce604409e 100644 --- a/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeHotBootSnapshotFactory.swift +++ b/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeHotBootSnapshotFactory.swift @@ -2,6 +2,7 @@ import Foundation import SSFUtils import RobinHood import SSFModels +import SSFRuntimeCodingService protocol RuntimeHotBootSnapshotFactoryProtocol { func createRuntimeSnapshotWrapper( @@ -45,8 +46,6 @@ final class RuntimeHotBootSnapshotFactory: RuntimeHotBootSnapshotFactoryProtocol ) return RuntimeSnapshot( - localCommonHash: nil, - localChainTypes: chainTypes, typeRegistryCatalog: catalog, specVersion: strongSelf.runtimeItem.version, txVersion: strongSelf.runtimeItem.txVersion, diff --git a/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeProvider.swift b/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeProvider.swift index 6eaa4b0869..725cb8ebdb 100644 --- a/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeProvider.swift +++ b/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeProvider.swift @@ -2,21 +2,22 @@ import Foundation import RobinHood import SSFUtils import SSFModels +import SSFRuntimeCodingService // swiftlint:disable file_length -protocol RuntimeProviderProtocol: AnyObject, RuntimeCodingServiceProtocol { - var chainId: ChainModel.Id { get } - var snapshot: RuntimeSnapshot? { get } - var runtimeSpecVersion: RuntimeSpecVersion { get } - - func setup() - func setupHot() - func cleanup() - func fetchCoderFactoryOperation( - with timeout: TimeInterval, - closure: RuntimeMetadataClosure? - ) -> BaseOperation -} +// protocol RuntimeProviderProtocol: AnyObject, RuntimeCodingServiceProtocol { +// var chainId: ChainModel.Id { get } +// var snapshot: RuntimeSnapshot? { get } +// var runtimeSpecVersion: RuntimeSpecVersion { get } +// +// func setup() +// func setupHot() +// func cleanup() +// func fetchCoderFactoryOperation( +// with timeout: TimeInterval, +// closure: RuntimeMetadataClosure? +// ) -> BaseOperation +// } enum RuntimeProviderError: Error { case providerUnavailable @@ -215,21 +216,6 @@ final class RuntimeProvider { } } - func fetchCoderFactoryOperation() -> BaseOperation { - AwaitOperation { [weak self] in - try await withCheckedThrowingContinuation { continuation in - self?.fetchCoderFactory(runCompletionIn: nil) { factory in - guard let factory = factory else { - continuation.resume(with: .failure(RuntimeProviderError.providerUnavailable)) - return - } - - continuation.resume(with: .success(factory)) - } - } - } - } - func fetchCoderFactoryOperation( with _: TimeInterval, closure _: RuntimeMetadataClosure? @@ -263,6 +249,59 @@ final class RuntimeProvider { } extension RuntimeProvider: RuntimeProviderProtocol { + var runtimeSpecVersion: SSFRuntimeCodingService.RuntimeSpecVersion { + runtimeSnapshot?.runtimeSpecVersion ?? RuntimeSpecVersion.defaultVersion + } + + func readySnapshot() async throws -> SSFRuntimeCodingService.RuntimeSnapshot { + guard + let chainTypes = chainTypes, + let chainMetadata = initialChainMetadata + else { + throw RuntimeProviderError.providerUnavailable + } + let wrapper = snapshotOperationFactory.createRuntimeSnapshotWrapper( + chainTypes: chainTypes, + chainMetadata: chainMetadata, + usedRuntimePaths: usedRuntimePaths + ) + currentWrapper = wrapper + operationQueue.addOperation(wrapper) + + return try await withUnsafeThrowingContinuation { continuation in + wrapper.completionBlock = { [weak self] in + let result = wrapper.result + self?.handleCompletion(result: result) + switch result { + case let .success(snapshot): + guard let snapshot = snapshot else { + return continuation.resume(throwing: RuntimeProviderError.providerUnavailable) + } + return continuation.resume(returning: snapshot) + case let .failure(error): + return continuation.resume(throwing: error) + case .none: + return continuation.resume(throwing: RuntimeProviderError.providerUnavailable) + } + } + } + } + + func fetchCoderFactoryOperation() -> BaseOperation { + AwaitOperation { [weak self] in + try await withCheckedThrowingContinuation { continuation in + self?.fetchCoderFactory(runCompletionIn: nil) { factory in + guard let factory = factory else { + continuation.resume(with: .failure(RuntimeProviderError.providerUnavailable)) + return + } + + continuation.resume(with: .success(factory)) + } + } + } + } + func setupHot() { mutex.lock() @@ -281,10 +320,6 @@ extension RuntimeProvider: RuntimeProviderProtocol { snapshot } - var runtimeSpecVersion: RuntimeSpecVersion { - snapshot?.runtimeSpecVersion(for: chainModel) ?? RuntimeSpecVersion.defaultVersion(for: chainModel) - } - func setup() { mutex.lock() diff --git a/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeProviderFactory.swift b/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeProviderFactory.swift index ccb1e6112e..822c799599 100644 --- a/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeProviderFactory.swift +++ b/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeProviderFactory.swift @@ -1,6 +1,7 @@ import Foundation import RobinHood import SSFModels +import SSFRuntimeCodingService protocol RuntimeProviderFactoryProtocol { func createRuntimeProvider( diff --git a/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeProviderPool.swift b/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeProviderPool.swift index 57af8c7e14..0327906bfc 100644 --- a/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeProviderPool.swift +++ b/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeProviderPool.swift @@ -1,5 +1,6 @@ import Foundation import SSFModels +import SSFRuntimeCodingService protocol RuntimeProviderPoolProtocol { @discardableResult diff --git a/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeSnapshot.swift b/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeSnapshot.swift index 25b7145507..a7a62c2032 100644 --- a/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeSnapshot.swift +++ b/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeSnapshot.swift @@ -2,15 +2,15 @@ import Foundation import SSFUtils import SSFModels -struct RuntimeSnapshot { - let localCommonHash: String? - let localChainTypes: Data? - let typeRegistryCatalog: TypeRegistryCatalogProtocol - let specVersion: UInt32 - let txVersion: UInt32 - let metadata: RuntimeMetadata - - func runtimeSpecVersion(for chain: ChainModel) -> RuntimeSpecVersion { - RuntimeSpecVersion.version(for: chain, rawValue: specVersion) ?? RuntimeSpecVersion.defaultVersion(for: chain) - } -} +// struct RuntimeSnapshot { +// let localCommonHash: String? +// let localChainTypes: Data? +// let typeRegistryCatalog: TypeRegistryCatalogProtocol +// let specVersion: UInt32 +// let txVersion: UInt32 +// let metadata: RuntimeMetadata +// +// func runtimeSpecVersion(for chain: ChainModel) -> RuntimeSpecVersion { +// RuntimeSpecVersion.version(for: chain, rawValue: specVersion) ?? RuntimeSpecVersion.defaultVersion(for: chain) +// } +// } diff --git a/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeSnapshotOperationFactory.swift b/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeSnapshotOperationFactory.swift index 7783f672fb..23774c3678 100644 --- a/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeSnapshotOperationFactory.swift +++ b/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeSnapshotOperationFactory.swift @@ -2,6 +2,7 @@ import Foundation import SSFUtils import RobinHood import SSFModels +import SSFRuntimeCodingService protocol RuntimeSnapshotFactoryProtocol { func createRuntimeSnapshotWrapper( @@ -45,8 +46,6 @@ final class RuntimeSnapshotFactory { ) return RuntimeSnapshot( - localCommonHash: nil, - localChainTypes: ownTypes, typeRegistryCatalog: catalog, specVersion: runtimeMetadataItem.version, txVersion: runtimeMetadataItem.txVersion, diff --git a/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeSpecVersion.swift b/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeSpecVersion.swift index 1ea5631e74..9f5f9818ea 100644 --- a/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeSpecVersion.swift +++ b/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeSpecVersion.swift @@ -1,45 +1,45 @@ import Foundation import SSFModels -enum RuntimeSpecVersion: UInt32, CaseIterable { - case v9370 = 9370 - case v9380 = 9380 - case v9390 = 9390 - case v9420 = 9420 - case v9430 = 9430 - - static func defaultVersion(for chain: ChainModel) -> RuntimeSpecVersion { - if chain.isPolkadotOrKusama || chain.isWestend { - return RuntimeSpecVersion.allCases.last ?? .v9430 - } else { - return .v9370 - } - } - - static func version(for chain: ChainModel, rawValue: UInt32) -> RuntimeSpecVersion? { - switch rawValue { - case 9370: - return .v9370 - case 9380: - return .v9380 - case 9390: - return .v9390 - case 9420: - return .v9420 - case 9430: - return .v9430 - default: - return defaultVersion(for: chain) - } - } - - // Helper methods - - func higherOrEqualThan(_ version: RuntimeSpecVersion) -> Bool { - rawValue >= version.rawValue - } - - func lowerOrEqualThan(_ version: RuntimeSpecVersion) -> Bool { - rawValue <= version.rawValue - } -} +// enum RuntimeSpecVersion: UInt32, CaseIterable { +// case v9370 = 9370 +// case v9380 = 9380 +// case v9390 = 9390 +// case v9420 = 9420 +// case v9430 = 9430 +// +// static func defaultVersion(for chain: ChainModel) -> RuntimeSpecVersion { +// if chain.isPolkadotOrKusama || chain.isWestend { +// return RuntimeSpecVersion.allCases.last ?? .v9430 +// } else { +// return .v9370 +// } +// } +// +// static func version(for chain: ChainModel, rawValue: UInt32) -> RuntimeSpecVersion? { +// switch rawValue { +// case 9370: +// return .v9370 +// case 9380: +// return .v9380 +// case 9390: +// return .v9390 +// case 9420: +// return .v9420 +// case 9430: +// return .v9430 +// default: +// return defaultVersion(for: chain) +// } +// } +// +// // Helper methods +// +// func higherOrEqualThan(_ version: RuntimeSpecVersion) -> Bool { +// rawValue >= version.rawValue +// } +// +// func lowerOrEqualThan(_ version: RuntimeSpecVersion) -> Bool { +// rawValue <= version.rawValue +// } +// } diff --git a/fearless/Common/Services/DeprecatedControllerStashAccountCheckService.swift b/fearless/Common/Services/DeprecatedControllerStashAccountCheckService.swift index 01170aa811..23b5d59352 100644 --- a/fearless/Common/Services/DeprecatedControllerStashAccountCheckService.swift +++ b/fearless/Common/Services/DeprecatedControllerStashAccountCheckService.swift @@ -1,6 +1,8 @@ import SSFModels import RobinHood import SSFUtils +import Foundation +import SSFRuntimeCodingService enum DeprecatedAccountIssue { case controller(issue: ControllerAccountIssue) diff --git a/fearless/Common/Services/ExtrinsicService/ExtrinsicEraOperationFactory.swift b/fearless/Common/Services/ExtrinsicService/ExtrinsicEraOperationFactory.swift index 1eb9fdfdf2..4efbcf5944 100644 --- a/fearless/Common/Services/ExtrinsicService/ExtrinsicEraOperationFactory.swift +++ b/fearless/Common/Services/ExtrinsicService/ExtrinsicEraOperationFactory.swift @@ -1,6 +1,7 @@ import Foundation import RobinHood import SSFUtils +import SSFRuntimeCodingService struct ExtrinsicEraParameters { let blockNumber: BlockNumber diff --git a/fearless/Common/Services/ExtrinsicService/ExtrinsicOperationFactory.swift b/fearless/Common/Services/ExtrinsicService/ExtrinsicOperationFactory.swift index 48bd5f302a..f4c2cc28b7 100644 --- a/fearless/Common/Services/ExtrinsicService/ExtrinsicOperationFactory.swift +++ b/fearless/Common/Services/ExtrinsicService/ExtrinsicOperationFactory.swift @@ -2,6 +2,8 @@ import Foundation import RobinHood import SSFUtils import IrohaCrypto +import SSFModels +import SSFRuntimeCodingService typealias ExtrinsicBuilderClosure = (ExtrinsicBuilderProtocol) throws -> (ExtrinsicBuilderProtocol) typealias ExtrinsicBuilderIndexedClosure = (ExtrinsicBuilderProtocol, Int) throws -> (ExtrinsicBuilderProtocol) @@ -213,7 +215,7 @@ final class ExtrinsicOperationFactory { builder = try customClosure(builder, index).signing( by: signingClosure, - of: currentCryptoType.utilsType, + of: currentCryptoType, using: codingFactory.createEncoder(), metadata: codingFactory.metadata ) diff --git a/fearless/Common/Services/ExtrinsicService/ExtrinsicService.swift b/fearless/Common/Services/ExtrinsicService/ExtrinsicService.swift index 2cfe13c3d4..09bf2c1740 100644 --- a/fearless/Common/Services/ExtrinsicService/ExtrinsicService.swift +++ b/fearless/Common/Services/ExtrinsicService/ExtrinsicService.swift @@ -2,6 +2,8 @@ import Foundation import SSFUtils import RobinHood import IrohaCrypto +import SSFModels +import SSFRuntimeCodingService typealias FeeExtrinsicResult = Result typealias EstimateFeeClosure = (FeeExtrinsicResult) -> Void diff --git a/fearless/Common/Services/ExtrinsicService/ImmortalEraOperationFactory.swift b/fearless/Common/Services/ExtrinsicService/ImmortalEraOperationFactory.swift index 23827f476b..ec73ec5bb7 100644 --- a/fearless/Common/Services/ExtrinsicService/ImmortalEraOperationFactory.swift +++ b/fearless/Common/Services/ExtrinsicService/ImmortalEraOperationFactory.swift @@ -1,6 +1,7 @@ import Foundation import RobinHood import SSFUtils +import SSFRuntimeCodingService final class ImmortalEraOperationFactory: ExtrinsicEraOperationFactoryProtocol { func createOperation( diff --git a/fearless/Common/Services/ExtrinsicService/MortalEraOperationFactory.swift b/fearless/Common/Services/ExtrinsicService/MortalEraOperationFactory.swift index 85989f5bab..bb3869e34f 100644 --- a/fearless/Common/Services/ExtrinsicService/MortalEraOperationFactory.swift +++ b/fearless/Common/Services/ExtrinsicService/MortalEraOperationFactory.swift @@ -2,6 +2,7 @@ import Foundation import RobinHood import SSFUtils import BigInt +import SSFRuntimeCodingService final class MortalEraOperationFactory { static let fallbackMaxHashCount: BlockNumber = 250 diff --git a/fearless/Common/Services/PayoutRewardsService/PayoutRewardsService+Fetch.swift b/fearless/Common/Services/PayoutRewardsService/PayoutRewardsService+Fetch.swift index dcf748ee6f..3a0b82b445 100644 --- a/fearless/Common/Services/PayoutRewardsService/PayoutRewardsService+Fetch.swift +++ b/fearless/Common/Services/PayoutRewardsService/PayoutRewardsService+Fetch.swift @@ -2,6 +2,7 @@ import RobinHood import SSFUtils import BigInt import IrohaCrypto +import SSFRuntimeCodingService extension PayoutRewardsService { func createChainHistoryRangeOperationWrapper( diff --git a/fearless/Common/Services/PayoutRewardsService/PayoutRewardsService.swift b/fearless/Common/Services/PayoutRewardsService/PayoutRewardsService.swift index 7c7027e3eb..48493e9307 100644 --- a/fearless/Common/Services/PayoutRewardsService/PayoutRewardsService.swift +++ b/fearless/Common/Services/PayoutRewardsService/PayoutRewardsService.swift @@ -4,6 +4,7 @@ import RobinHood import BigInt import IrohaCrypto import SSFModels +import SSFRuntimeCodingService final class PayoutRewardsService: PayoutRewardsServiceProtocol { let selectedAccountAddress: String diff --git a/fearless/Common/Services/RemoteSubscription/PoolStakingAccountSubscription.swift b/fearless/Common/Services/RemoteSubscription/PoolStakingAccountSubscription.swift index 13b2b51529..38f29c29e9 100644 --- a/fearless/Common/Services/RemoteSubscription/PoolStakingAccountSubscription.swift +++ b/fearless/Common/Services/RemoteSubscription/PoolStakingAccountSubscription.swift @@ -3,6 +3,7 @@ import RobinHood import IrohaCrypto import SSFUtils import SSFModels +import SSFRuntimeCodingService protocol PoolStakingAccountSubscriptionProtocol { func subscribeRemote() diff --git a/fearless/Common/Services/RemoteSubscription/RemoteSubscriptionRequests.swift b/fearless/Common/Services/RemoteSubscription/RemoteSubscriptionRequests.swift index 08f6dfcec6..a32580f944 100644 --- a/fearless/Common/Services/RemoteSubscription/RemoteSubscriptionRequests.swift +++ b/fearless/Common/Services/RemoteSubscription/RemoteSubscriptionRequests.swift @@ -1,6 +1,7 @@ import Foundation import RobinHood import SSFUtils +import SSFRuntimeCodingService protocol SubscriptionRequestProtocol { var localKey: String { get } diff --git a/fearless/Common/Services/RemoteSubscription/StakingAccountResolving/BaseStakingAccountResolver.swift b/fearless/Common/Services/RemoteSubscription/StakingAccountResolving/BaseStakingAccountResolver.swift index 48b15fd500..a65feda041 100644 --- a/fearless/Common/Services/RemoteSubscription/StakingAccountResolving/BaseStakingAccountResolver.swift +++ b/fearless/Common/Services/RemoteSubscription/StakingAccountResolving/BaseStakingAccountResolver.swift @@ -3,6 +3,7 @@ import RobinHood import IrohaCrypto import SSFUtils import SSFModels +import SSFRuntimeCodingService class BaseStakingAccountResolver: StakingAccountResolver { struct Subscription { diff --git a/fearless/Common/Services/RemoteSubscription/StakingAccountResolving/StakingAccountResolverV13.swift b/fearless/Common/Services/RemoteSubscription/StakingAccountResolving/StakingAccountResolverV13.swift index d9ebb466b2..1cb8f132db 100644 --- a/fearless/Common/Services/RemoteSubscription/StakingAccountResolving/StakingAccountResolverV13.swift +++ b/fearless/Common/Services/RemoteSubscription/StakingAccountResolving/StakingAccountResolverV13.swift @@ -3,6 +3,7 @@ import RobinHood import IrohaCrypto import SSFUtils import SSFModels +import SSFRuntimeCodingService final class StakingAccountResolverV13: BaseStakingAccountResolver { override func resolveKeysAndSubscribe() { diff --git a/fearless/Common/Services/RemoteSubscription/StakingAccountResolving/StakingAccountResolverV14.swift b/fearless/Common/Services/RemoteSubscription/StakingAccountResolving/StakingAccountResolverV14.swift index fe1d2a957d..308c6341be 100644 --- a/fearless/Common/Services/RemoteSubscription/StakingAccountResolving/StakingAccountResolverV14.swift +++ b/fearless/Common/Services/RemoteSubscription/StakingAccountResolving/StakingAccountResolverV14.swift @@ -3,6 +3,7 @@ import RobinHood import IrohaCrypto import SSFUtils import SSFModels +import SSFRuntimeCodingService final class StakingAccountResolverV14: BaseStakingAccountResolver { override func resolveKeysAndSubscribe() { diff --git a/fearless/Common/Services/RemoteSubscription/StakingAccountSubscription/BaseStakingAccountSubscription.swift b/fearless/Common/Services/RemoteSubscription/StakingAccountSubscription/BaseStakingAccountSubscription.swift index 38121b1523..3c779451b8 100644 --- a/fearless/Common/Services/RemoteSubscription/StakingAccountSubscription/BaseStakingAccountSubscription.swift +++ b/fearless/Common/Services/RemoteSubscription/StakingAccountSubscription/BaseStakingAccountSubscription.swift @@ -3,6 +3,7 @@ import RobinHood import IrohaCrypto import SSFUtils import SSFModels +import SSFRuntimeCodingService class BaseStakingAccountSubscription: StakingAccountSubscription { struct Subscription { diff --git a/fearless/Common/Services/RuntimeRegistryService/RuntimeCoderFactory.swift b/fearless/Common/Services/RuntimeRegistryService/RuntimeCoderFactory.swift index 13c63cfecf..fc7ed49e7b 100644 --- a/fearless/Common/Services/RuntimeRegistryService/RuntimeCoderFactory.swift +++ b/fearless/Common/Services/RuntimeRegistryService/RuntimeCoderFactory.swift @@ -1,38 +1,38 @@ import Foundation import SSFUtils -protocol RuntimeCoderFactoryProtocol { - var specVersion: UInt32 { get } - var txVersion: UInt32 { get } - var metadata: RuntimeMetadata { get } - - func createEncoder() -> DynamicScaleEncoding - func createDecoder(from data: Data) throws -> DynamicScaleDecoding -} - -final class RuntimeCoderFactory: RuntimeCoderFactoryProtocol { - let catalog: TypeRegistryCatalogProtocol - let specVersion: UInt32 - let txVersion: UInt32 - let metadata: RuntimeMetadata - - init( - catalog: TypeRegistryCatalogProtocol, - specVersion: UInt32, - txVersion: UInt32, - metadata: RuntimeMetadata - ) { - self.catalog = catalog - self.specVersion = specVersion - self.txVersion = txVersion - self.metadata = metadata - } - - func createEncoder() -> DynamicScaleEncoding { - DynamicScaleEncoder(registry: catalog, version: UInt64(specVersion)) - } - - func createDecoder(from data: Data) throws -> DynamicScaleDecoding { - try DynamicScaleDecoder(data: data, registry: catalog, version: UInt64(specVersion)) - } -} +// protocol RuntimeCoderFactoryProtocol { +// var specVersion: UInt32 { get } +// var txVersion: UInt32 { get } +// var metadata: RuntimeMetadata { get } +// +// func createEncoder() -> DynamicScaleEncoding +// func createDecoder(from data: Data) throws -> DynamicScaleDecoding +// } +// +// final class RuntimeCoderFactory: RuntimeCoderFactoryProtocol { +// let catalog: TypeRegistryCatalogProtocol +// let specVersion: UInt32 +// let txVersion: UInt32 +// let metadata: RuntimeMetadata +// +// init( +// catalog: TypeRegistryCatalogProtocol, +// specVersion: UInt32, +// txVersion: UInt32, +// metadata: RuntimeMetadata +// ) { +// self.catalog = catalog +// self.specVersion = specVersion +// self.txVersion = txVersion +// self.metadata = metadata +// } +// +// func createEncoder() -> DynamicScaleEncoding { +// DynamicScaleEncoder(registry: catalog, version: UInt64(specVersion)) +// } +// +// func createDecoder(from data: Data) throws -> DynamicScaleDecoding { +// try DynamicScaleDecoder(data: data, registry: catalog, version: UInt64(specVersion)) +// } +// } diff --git a/fearless/Common/Services/RuntimeRegistryService/RuntimeRegistryServiceProtocol.swift b/fearless/Common/Services/RuntimeRegistryService/RuntimeRegistryServiceProtocol.swift index 7344c562f0..a69ba1e36b 100644 --- a/fearless/Common/Services/RuntimeRegistryService/RuntimeRegistryServiceProtocol.swift +++ b/fearless/Common/Services/RuntimeRegistryService/RuntimeRegistryServiceProtocol.swift @@ -2,25 +2,25 @@ import Foundation import RobinHood import SSFUtils -typealias RuntimeMetadataClosure = () throws -> RuntimeMetadata - -protocol RuntimeCodingServiceProtocol { - var snapshot: RuntimeSnapshot? { get } - - func fetchCoderFactoryOperation( - with timeout: TimeInterval, - closure: RuntimeMetadataClosure? - ) -> BaseOperation - - func fetchCoderFactory() async throws -> RuntimeCoderFactoryProtocol -} - -extension RuntimeCodingServiceProtocol { - func fetchCoderFactoryOperation(with timeout: TimeInterval) -> BaseOperation { - fetchCoderFactoryOperation(with: timeout, closure: nil) - } - - func fetchCoderFactoryOperation() -> BaseOperation { - fetchCoderFactoryOperation(with: 20, closure: nil) - } -} +// typealias RuntimeMetadataClosure = () throws -> RuntimeMetadata +// +// protocol RuntimeCodingServiceProtocol { +// var snapshot: RuntimeSnapshot? { get } +// +// func fetchCoderFactoryOperation( +// with timeout: TimeInterval, +// closure: RuntimeMetadataClosure? +// ) -> BaseOperation +// +// func fetchCoderFactory() async throws -> RuntimeCoderFactoryProtocol +// } +// +// extension RuntimeCodingServiceProtocol { +// func fetchCoderFactoryOperation(with timeout: TimeInterval) -> BaseOperation { +// fetchCoderFactoryOperation(with: timeout, closure: nil) +// } +// +// func fetchCoderFactoryOperation() -> BaseOperation { +// fetchCoderFactoryOperation(with: 20, closure: nil) +// } +// } diff --git a/fearless/Common/Services/ServiceCoordinator.swift b/fearless/Common/Services/ServiceCoordinator.swift index 0d20e282c7..0f5125d983 100644 --- a/fearless/Common/Services/ServiceCoordinator.swift +++ b/fearless/Common/Services/ServiceCoordinator.swift @@ -3,6 +3,8 @@ import SoraKeystore import SoraFoundation import RobinHood import SSFUtils +import SSFChainRegistry +import SSFNetwork protocol ServiceCoordinatorProtocol: ApplicationServiceProtocol { func updateOnAccountChange() @@ -15,6 +17,7 @@ final class ServiceCoordinator { private let scamSyncService: ScamSyncServiceProtocol private let polkaswapSettingsService: PolkaswapSettingsSyncServiceProtocol private let walletConnect: WalletConnectService + private let walletAssetsObserver: WalletAssetsObserver init( walletSettings: SelectedWalletSettings, @@ -22,7 +25,8 @@ final class ServiceCoordinator { githubPhishingService: ApplicationServiceProtocol, scamSyncService: ScamSyncServiceProtocol, polkaswapSettingsService: PolkaswapSettingsSyncServiceProtocol, - walletConnect: WalletConnectService + walletConnect: WalletConnectService, + walletAssetsObserver: WalletAssetsObserver ) { self.walletSettings = walletSettings self.accountInfoService = accountInfoService @@ -30,6 +34,7 @@ final class ServiceCoordinator { self.scamSyncService = scamSyncService self.polkaswapSettingsService = polkaswapSettingsService self.walletConnect = walletConnect + self.walletAssetsObserver = walletAssetsObserver } } @@ -37,6 +42,7 @@ extension ServiceCoordinator: ServiceCoordinatorProtocol { func updateOnAccountChange() { if let seletedMetaAccount = walletSettings.value { accountInfoService.update(selectedMetaAccount: seletedMetaAccount) + walletAssetsObserver.update(wallet: seletedMetaAccount) } } @@ -49,12 +55,14 @@ extension ServiceCoordinator: ServiceCoordinatorProtocol { scamSyncService.syncUp() polkaswapSettingsService.syncUp() walletConnect.setup() + walletAssetsObserver.setup() } func throttle() { githubPhishingService.throttle() accountInfoService.throttle() walletConnect.throttle() + walletAssetsObserver.throttle() } } @@ -102,13 +110,60 @@ extension ServiceCoordinator { eventCenter: EventCenter.shared ) + let runtimeMetadataRepository: AsyncCoreDataRepositoryDefault = + SubstrateDataStorageFacade.shared.createAsyncRepository() + + let ethereumRemoteBalanceFetching = EthereumRemoteBalanceFetching( + chainRegistry: chainRegistry, + repositoryWrapper: ethereumBalanceRepositoryWrapper + ) + + let accountInfoRemote = AccountInfoRemoteServiceDefault( + runtimeItemRepository: AsyncAnyRepository(runtimeMetadataRepository), + ethereumRemoteBalanceFetching: ethereumRemoteBalanceFetching, + chainRegistry: createPackageChainRegistry() + ) + + let walletAssetsObserver = WalletAssetsObserverImpl( + wallet: selectedMetaAccount, + chainRegistry: chainRegistry, + accountInfoRemote: accountInfoRemote, + eventCenter: EventCenter.shared + ) + return ServiceCoordinator( walletSettings: walletSettings, accountInfoService: accountInfoService, githubPhishingService: githubPhishingAPIService, scamSyncService: scamSyncService, polkaswapSettingsService: polkaswapSettingsService, - walletConnect: walletConnect + walletConnect: walletConnect, + walletAssetsObserver: walletAssetsObserver + ) + } + + private static func createPackageChainRegistry() -> SSFChainRegistry.ChainRegistryProtocol { + let chainSyncService = SSFChainRegistry.ChainSyncService( + chainsUrl: ApplicationConfig.shared.chainsSourceUrl, + operationQueue: OperationQueue(), + dataFetchFactory: SSFNetwork.NetworkOperationFactory() + ) + + let chainsTypesSyncService = SSFChainRegistry.ChainsTypesSyncService( + url: ApplicationConfig.shared.chainTypesSourceUrl, + dataOperationFactory: SSFNetwork.NetworkOperationFactory(), + operationQueue: OperationQueue() + ) + + let runtimeSyncService = SSFChainRegistry.RuntimeSyncService(dataOperationFactory: NetworkOperationFactory()) + + let chainRegistry = SSFChainRegistry.ChainRegistry( + runtimeProviderPool: SSFChainRegistry.RuntimeProviderPool(), + connectionPool: SSFChainRegistry.ConnectionPool(), + chainSyncService: chainSyncService, + chainsTypesSyncService: chainsTypesSyncService, + runtimeSyncService: runtimeSyncService ) + return chainRegistry } } diff --git a/fearless/Common/Services/StorageQueryService/StorageKeySuffixMapper.swift b/fearless/Common/Services/StorageQueryService/StorageKeySuffixMapper.swift index cc02d4adc0..8f7e4990a9 100644 --- a/fearless/Common/Services/StorageQueryService/StorageKeySuffixMapper.swift +++ b/fearless/Common/Services/StorageQueryService/StorageKeySuffixMapper.swift @@ -1,4 +1,5 @@ import Foundation +import SSFRuntimeCodingService final class StorageKeySuffixMapper: Mapping { typealias InputType = Data diff --git a/fearless/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessing.swift b/fearless/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessing.swift index 8eac4fd042..d4e6319f34 100644 --- a/fearless/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessing.swift +++ b/fearless/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessing.swift @@ -1,6 +1,7 @@ import Foundation import SSFUtils import BigInt +import SSFRuntimeCodingService struct ExtrinsicProcessingResult { let extrinsic: Extrinsic diff --git a/fearless/Common/Services/WebSocketService/StorageSubscription/TransactionSubscription.swift b/fearless/Common/Services/WebSocketService/StorageSubscription/TransactionSubscription.swift index b3325bca28..ef3687ad05 100644 --- a/fearless/Common/Services/WebSocketService/StorageSubscription/TransactionSubscription.swift +++ b/fearless/Common/Services/WebSocketService/StorageSubscription/TransactionSubscription.swift @@ -4,6 +4,7 @@ import IrohaCrypto import RobinHood import BigInt import SSFModels +import SSFRuntimeCodingService struct TransactionSubscriptionResult { let processingResult: ExtrinsicProcessingResult diff --git a/fearless/Common/Storage/StorageFacadeProtocol.swift b/fearless/Common/Storage/StorageFacadeProtocol.swift index 0f10bddabb..16162a42fc 100644 --- a/fearless/Common/Storage/StorageFacadeProtocol.swift +++ b/fearless/Common/Storage/StorageFacadeProtocol.swift @@ -11,6 +11,13 @@ protocol StorageFacadeProtocol: AnyObject { mapper: AnyCoreDataMapper ) -> CoreDataRepository where T: Identifiable, U: NSManagedObject + + func createAsyncRepository( + filter: NSPredicate?, + sortDescriptors: [NSSortDescriptor], + mapper: AnyCoreDataMapper + ) -> AsyncCoreDataRepositoryDefault + where T: Identifiable, U: NSManagedObject } extension StorageFacadeProtocol { @@ -26,6 +33,12 @@ extension StorageFacadeProtocol { return createRepository(filter: nil, sortDescriptors: [], mapper: mapper) } + func createAsyncRepository() + -> AsyncCoreDataRepositoryDefault where T: Identifiable & Codable, U: NSManagedObject & CoreDataCodable { + let mapper = AnyCoreDataMapper(CodableCoreDataMapper()) + return createAsyncRepository(filter: nil, sortDescriptors: [], mapper: mapper) + } + func createRepository( filter: NSPredicate ) -> CoreDataRepository where T: Identifiable & Codable, U: NSManagedObject & CoreDataCodable { diff --git a/fearless/Common/Storage/SubstrateDataStorageFacade.swift b/fearless/Common/Storage/SubstrateDataStorageFacade.swift index cfbc41aa23..2a2e435566 100644 --- a/fearless/Common/Storage/SubstrateDataStorageFacade.swift +++ b/fearless/Common/Storage/SubstrateDataStorageFacade.swift @@ -67,4 +67,17 @@ class SubstrateDataStorageFacade: StorageFacadeProtocol { sortDescriptors: sortDescriptors ) } + + func createAsyncRepository( + filter: NSPredicate?, + sortDescriptors: [NSSortDescriptor], + mapper: AnyCoreDataMapper + ) -> AsyncCoreDataRepositoryDefault where T: Identifiable, U: NSManagedObject { + AsyncCoreDataRepositoryDefault( + databaseService: databaseService, + mapper: mapper, + filter: filter, + sortDescriptors: sortDescriptors + ) + } } diff --git a/fearless/Common/Storage/UserDataStorageFacade.swift b/fearless/Common/Storage/UserDataStorageFacade.swift index 828f8283c0..3bfe761d56 100644 --- a/fearless/Common/Storage/UserDataStorageFacade.swift +++ b/fearless/Common/Storage/UserDataStorageFacade.swift @@ -75,6 +75,19 @@ class UserDataStorageFacade: StorageFacadeProtocol { ) } + func createAsyncRepository( + filter: NSPredicate?, + sortDescriptors: [NSSortDescriptor], + mapper: AnyCoreDataMapper + ) -> AsyncCoreDataRepositoryDefault where T: Identifiable, U: NSManagedObject { + AsyncCoreDataRepositoryDefault( + databaseService: databaseService, + mapper: mapper, + filter: filter, + sortDescriptors: sortDescriptors + ) + } + func createStreamableProvider( filter: NSPredicate?, sortDescriptors: [NSSortDescriptor], diff --git a/fearless/Common/Substrate/Types/AccountInfo.swift b/fearless/Common/Substrate/Types/AccountInfo.swift index 7ad4d77683..76e5214dad 100644 --- a/fearless/Common/Substrate/Types/AccountInfo.swift +++ b/fearless/Common/Substrate/Types/AccountInfo.swift @@ -218,6 +218,13 @@ enum EquilibriumAccountData: Decodable { ) } } + + var info: EquilibriumV0AccountData? { + switch self { + case let .v0data(info): + return info + } + } } struct EquilibriumV0AccountData: Decodable { diff --git a/fearless/Common/Substrate/Types/StorageCodingPath.swift b/fearless/Common/Substrate/Types/StorageCodingPath.swift index c868b27b28..0535d95507 100644 --- a/fearless/Common/Substrate/Types/StorageCodingPath.swift +++ b/fearless/Common/Substrate/Types/StorageCodingPath.swift @@ -1,4 +1,7 @@ import Foundation +import SSFModels + +extension StorageCodingPath: StorageCodingPathProtocol {} enum StorageCodingPath: Equatable, CaseIterable { var moduleName: String { diff --git a/fearless/CoreLayer/CoreComponents/Storage/ResponseDecoders/PrefixStorageResponseValueExtractor.swift b/fearless/CoreLayer/CoreComponents/Storage/ResponseDecoders/PrefixStorageResponseValueExtractor.swift index 44d03b08d2..ab44c3c0a9 100644 --- a/fearless/CoreLayer/CoreComponents/Storage/ResponseDecoders/PrefixStorageResponseValueExtractor.swift +++ b/fearless/CoreLayer/CoreComponents/Storage/ResponseDecoders/PrefixStorageResponseValueExtractor.swift @@ -1,5 +1,6 @@ import Foundation import SSFUtils +import SSFRuntimeCodingService protocol PrefixResponseValueExtractor { func extractValue(request: PrefixRequest, storageResponse: [StorageResponse]) async throws -> [K: T]? diff --git a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/SoraSubsquidHistoryOperationFactory.swift b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/SoraSubsquidHistoryOperationFactory.swift index af96f0871d..a107c02cc5 100644 --- a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/SoraSubsquidHistoryOperationFactory.swift +++ b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/SoraSubsquidHistoryOperationFactory.swift @@ -4,6 +4,7 @@ import CommonWallet import IrohaCrypto import SSFUtils import SSFModels +import SSFRuntimeCodingService class SoraSubsquidHistoryOperationFactory { private let txStorage: AnyDataProviderRepository diff --git a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/SubqueryHistoryOperationFactory.swift b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/SubqueryHistoryOperationFactory.swift index 018e283ef2..6bff1e8038 100644 --- a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/SubqueryHistoryOperationFactory.swift +++ b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/SubqueryHistoryOperationFactory.swift @@ -4,6 +4,7 @@ import CommonWallet import IrohaCrypto import SSFUtils import SSFModels +import SSFRuntimeCodingService class SubqueryHistoryOperationFactory { private let txStorage: AnyDataProviderRepository diff --git a/fearless/CoreLayer/OperationFactory/Polkaswap/PolkaswapOperationFactoryProtocol.swift b/fearless/CoreLayer/OperationFactory/Polkaswap/PolkaswapOperationFactoryProtocol.swift index 0f4cbdad66..f02a2a1b25 100644 --- a/fearless/CoreLayer/OperationFactory/Polkaswap/PolkaswapOperationFactoryProtocol.swift +++ b/fearless/CoreLayer/OperationFactory/Polkaswap/PolkaswapOperationFactoryProtocol.swift @@ -2,6 +2,7 @@ import Foundation import SSFUtils import RobinHood import SSFModels +import SSFRuntimeCodingService protocol PolkaswapOperationFactoryProtocol { func createIsPathAvailableOperation( diff --git a/fearless/CoreLayer/OperationFactory/StakingPoolOperationFactory.swift b/fearless/CoreLayer/OperationFactory/StakingPoolOperationFactory.swift index 3b0bedfd17..d98c1d9e23 100644 --- a/fearless/CoreLayer/OperationFactory/StakingPoolOperationFactory.swift +++ b/fearless/CoreLayer/OperationFactory/StakingPoolOperationFactory.swift @@ -3,6 +3,7 @@ import SSFUtils import RobinHood import BigInt import SSFModels +import SSFRuntimeCodingService protocol StakingPoolOperationFactoryProtocol { func fetchBondedPoolsOperation() -> CompoundOperationWrapper<[StakingPool]> diff --git a/fearless/Modules/BalanceInfo/BalanceInfoDependencyContainer.swift b/fearless/Modules/BalanceInfo/BalanceInfoDependencyContainer.swift index 74aca97bfc..739c9d5e3a 100644 --- a/fearless/Modules/BalanceInfo/BalanceInfoDependencyContainer.swift +++ b/fearless/Modules/BalanceInfo/BalanceInfoDependencyContainer.swift @@ -1,5 +1,6 @@ import SSFUtils import SSFModels +import SSFRuntimeCodingService struct BalanceInfoDependencies { let connection: JSONRPCEngine? diff --git a/fearless/Modules/BalanceInfo/BalanceInfoInteractor.swift b/fearless/Modules/BalanceInfo/BalanceInfoInteractor.swift index cb0348f4f4..b9bf79988f 100644 --- a/fearless/Modules/BalanceInfo/BalanceInfoInteractor.swift +++ b/fearless/Modules/BalanceInfo/BalanceInfoInteractor.swift @@ -2,6 +2,7 @@ import UIKit import RobinHood import SSFUtils import SSFModels +import SSFRuntimeCodingService final class BalanceInfoInteractor { // MARK: - Private properties diff --git a/fearless/Modules/Crowdloan/CrowdloanContribution/CrowdloanContributionInteractor.swift b/fearless/Modules/Crowdloan/CrowdloanContribution/CrowdloanContributionInteractor.swift index 358d1b451b..c14859eeb0 100644 --- a/fearless/Modules/Crowdloan/CrowdloanContribution/CrowdloanContributionInteractor.swift +++ b/fearless/Modules/Crowdloan/CrowdloanContribution/CrowdloanContributionInteractor.swift @@ -2,6 +2,7 @@ import UIKit import RobinHood import BigInt import SSFModels +import SSFRuntimeCodingService class CrowdloanContributionInteractor: CrowdloanContributionInteractorInputProtocol, RuntimeConstantFetching { weak var presenter: CrowdloanContributionInteractorOutputProtocol! diff --git a/fearless/Modules/Crowdloan/CrowdloanContributionConfirm/CrowdloanContributionConfirmInteractor.swift b/fearless/Modules/Crowdloan/CrowdloanContributionConfirm/CrowdloanContributionConfirmInteractor.swift index e0bc8a2fe9..8e660a2ca3 100644 --- a/fearless/Modules/Crowdloan/CrowdloanContributionConfirm/CrowdloanContributionConfirmInteractor.swift +++ b/fearless/Modules/Crowdloan/CrowdloanContributionConfirm/CrowdloanContributionConfirmInteractor.swift @@ -2,6 +2,7 @@ import UIKit import RobinHood import BigInt import SSFModels +import SSFRuntimeCodingService final class CrowdloanContributionConfirmInteractor: CrowdloanContributionInteractor, AccountFetching { var confirmPresenter: CrowdloanContributionConfirmInteractorOutputProtocol? { diff --git a/fearless/Modules/Crowdloan/CrowdloanList/CrowdloanListInteractor.swift b/fearless/Modules/Crowdloan/CrowdloanList/CrowdloanListInteractor.swift index 35ded39f15..64a7f6e12c 100644 --- a/fearless/Modules/Crowdloan/CrowdloanList/CrowdloanListInteractor.swift +++ b/fearless/Modules/Crowdloan/CrowdloanList/CrowdloanListInteractor.swift @@ -1,6 +1,7 @@ import UIKit import RobinHood import SSFModels +import SSFRuntimeCodingService final class CrowdloanListInteractor: RuntimeConstantFetching { weak var presenter: CrowdloanListInteractorOutputProtocol! diff --git a/fearless/Modules/Crowdloan/Operation/CrowdloanOperationFactory.swift b/fearless/Modules/Crowdloan/Operation/CrowdloanOperationFactory.swift index 7b00852239..c61f1e2137 100644 --- a/fearless/Modules/Crowdloan/Operation/CrowdloanOperationFactory.swift +++ b/fearless/Modules/Crowdloan/Operation/CrowdloanOperationFactory.swift @@ -4,6 +4,7 @@ import SSFUtils import IrohaCrypto import BigInt import SSFModels +import SSFRuntimeCodingService protocol CrowdloanOperationFactoryProtocol { func fetchCrowdloansOperation( diff --git a/fearless/Modules/Send/SendDependencyContainer.swift b/fearless/Modules/Send/SendDependencyContainer.swift index a3a37e9723..e9359f129f 100644 --- a/fearless/Modules/Send/SendDependencyContainer.swift +++ b/fearless/Modules/Send/SendDependencyContainer.swift @@ -8,6 +8,8 @@ import SSFExtrinsicKit import Web3 import SSFSigner import SSFCrypto +import Foundation +import SSFRuntimeCodingService struct SendDependencies { let wallet: MetaAccountModel @@ -137,7 +139,7 @@ final class SendDepencyContainer { chainsTypesSyncService: chainsTypesSyncService, runtimeSyncService: runtimeSyncService ) - let connection = try chainRegistry.getConnection(for: chainAsset.chain) + let connection = try await chainRegistry.getSubstrateConnection(for: chainAsset.chain) let runtimeService = try await chainRegistry.getRuntimeProvider( chainId: chainAsset.chain.chainId, @@ -150,16 +152,16 @@ final class SendDepencyContainer { let extrinsicService = SSFExtrinsicKit.ExtrinsicService( accountId: accountResponse.accountId, chainFormat: chainAsset.chain.chainFormat.asSfCrypto(), - cryptoType: SFCryptoType(accountResponse.cryptoType.utilsType), + cryptoType: accountResponse.cryptoType, runtimeRegistry: runtimeService, engine: connection, operationManager: operationManager ) let secretKey = try fetchSecretKey(for: chainAsset.chain, accountResponse: accountResponse) - let signer = TransactionSigner( + let signer = SubstrateTransactionSigner( publicKeyData: accountResponse.publicKey, secretKeyData: secretKey, - cryptoType: SFCryptoType(utilsType: accountResponse.cryptoType.utilsType, isEthereum: chainAsset.chain.isEthereumBased) + cryptoType: accountResponse.cryptoType ) let callFactory = SubstrateCallFactoryDefault(runtimeService: nativeRuntimeService) diff --git a/fearless/Modules/Staking/Analytics/Validators/AnalyticsValidatorsInteractor.swift b/fearless/Modules/Staking/Analytics/Validators/AnalyticsValidatorsInteractor.swift index 4710c2779f..c3043778f1 100644 --- a/fearless/Modules/Staking/Analytics/Validators/AnalyticsValidatorsInteractor.swift +++ b/fearless/Modules/Staking/Analytics/Validators/AnalyticsValidatorsInteractor.swift @@ -3,6 +3,7 @@ import RobinHood import IrohaCrypto import SSFUtils import SSFModels +import SSFRuntimeCodingService final class AnalyticsValidatorsInteractor { weak var presenter: AnalyticsValidatorsInteractorOutputProtocol! diff --git a/fearless/Modules/Staking/ControllerAccount/ControllerAccountInteractor.swift b/fearless/Modules/Staking/ControllerAccount/ControllerAccountInteractor.swift index b97b914209..f0de4b31c3 100644 --- a/fearless/Modules/Staking/ControllerAccount/ControllerAccountInteractor.swift +++ b/fearless/Modules/Staking/ControllerAccount/ControllerAccountInteractor.swift @@ -4,6 +4,7 @@ import RobinHood import IrohaCrypto import SSFUtils import SSFModels +import SSFRuntimeCodingService final class ControllerAccountInteractor { weak var presenter: ControllerAccountInteractorOutputProtocol! diff --git a/fearless/Modules/Staking/ControllerAccountConfirmation/ControllerAccountConfirmationInteractor.swift b/fearless/Modules/Staking/ControllerAccountConfirmation/ControllerAccountConfirmationInteractor.swift index 4f1ea2f0ab..a440017d61 100644 --- a/fearless/Modules/Staking/ControllerAccountConfirmation/ControllerAccountConfirmationInteractor.swift +++ b/fearless/Modules/Staking/ControllerAccountConfirmation/ControllerAccountConfirmationInteractor.swift @@ -3,6 +3,7 @@ import RobinHood import IrohaCrypto import SSFUtils import SSFModels +import SSFRuntimeCodingService final class ControllerAccountConfirmationInteractor { weak var presenter: ControllerAccountConfirmationInteractorOutputProtocol! diff --git a/fearless/Modules/Staking/Model/ChainStakingSettings.swift b/fearless/Modules/Staking/Model/ChainStakingSettings.swift index aab555e8ff..d385f6dc82 100644 --- a/fearless/Modules/Staking/Model/ChainStakingSettings.swift +++ b/fearless/Modules/Staking/Model/ChainStakingSettings.swift @@ -1,6 +1,7 @@ import Foundation import SSFUtils import RobinHood +import SSFRuntimeCodingService enum ChainStakingSettingsType { case `default` diff --git a/fearless/Modules/Staking/Operations/EraCountdownOperationFactory/EraCountdownOperationFactory.swift b/fearless/Modules/Staking/Operations/EraCountdownOperationFactory/EraCountdownOperationFactory.swift index 01fa80390a..6a9eeb1d70 100644 --- a/fearless/Modules/Staking/Operations/EraCountdownOperationFactory/EraCountdownOperationFactory.swift +++ b/fearless/Modules/Staking/Operations/EraCountdownOperationFactory/EraCountdownOperationFactory.swift @@ -1,6 +1,8 @@ import RobinHood import SSFUtils import SoraKeystore +import Foundation +import SSFRuntimeCodingService protocol EraCountdownOperationFactoryProtocol { func fetchCountdownOperationWrapper( diff --git a/fearless/Modules/Staking/Operations/IdentityOperationFactory.swift b/fearless/Modules/Staking/Operations/IdentityOperationFactory.swift index 191919da92..db84b9b119 100644 --- a/fearless/Modules/Staking/Operations/IdentityOperationFactory.swift +++ b/fearless/Modules/Staking/Operations/IdentityOperationFactory.swift @@ -3,6 +3,7 @@ import SSFUtils import RobinHood import IrohaCrypto import SSFModels +import SSFRuntimeCodingService protocol IdentityOperationFactoryProtocol { func createIdentityWrapper( diff --git a/fearless/Modules/Staking/Operations/SlashesOperationFactory.swift b/fearless/Modules/Staking/Operations/SlashesOperationFactory.swift index db319311ec..caa4f89807 100644 --- a/fearless/Modules/Staking/Operations/SlashesOperationFactory.swift +++ b/fearless/Modules/Staking/Operations/SlashesOperationFactory.swift @@ -3,6 +3,7 @@ import RobinHood import IrohaCrypto import SSFUtils import SSFModels +import SSFRuntimeCodingService protocol SlashesOperationFactoryProtocol { func createSlashingSpansOperationForStash( diff --git a/fearless/Modules/Staking/Operations/StakingDurationFetching.swift b/fearless/Modules/Staking/Operations/StakingDurationFetching.swift index 655c2819ae..762b682c95 100644 --- a/fearless/Modules/Staking/Operations/StakingDurationFetching.swift +++ b/fearless/Modules/Staking/Operations/StakingDurationFetching.swift @@ -1,5 +1,6 @@ import Foundation import RobinHood +import SSFRuntimeCodingService protocol StakingDurationFetching { func fetchStakingDuration( diff --git a/fearless/Modules/Staking/Operations/StakingDurationOperationFactory.swift b/fearless/Modules/Staking/Operations/StakingDurationOperationFactory.swift index 2230492aba..76f831f8b5 100644 --- a/fearless/Modules/Staking/Operations/StakingDurationOperationFactory.swift +++ b/fearless/Modules/Staking/Operations/StakingDurationOperationFactory.swift @@ -1,5 +1,6 @@ import Foundation import RobinHood +import SSFRuntimeCodingService struct StakingDuration { let session: TimeInterval diff --git a/fearless/Modules/Staking/Operations/StakingOperationFactory/ParachainStakingInfoOperationFactory.swift b/fearless/Modules/Staking/Operations/StakingOperationFactory/ParachainStakingInfoOperationFactory.swift index bd1c005754..693f26464f 100644 --- a/fearless/Modules/Staking/Operations/StakingOperationFactory/ParachainStakingInfoOperationFactory.swift +++ b/fearless/Modules/Staking/Operations/StakingOperationFactory/ParachainStakingInfoOperationFactory.swift @@ -1,6 +1,7 @@ import Foundation import RobinHood import BigInt +import SSFRuntimeCodingService final class ParachainStakingInfoOperationFactory: NetworkStakingInfoOperationFactory { private func createMapOperation( diff --git a/fearless/Modules/Staking/Operations/StakingOperationFactory/RelaychainStakingInfoOperationFactory.swift b/fearless/Modules/Staking/Operations/StakingOperationFactory/RelaychainStakingInfoOperationFactory.swift index 2ed859c0ad..5c6de939ec 100644 --- a/fearless/Modules/Staking/Operations/StakingOperationFactory/RelaychainStakingInfoOperationFactory.swift +++ b/fearless/Modules/Staking/Operations/StakingOperationFactory/RelaychainStakingInfoOperationFactory.swift @@ -1,6 +1,7 @@ import Foundation import RobinHood import BigInt +import SSFRuntimeCodingService final class RelaychainStakingInfoOperationFactory: NetworkStakingInfoOperationFactory { private func deriveTotalStake(from eraStakersInfo: EraStakersInfo) -> BigUInt { diff --git a/fearless/Modules/Staking/Operations/StakingOperationFactory/StakingOperationFactory.swift b/fearless/Modules/Staking/Operations/StakingOperationFactory/StakingOperationFactory.swift index bcd0be394d..8c52704712 100644 --- a/fearless/Modules/Staking/Operations/StakingOperationFactory/StakingOperationFactory.swift +++ b/fearless/Modules/Staking/Operations/StakingOperationFactory/StakingOperationFactory.swift @@ -1,5 +1,6 @@ import Foundation import RobinHood +import SSFRuntimeCodingService protocol NetworkStakingInfoOperationFactoryProtocol { func networkStakingOperation( diff --git a/fearless/Modules/Staking/Operations/ValidatorOperationFactory/ParachainCollatorOperationFactory.swift b/fearless/Modules/Staking/Operations/ValidatorOperationFactory/ParachainCollatorOperationFactory.swift index b427d5fe38..170df1eab7 100644 --- a/fearless/Modules/Staking/Operations/ValidatorOperationFactory/ParachainCollatorOperationFactory.swift +++ b/fearless/Modules/Staking/Operations/ValidatorOperationFactory/ParachainCollatorOperationFactory.swift @@ -2,8 +2,8 @@ import Foundation import SSFUtils import RobinHood import BigInt -import CommonWallet import SSFModels +import SSFRuntimeCodingService final class ParachainCollatorOperationFactory { private let asset: AssetModel diff --git a/fearless/Modules/Staking/Operations/ValidatorOperationFactory/RelaychainValidatorOperationFactory.swift b/fearless/Modules/Staking/Operations/ValidatorOperationFactory/RelaychainValidatorOperationFactory.swift index c17ac2ba77..0612ef527a 100644 --- a/fearless/Modules/Staking/Operations/ValidatorOperationFactory/RelaychainValidatorOperationFactory.swift +++ b/fearless/Modules/Staking/Operations/ValidatorOperationFactory/RelaychainValidatorOperationFactory.swift @@ -3,6 +3,7 @@ import RobinHood import IrohaCrypto import SSFUtils import SSFModels +import SSFRuntimeCodingService // swiftlint:disable type_body_length final class RelaychainValidatorOperationFactory { diff --git a/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/Flow/Parachain/SelectValidatorsConfirmParachainStrategy.swift b/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/Flow/Parachain/SelectValidatorsConfirmParachainStrategy.swift index 787e9aec53..2949c38d75 100644 --- a/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/Flow/Parachain/SelectValidatorsConfirmParachainStrategy.swift +++ b/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/Flow/Parachain/SelectValidatorsConfirmParachainStrategy.swift @@ -1,6 +1,7 @@ import Foundation import RobinHood import SSFModels +import SSFRuntimeCodingService protocol SelectValidatorsConfirmParachainStrategyOutput: SelectValidatorsConfirmStrategyOutput { func didReceiveAtStake(snapshot: ParachainStakingCollatorSnapshot?) diff --git a/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/Flow/PoolExisting/SelectValidatorsConfirmPoolStrategy.swift b/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/Flow/PoolExisting/SelectValidatorsConfirmPoolStrategy.swift index d786f86fd4..0e8a8c060d 100644 --- a/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/Flow/PoolExisting/SelectValidatorsConfirmPoolStrategy.swift +++ b/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/Flow/PoolExisting/SelectValidatorsConfirmPoolStrategy.swift @@ -2,6 +2,7 @@ import Foundation import RobinHood import BigInt import SSFModels +import SSFRuntimeCodingService protocol SelectValidatorsConfirmPoolExistingStrategyOutput: SelectValidatorsConfirmStrategyOutput { func didSetup() diff --git a/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/Flow/PoolInitiated/SelectValidatorsConfirmPoolInitiatedStrategy.swift b/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/Flow/PoolInitiated/SelectValidatorsConfirmPoolInitiatedStrategy.swift index 795028eccf..724f823362 100644 --- a/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/Flow/PoolInitiated/SelectValidatorsConfirmPoolInitiatedStrategy.swift +++ b/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/Flow/PoolInitiated/SelectValidatorsConfirmPoolInitiatedStrategy.swift @@ -2,6 +2,7 @@ import Foundation import RobinHood import BigInt import SSFModels +import SSFRuntimeCodingService protocol SelectValidatorsConfirmPoolInitiatedStrategyOutput: SelectValidatorsConfirmStrategyOutput { func didReceiveMinBond(result: Result) diff --git a/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/Flow/RelaychainExisting/SelectValidatorsConfirmRelaychainExistingStrategy.swift b/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/Flow/RelaychainExisting/SelectValidatorsConfirmRelaychainExistingStrategy.swift index ff7a85301e..b8e66bfc7e 100644 --- a/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/Flow/RelaychainExisting/SelectValidatorsConfirmRelaychainExistingStrategy.swift +++ b/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/Flow/RelaychainExisting/SelectValidatorsConfirmRelaychainExistingStrategy.swift @@ -2,6 +2,7 @@ import Foundation import RobinHood import BigInt import SSFModels +import SSFRuntimeCodingService protocol SelectValidatorsConfirmRelaychainExistingStrategyOutput: SelectValidatorsConfirmStrategyOutput { func didSetup() diff --git a/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/Flow/RelaychainInitiated/SelectValidatorsConfirmRelaychainInitiatedStrategy.swift b/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/Flow/RelaychainInitiated/SelectValidatorsConfirmRelaychainInitiatedStrategy.swift index 0857af3b3e..b986b66ad9 100644 --- a/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/Flow/RelaychainInitiated/SelectValidatorsConfirmRelaychainInitiatedStrategy.swift +++ b/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/Flow/RelaychainInitiated/SelectValidatorsConfirmRelaychainInitiatedStrategy.swift @@ -2,6 +2,7 @@ import Foundation import RobinHood import BigInt import SSFModels +import SSFRuntimeCodingService protocol SelectValidatorsConfirmRelaychainInitiatedStrategyOutput: SelectValidatorsConfirmStrategyOutput { func didReceiveMinBond(result: Result) diff --git a/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/SelectValidatorsConfirmViewFactory.swift b/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/SelectValidatorsConfirmViewFactory.swift index e5bc2759b2..fd83f58feb 100644 --- a/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/SelectValidatorsConfirmViewFactory.swift +++ b/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/SelectValidatorsConfirmViewFactory.swift @@ -4,6 +4,7 @@ import SoraFoundation import RobinHood import SSFUtils import SSFModels +import SSFRuntimeCodingService // swiftlint:disable type_body_length function_body_length final class SelectValidatorsConfirmViewFactory: SelectValidatorsConfirmViewFactoryProtocol { diff --git a/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/Flow/Parachain/SelectValidatorsStartParachainStrategy.swift b/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/Flow/Parachain/SelectValidatorsStartParachainStrategy.swift index 18b866486f..a4019ac30f 100644 --- a/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/Flow/Parachain/SelectValidatorsStartParachainStrategy.swift +++ b/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/Flow/Parachain/SelectValidatorsStartParachainStrategy.swift @@ -1,6 +1,7 @@ import Foundation import RobinHood import SSFModels +import SSFRuntimeCodingService protocol SelectValidatorsStartParachainStrategyOutput: AnyObject { func didReceiveMaxDelegations(result: Result) diff --git a/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/Flow/Pool/SelectValidatorsStartPoolStrategy.swift b/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/Flow/Pool/SelectValidatorsStartPoolStrategy.swift index 00641768a3..a99efa46b9 100644 --- a/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/Flow/Pool/SelectValidatorsStartPoolStrategy.swift +++ b/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/Flow/Pool/SelectValidatorsStartPoolStrategy.swift @@ -1,5 +1,6 @@ import Foundation import RobinHood +import SSFRuntimeCodingService protocol SelectValidatorsStartPoolStrategyOutput: AnyObject { func didReceiveValidators(result: Result<[ElectedValidatorInfo], Error>) diff --git a/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/Flow/Relaychain/SelectValidatorsStartRelaychainStrategy.swift b/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/Flow/Relaychain/SelectValidatorsStartRelaychainStrategy.swift index 626c05d1e3..42f4ac6394 100644 --- a/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/Flow/Relaychain/SelectValidatorsStartRelaychainStrategy.swift +++ b/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/Flow/Relaychain/SelectValidatorsStartRelaychainStrategy.swift @@ -1,5 +1,6 @@ import Foundation import RobinHood +import SSFRuntimeCodingService protocol SelectValidatorsStartRelaychainStrategyOutput: AnyObject { func didReceiveValidators(result: Result<[ElectedValidatorInfo], Error>) diff --git a/fearless/Modules/Staking/SelectValidatorsFlow/YourValidatorList/Flows/Pool/YourValidatorListPoolStrategy.swift b/fearless/Modules/Staking/SelectValidatorsFlow/YourValidatorList/Flows/Pool/YourValidatorListPoolStrategy.swift index d00e5e0cdb..d793adcfb6 100644 --- a/fearless/Modules/Staking/SelectValidatorsFlow/YourValidatorList/Flows/Pool/YourValidatorListPoolStrategy.swift +++ b/fearless/Modules/Staking/SelectValidatorsFlow/YourValidatorList/Flows/Pool/YourValidatorListPoolStrategy.swift @@ -1,6 +1,7 @@ import Foundation import RobinHood import SSFModels +import SSFRuntimeCodingService protocol YourValidatorListPoolStrategyOutput { func didReceive(stakeInfo: StakingPoolMember?) diff --git a/fearless/Modules/Staking/SelectValidatorsFlow/YourValidatorList/Flows/Relaychain/YourValidatorListRelaychainStrategy.swift b/fearless/Modules/Staking/SelectValidatorsFlow/YourValidatorList/Flows/Relaychain/YourValidatorListRelaychainStrategy.swift index b055f61d15..b6263c49da 100644 --- a/fearless/Modules/Staking/SelectValidatorsFlow/YourValidatorList/Flows/Relaychain/YourValidatorListRelaychainStrategy.swift +++ b/fearless/Modules/Staking/SelectValidatorsFlow/YourValidatorList/Flows/Relaychain/YourValidatorListRelaychainStrategy.swift @@ -1,6 +1,7 @@ import Foundation import RobinHood import SSFModels +import SSFRuntimeCodingService protocol YourValidatorListRelaychainStrategyOutput { func didReceiveValidators(result: Result) diff --git a/fearless/Modules/Staking/Services/EraValidatorsService/EraValidatorService+Fetch.swift b/fearless/Modules/Staking/Services/EraValidatorsService/EraValidatorService+Fetch.swift index 664c72d54b..af14df9898 100644 --- a/fearless/Modules/Staking/Services/EraValidatorsService/EraValidatorService+Fetch.swift +++ b/fearless/Modules/Staking/Services/EraValidatorsService/EraValidatorService+Fetch.swift @@ -2,6 +2,7 @@ import Foundation import RobinHood import SSFUtils import SSFModels +import SSFRuntimeCodingService private typealias IdentifiableExposure = (Data, ValidatorExposure) diff --git a/fearless/Modules/Staking/Services/RewardCalculatorService/PortionRewardCalculatorService.swift b/fearless/Modules/Staking/Services/RewardCalculatorService/PortionRewardCalculatorService.swift index 10b1da8a1b..bda450b2a1 100644 --- a/fearless/Modules/Staking/Services/RewardCalculatorService/PortionRewardCalculatorService.swift +++ b/fearless/Modules/Staking/Services/RewardCalculatorService/PortionRewardCalculatorService.swift @@ -3,6 +3,7 @@ import RobinHood import SSFUtils import BigInt import SSFModels +import SSFRuntimeCodingService enum PortionCalculatorServiceError: Error { case timedOut diff --git a/fearless/Modules/Staking/StakingAmount/Flow/Parachain/StakingAmountParachainStrategy.swift b/fearless/Modules/Staking/StakingAmount/Flow/Parachain/StakingAmountParachainStrategy.swift index dc29b34626..7a6cc21221 100644 --- a/fearless/Modules/Staking/StakingAmount/Flow/Parachain/StakingAmountParachainStrategy.swift +++ b/fearless/Modules/Staking/StakingAmount/Flow/Parachain/StakingAmountParachainStrategy.swift @@ -2,6 +2,7 @@ import Foundation import RobinHood import BigInt import SSFModels +import SSFRuntimeCodingService protocol StakingAmountParachainStrategyOutput: AnyObject { func didSetup() diff --git a/fearless/Modules/Staking/StakingAmount/Flow/Relaychain/StakingAmountRelaychainStrategy.swift b/fearless/Modules/Staking/StakingAmount/Flow/Relaychain/StakingAmountRelaychainStrategy.swift index 1cf5471bba..63aa972021 100644 --- a/fearless/Modules/Staking/StakingAmount/Flow/Relaychain/StakingAmountRelaychainStrategy.swift +++ b/fearless/Modules/Staking/StakingAmount/Flow/Relaychain/StakingAmountRelaychainStrategy.swift @@ -2,6 +2,7 @@ import Foundation import BigInt import RobinHood import SSFModels +import SSFRuntimeCodingService protocol StakingAmountRelaychainStrategyOutput: AnyObject { func didReceive(error: Error) diff --git a/fearless/Modules/Staking/StakingAmount/StakingAmountInteractor.swift b/fearless/Modules/Staking/StakingAmount/StakingAmountInteractor.swift index f6445412f6..dca6d318fa 100644 --- a/fearless/Modules/Staking/StakingAmount/StakingAmountInteractor.swift +++ b/fearless/Modules/Staking/StakingAmount/StakingAmountInteractor.swift @@ -5,6 +5,7 @@ import IrohaCrypto import BigInt import SSFUtils import SSFModels +import SSFRuntimeCodingService final class StakingAmountInteractor { weak var presenter: StakingAmountInteractorOutputProtocol? diff --git a/fearless/Modules/Staking/StakingBalance/Flow/Relaychain/StakingBalanceRelaychainStrategy.swift b/fearless/Modules/Staking/StakingBalance/Flow/Relaychain/StakingBalanceRelaychainStrategy.swift index 411f947d3c..8d990054e2 100644 --- a/fearless/Modules/Staking/StakingBalance/Flow/Relaychain/StakingBalanceRelaychainStrategy.swift +++ b/fearless/Modules/Staking/StakingBalance/Flow/Relaychain/StakingBalanceRelaychainStrategy.swift @@ -2,6 +2,7 @@ import Foundation import RobinHood import SSFUtils import SSFModels +import SSFRuntimeCodingService protocol StakingBalanceRelaychainStrategyOutput: AnyObject { func didReceive(ledgerResult: Result) diff --git a/fearless/Modules/Staking/StakingBondMore/Flow/Parachain/StakingBondMoreParachainStrategy.swift b/fearless/Modules/Staking/StakingBondMore/Flow/Parachain/StakingBondMoreParachainStrategy.swift index 1f4315c5ce..b460f3a7e8 100644 --- a/fearless/Modules/Staking/StakingBondMore/Flow/Parachain/StakingBondMoreParachainStrategy.swift +++ b/fearless/Modules/Staking/StakingBondMore/Flow/Parachain/StakingBondMoreParachainStrategy.swift @@ -3,6 +3,7 @@ import RobinHood import SSFUtils import BigInt import SSFModels +import SSFRuntimeCodingService protocol StakingBondMoreParachainStrategyOutput: AnyObject { func didReceiveAccountInfo(result: Result) diff --git a/fearless/Modules/Staking/StakingBondMore/Flow/Pool/StakingBondMorePoolStrategy.swift b/fearless/Modules/Staking/StakingBondMore/Flow/Pool/StakingBondMorePoolStrategy.swift index 6fcd61ed20..576bbde60f 100644 --- a/fearless/Modules/Staking/StakingBondMore/Flow/Pool/StakingBondMorePoolStrategy.swift +++ b/fearless/Modules/Staking/StakingBondMore/Flow/Pool/StakingBondMorePoolStrategy.swift @@ -3,6 +3,7 @@ import RobinHood import SSFUtils import BigInt import SSFModels +import SSFRuntimeCodingService protocol StakingBondMorePoolStrategyOutput: AnyObject { func didReceiveAccountInfo(result: Result) diff --git a/fearless/Modules/Staking/StakingBondMore/Flow/Relaychain/StakingBondMoreRelaychainStrategy.swift b/fearless/Modules/Staking/StakingBondMore/Flow/Relaychain/StakingBondMoreRelaychainStrategy.swift index f65825cf0c..b705c0c793 100644 --- a/fearless/Modules/Staking/StakingBondMore/Flow/Relaychain/StakingBondMoreRelaychainStrategy.swift +++ b/fearless/Modules/Staking/StakingBondMore/Flow/Relaychain/StakingBondMoreRelaychainStrategy.swift @@ -3,6 +3,7 @@ import RobinHood import SSFUtils import BigInt import SSFModels +import SSFRuntimeCodingService protocol StakingBondMoreRelaychainStrategyOutput: AnyObject { func didReceiveStash(result: Result) diff --git a/fearless/Modules/Staking/StakingBondMoreConfirmation/Flow/Parachain/StakingBondMoreConfirmationParachainStrategy.swift b/fearless/Modules/Staking/StakingBondMoreConfirmation/Flow/Parachain/StakingBondMoreConfirmationParachainStrategy.swift index e3fc8fe77a..7b258a3bb8 100644 --- a/fearless/Modules/Staking/StakingBondMoreConfirmation/Flow/Parachain/StakingBondMoreConfirmationParachainStrategy.swift +++ b/fearless/Modules/Staking/StakingBondMoreConfirmation/Flow/Parachain/StakingBondMoreConfirmationParachainStrategy.swift @@ -3,6 +3,7 @@ import RobinHood import SSFUtils import SoraKeystore import SSFModels +import SSFRuntimeCodingService protocol StakingBondMoreConfirmationParachainStrategyOutput: AnyObject { func didReceiveAccountInfo(result: Result) diff --git a/fearless/Modules/Staking/StakingBondMoreConfirmation/Flow/Pool/StakingBondMoreConfirmationPoolStrategy.swift b/fearless/Modules/Staking/StakingBondMoreConfirmation/Flow/Pool/StakingBondMoreConfirmationPoolStrategy.swift index 2176757038..9075a9c0f3 100644 --- a/fearless/Modules/Staking/StakingBondMoreConfirmation/Flow/Pool/StakingBondMoreConfirmationPoolStrategy.swift +++ b/fearless/Modules/Staking/StakingBondMoreConfirmation/Flow/Pool/StakingBondMoreConfirmationPoolStrategy.swift @@ -3,6 +3,7 @@ import RobinHood import SSFUtils import SoraKeystore import SSFModels +import SSFRuntimeCodingService protocol StakingBondMoreConfirmationPoolStrategyOutput: AnyObject { func didReceiveAccountInfo(result: Result) diff --git a/fearless/Modules/Staking/StakingBondMoreConfirmation/Flow/Relaychain/StakingBondMoreConfirmationRelaychainStrategy.swift b/fearless/Modules/Staking/StakingBondMoreConfirmation/Flow/Relaychain/StakingBondMoreConfirmationRelaychainStrategy.swift index 9ed75de98e..71456eb925 100644 --- a/fearless/Modules/Staking/StakingBondMoreConfirmation/Flow/Relaychain/StakingBondMoreConfirmationRelaychainStrategy.swift +++ b/fearless/Modules/Staking/StakingBondMoreConfirmation/Flow/Relaychain/StakingBondMoreConfirmationRelaychainStrategy.swift @@ -3,6 +3,7 @@ import RobinHood import SSFUtils import SoraKeystore import SSFModels +import SSFRuntimeCodingService protocol StakingBondMoreConfirmationRelaychainStrategyOutput: AnyObject { func didReceiveAccountInfo(result: Result) diff --git a/fearless/Modules/Staking/StakingMain/StakingMainInteractor.swift b/fearless/Modules/Staking/StakingMain/StakingMainInteractor.swift index d7ef8440db..af24ccfcb7 100644 --- a/fearless/Modules/Staking/StakingMain/StakingMainInteractor.swift +++ b/fearless/Modules/Staking/StakingMain/StakingMainInteractor.swift @@ -4,6 +4,7 @@ import RobinHood import SSFUtils import SoraFoundation import SSFModels +import SSFRuntimeCodingService final class StakingMainInteractor: RuntimeConstantFetching { weak var presenter: StakingMainInteractorOutputProtocol? diff --git a/fearless/Modules/Staking/StakingPayoutConfirmation/Floww/Pool/StakingPayoutConfirmationPoolStrategy.swift b/fearless/Modules/Staking/StakingPayoutConfirmation/Floww/Pool/StakingPayoutConfirmationPoolStrategy.swift index 2259645993..f62dde9c03 100644 --- a/fearless/Modules/Staking/StakingPayoutConfirmation/Floww/Pool/StakingPayoutConfirmationPoolStrategy.swift +++ b/fearless/Modules/Staking/StakingPayoutConfirmation/Floww/Pool/StakingPayoutConfirmationPoolStrategy.swift @@ -2,6 +2,7 @@ import Foundation import RobinHood import BigInt import SSFModels +import SSFRuntimeCodingService protocol StakingPayoutConfirmationPoolStrategyOutput { func didRecieve(account: ChainAccountResponse, rewardAmount: Decimal) diff --git a/fearless/Modules/Staking/StakingPayoutConfirmation/Floww/Relaychain/StakingPayoutConfirmationrelaychainStrategy.swift b/fearless/Modules/Staking/StakingPayoutConfirmation/Floww/Relaychain/StakingPayoutConfirmationrelaychainStrategy.swift index cb66a4c467..5fad7854d0 100644 --- a/fearless/Modules/Staking/StakingPayoutConfirmation/Floww/Relaychain/StakingPayoutConfirmationrelaychainStrategy.swift +++ b/fearless/Modules/Staking/StakingPayoutConfirmation/Floww/Relaychain/StakingPayoutConfirmationrelaychainStrategy.swift @@ -2,6 +2,7 @@ import Foundation import RobinHood import BigInt import SSFModels +import SSFRuntimeCodingService protocol StakingPayoutConfirmationrelaychainStrategyOutput { func didRecieve(account: ChainAccountResponse, rewardAmount: Decimal) diff --git a/fearless/Modules/Staking/StakingRebondConfirmation/Flow/Parachain/StakingRebondConfirmationParachainStrategy.swift b/fearless/Modules/Staking/StakingRebondConfirmation/Flow/Parachain/StakingRebondConfirmationParachainStrategy.swift index b5575d72e5..c275563128 100644 --- a/fearless/Modules/Staking/StakingRebondConfirmation/Flow/Parachain/StakingRebondConfirmationParachainStrategy.swift +++ b/fearless/Modules/Staking/StakingRebondConfirmation/Flow/Parachain/StakingRebondConfirmationParachainStrategy.swift @@ -3,6 +3,7 @@ import RobinHood import SSFUtils import SoraKeystore import SSFModels +import SSFRuntimeCodingService protocol StakingRebondConfirmationParachainStrategyOutput: AnyObject { func didReceiveFee(result: Result) diff --git a/fearless/Modules/Staking/StakingRebondConfirmation/Flow/Relaychain/StakingRebondConfirmationRelaychainStrategy.swift b/fearless/Modules/Staking/StakingRebondConfirmation/Flow/Relaychain/StakingRebondConfirmationRelaychainStrategy.swift index 5ed1cb2994..1715173ff8 100644 --- a/fearless/Modules/Staking/StakingRebondConfirmation/Flow/Relaychain/StakingRebondConfirmationRelaychainStrategy.swift +++ b/fearless/Modules/Staking/StakingRebondConfirmation/Flow/Relaychain/StakingRebondConfirmationRelaychainStrategy.swift @@ -3,6 +3,7 @@ import RobinHood import SSFUtils import SoraKeystore import SSFModels +import SSFRuntimeCodingService protocol StakingRebondConfirmationRelaychainStrategyOutput: AnyObject { func didReceiveStakingLedger(result: Result) diff --git a/fearless/Modules/Staking/StakingRebondSetup/StakingRebondSetupInteractor.swift b/fearless/Modules/Staking/StakingRebondSetup/StakingRebondSetupInteractor.swift index d38e51153d..8e9e055592 100644 --- a/fearless/Modules/Staking/StakingRebondSetup/StakingRebondSetupInteractor.swift +++ b/fearless/Modules/Staking/StakingRebondSetup/StakingRebondSetupInteractor.swift @@ -3,6 +3,7 @@ import SoraKeystore import SSFUtils import IrohaCrypto import SSFModels +import SSFRuntimeCodingService final class StakingRebondSetupInteractor: RuntimeConstantFetching, AccountFetching { weak var presenter: StakingRebondSetupInteractorOutputProtocol! diff --git a/fearless/Modules/Staking/StakingRedeem/Flow/Parachain/StakingRedeemParachainStrategy.swift b/fearless/Modules/Staking/StakingRedeem/Flow/Parachain/StakingRedeemParachainStrategy.swift index e80ec2849c..0a9e1b877a 100644 --- a/fearless/Modules/Staking/StakingRedeem/Flow/Parachain/StakingRedeemParachainStrategy.swift +++ b/fearless/Modules/Staking/StakingRedeem/Flow/Parachain/StakingRedeemParachainStrategy.swift @@ -4,6 +4,7 @@ import SoraKeystore import SSFUtils import BigInt import SSFModels +import SSFRuntimeCodingService protocol StakingRedeemParachainStrategyOutput: AnyObject { func didReceiveAccountInfo(result: Result) diff --git a/fearless/Modules/Staking/StakingRedeem/Flow/Pool/StakingRedeemPoolStrategy.swift b/fearless/Modules/Staking/StakingRedeem/Flow/Pool/StakingRedeemPoolStrategy.swift index 5f347b8889..c768097c10 100644 --- a/fearless/Modules/Staking/StakingRedeem/Flow/Pool/StakingRedeemPoolStrategy.swift +++ b/fearless/Modules/Staking/StakingRedeem/Flow/Pool/StakingRedeemPoolStrategy.swift @@ -4,6 +4,7 @@ import SoraKeystore import SSFUtils import BigInt import SSFModels +import SSFRuntimeCodingService protocol StakingRedeemPoolStrategyOutput: AnyObject { func didReceiveAccountInfo(result: Result) diff --git a/fearless/Modules/Staking/StakingRedeem/Flow/Relaychain/StakingRedeemRelaychainStrategy.swift b/fearless/Modules/Staking/StakingRedeem/Flow/Relaychain/StakingRedeemRelaychainStrategy.swift index 11cd641f0a..56e80be60c 100644 --- a/fearless/Modules/Staking/StakingRedeem/Flow/Relaychain/StakingRedeemRelaychainStrategy.swift +++ b/fearless/Modules/Staking/StakingRedeem/Flow/Relaychain/StakingRedeemRelaychainStrategy.swift @@ -4,6 +4,7 @@ import SoraKeystore import SSFUtils import BigInt import SSFModels +import SSFRuntimeCodingService protocol StakingRedeemRelaychainStrategyOutput: AnyObject { func didReceiveStakingLedger(result: Result) diff --git a/fearless/Modules/Staking/StakingRedeemConfirmation/Flow/Parachain/StakingRedeemConfirmationParachainStrategy.swift b/fearless/Modules/Staking/StakingRedeemConfirmation/Flow/Parachain/StakingRedeemConfirmationParachainStrategy.swift index 47189c595b..cf03808d18 100644 --- a/fearless/Modules/Staking/StakingRedeemConfirmation/Flow/Parachain/StakingRedeemConfirmationParachainStrategy.swift +++ b/fearless/Modules/Staking/StakingRedeemConfirmation/Flow/Parachain/StakingRedeemConfirmationParachainStrategy.swift @@ -4,6 +4,7 @@ import SoraKeystore import SSFUtils import BigInt import SSFModels +import SSFRuntimeCodingService protocol StakingRedeemConfirmationParachainStrategyOutput: AnyObject { func didReceiveAccountInfo(result: Result) diff --git a/fearless/Modules/Staking/StakingRedeemConfirmation/Flow/Pool/StakingRedeemConfirmation]PoolStrategy.swift b/fearless/Modules/Staking/StakingRedeemConfirmation/Flow/Pool/StakingRedeemConfirmation]PoolStrategy.swift index b394f0a353..cd370cb6e3 100644 --- a/fearless/Modules/Staking/StakingRedeemConfirmation/Flow/Pool/StakingRedeemConfirmation]PoolStrategy.swift +++ b/fearless/Modules/Staking/StakingRedeemConfirmation/Flow/Pool/StakingRedeemConfirmation]PoolStrategy.swift @@ -4,6 +4,7 @@ import SoraKeystore import SSFUtils import BigInt import SSFModels +import SSFRuntimeCodingService protocol StakingRedeemConfirmationPoolStrategyOutput: AnyObject { func didReceiveAccountInfo(result: Result) diff --git a/fearless/Modules/Staking/StakingRedeemConfirmation/Flow/Relaychain/StakingRedeemConfirmationRelaychainStrategy.swift b/fearless/Modules/Staking/StakingRedeemConfirmation/Flow/Relaychain/StakingRedeemConfirmationRelaychainStrategy.swift index d11aa421a5..629ee9dc7c 100644 --- a/fearless/Modules/Staking/StakingRedeemConfirmation/Flow/Relaychain/StakingRedeemConfirmationRelaychainStrategy.swift +++ b/fearless/Modules/Staking/StakingRedeemConfirmation/Flow/Relaychain/StakingRedeemConfirmationRelaychainStrategy.swift @@ -4,6 +4,7 @@ import SoraKeystore import SSFUtils import BigInt import SSFModels +import SSFRuntimeCodingService protocol StakingRedeemConfirmationRelaychainStrategyOutput: AnyObject { func didReceiveStakingLedger(result: Result) diff --git a/fearless/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmInteractor.swift b/fearless/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmInteractor.swift index 73fd78f4f1..991d2bc159 100644 --- a/fearless/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmInteractor.swift +++ b/fearless/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmInteractor.swift @@ -4,6 +4,7 @@ import IrohaCrypto import SoraKeystore import SSFUtils import SSFModels +import SSFRuntimeCodingService final class StakingRewardDestConfirmInteractor: AccountFetching { weak var presenter: StakingRewardDestConfirmInteractorOutputProtocol! diff --git a/fearless/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupInteractor.swift b/fearless/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupInteractor.swift index e520340111..8d4199dce4 100644 --- a/fearless/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupInteractor.swift +++ b/fearless/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupInteractor.swift @@ -3,6 +3,7 @@ import RobinHood import IrohaCrypto import SSFUtils import SSFModels +import SSFRuntimeCodingService final class StakingRewardDestSetupInteractor: AccountFetching { weak var presenter: StakingRewardDestSetupInteractorOutputProtocol! diff --git a/fearless/Modules/Staking/StakingRewardPayouts/StakingRewardPayoutsInteractor.swift b/fearless/Modules/Staking/StakingRewardPayouts/StakingRewardPayoutsInteractor.swift index bc1c060dac..21a04ce856 100644 --- a/fearless/Modules/Staking/StakingRewardPayouts/StakingRewardPayoutsInteractor.swift +++ b/fearless/Modules/Staking/StakingRewardPayouts/StakingRewardPayoutsInteractor.swift @@ -2,6 +2,7 @@ import UIKit import RobinHood import SSFUtils import SSFModels +import SSFRuntimeCodingService final class StakingRewardPayoutsInteractor { weak var presenter: StakingRewardPayoutsInteractorOutputProtocol! diff --git a/fearless/Modules/Staking/StakingUnbondConfirm/Flow/Parachain/StakingUnbondConfirmParachainStrategy.swift b/fearless/Modules/Staking/StakingUnbondConfirm/Flow/Parachain/StakingUnbondConfirmParachainStrategy.swift index 64d99c1eea..ed7098f827 100644 --- a/fearless/Modules/Staking/StakingUnbondConfirm/Flow/Parachain/StakingUnbondConfirmParachainStrategy.swift +++ b/fearless/Modules/Staking/StakingUnbondConfirm/Flow/Parachain/StakingUnbondConfirmParachainStrategy.swift @@ -4,6 +4,7 @@ import SoraKeystore import RobinHood import BigInt import SSFModels +import SSFRuntimeCodingService protocol StakingUnbondConfirmParachainStrategyOutput: AnyObject { func didReceiveAccountInfo(result: Result) diff --git a/fearless/Modules/Staking/StakingUnbondConfirm/Flow/Pool/StakingUnbondConfirmPoolStrategy.swift b/fearless/Modules/Staking/StakingUnbondConfirm/Flow/Pool/StakingUnbondConfirmPoolStrategy.swift index bf0f65c7d1..a71afb4016 100644 --- a/fearless/Modules/Staking/StakingUnbondConfirm/Flow/Pool/StakingUnbondConfirmPoolStrategy.swift +++ b/fearless/Modules/Staking/StakingUnbondConfirm/Flow/Pool/StakingUnbondConfirmPoolStrategy.swift @@ -4,6 +4,7 @@ import SoraKeystore import RobinHood import BigInt import SSFModels +import SSFRuntimeCodingService protocol StakingUnbondConfirmPoolStrategyOutput: AnyObject { func didReceiveAccountInfo(result: Result) diff --git a/fearless/Modules/Staking/StakingUnbondConfirm/Flow/Relaychain/StakingUnbondConfirmRelaychainStrategy.swift b/fearless/Modules/Staking/StakingUnbondConfirm/Flow/Relaychain/StakingUnbondConfirmRelaychainStrategy.swift index 77df1344f8..2979c9e258 100644 --- a/fearless/Modules/Staking/StakingUnbondConfirm/Flow/Relaychain/StakingUnbondConfirmRelaychainStrategy.swift +++ b/fearless/Modules/Staking/StakingUnbondConfirm/Flow/Relaychain/StakingUnbondConfirmRelaychainStrategy.swift @@ -4,6 +4,7 @@ import SoraKeystore import RobinHood import BigInt import SSFModels +import SSFRuntimeCodingService protocol StakingUnbondConfirmRelaychainStrategyOutput: AnyObject { func didReceiveStakingLedger(result: Result) diff --git a/fearless/Modules/Staking/StakingUnbondSetup/Flow/Parachain/StakingUnbondSetupParachainStrategy.swift b/fearless/Modules/Staking/StakingUnbondSetup/Flow/Parachain/StakingUnbondSetupParachainStrategy.swift index 62b986e7b9..09be20de2b 100644 --- a/fearless/Modules/Staking/StakingUnbondSetup/Flow/Parachain/StakingUnbondSetupParachainStrategy.swift +++ b/fearless/Modules/Staking/StakingUnbondSetup/Flow/Parachain/StakingUnbondSetupParachainStrategy.swift @@ -3,6 +3,7 @@ import SSFUtils import RobinHood import BigInt import SSFModels +import SSFRuntimeCodingService protocol StakingUnbondSetupParachainStrategyOutput: AnyObject { func didReceiveAccountInfo(result: Result) diff --git a/fearless/Modules/Staking/StakingUnbondSetup/Flow/Pool/StakingUnbondSetupPoolStrategy.swift b/fearless/Modules/Staking/StakingUnbondSetup/Flow/Pool/StakingUnbondSetupPoolStrategy.swift index ae2ac1eaff..09ed3c434a 100644 --- a/fearless/Modules/Staking/StakingUnbondSetup/Flow/Pool/StakingUnbondSetupPoolStrategy.swift +++ b/fearless/Modules/Staking/StakingUnbondSetup/Flow/Pool/StakingUnbondSetupPoolStrategy.swift @@ -3,6 +3,7 @@ import SSFUtils import RobinHood import BigInt import SSFModels +import SSFRuntimeCodingService protocol StakingUnbondSetupPoolStrategyOutput: AnyObject { func didReceiveAccountInfo(result: Result) diff --git a/fearless/Modules/Staking/StakingUnbondSetup/Flow/Relaychain/StakingUnbondSetupRelaychainStrategy.swift b/fearless/Modules/Staking/StakingUnbondSetup/Flow/Relaychain/StakingUnbondSetupRelaychainStrategy.swift index 96a8182ba9..6be71aa4e0 100644 --- a/fearless/Modules/Staking/StakingUnbondSetup/Flow/Relaychain/StakingUnbondSetupRelaychainStrategy.swift +++ b/fearless/Modules/Staking/StakingUnbondSetup/Flow/Relaychain/StakingUnbondSetupRelaychainStrategy.swift @@ -3,6 +3,7 @@ import SSFUtils import RobinHood import BigInt import SSFModels +import SSFRuntimeCodingService protocol StakingUnbondSetupRelaychainStrategyOutput: AnyObject { func didReceiveStakingLedger(result: Result) diff --git a/fearless/Modules/StakingPool/Join/StakingPoolJoinConfirm/StakingPoolJoinConfirmInteractor.swift b/fearless/Modules/StakingPool/Join/StakingPoolJoinConfirm/StakingPoolJoinConfirmInteractor.swift index a3ac3e4006..9408904f84 100644 --- a/fearless/Modules/StakingPool/Join/StakingPoolJoinConfirm/StakingPoolJoinConfirmInteractor.swift +++ b/fearless/Modules/StakingPool/Join/StakingPoolJoinConfirm/StakingPoolJoinConfirmInteractor.swift @@ -1,6 +1,7 @@ import UIKit import RobinHood import SSFModels +import SSFRuntimeCodingService final class StakingPoolJoinConfirmInteractor: RuntimeConstantFetching { // MARK: - Private properties diff --git a/fearless/Modules/StakingPool/Join/StakingPoolStart/StakingPoolStartInteractor.swift b/fearless/Modules/StakingPool/Join/StakingPoolStart/StakingPoolStartInteractor.swift index 5d11d63f67..07614d5e17 100644 --- a/fearless/Modules/StakingPool/Join/StakingPoolStart/StakingPoolStartInteractor.swift +++ b/fearless/Modules/StakingPool/Join/StakingPoolStart/StakingPoolStartInteractor.swift @@ -1,5 +1,6 @@ import UIKit import RobinHood +import SSFRuntimeCodingService final class StakingPoolStartInteractor { // MARK: - Private properties diff --git a/fearless/Modules/StakingPool/StakingPoolInfo/StakingPoolInfoInteractor.swift b/fearless/Modules/StakingPool/StakingPoolInfo/StakingPoolInfoInteractor.swift index 685d44dfc3..00d2d3a083 100644 --- a/fearless/Modules/StakingPool/StakingPoolInfo/StakingPoolInfoInteractor.swift +++ b/fearless/Modules/StakingPool/StakingPoolInfo/StakingPoolInfoInteractor.swift @@ -1,6 +1,7 @@ import UIKit import RobinHood import SSFModels +import SSFRuntimeCodingService final class StakingPoolInfoInteractor: RuntimeConstantFetching { // MARK: - Private properties diff --git a/fearless/Modules/StakingPool/StakingPoolManagement/StakingPoolManagementInteractor.swift b/fearless/Modules/StakingPool/StakingPoolManagement/StakingPoolManagementInteractor.swift index 3d8f0ee1b8..b000abf54f 100644 --- a/fearless/Modules/StakingPool/StakingPoolManagement/StakingPoolManagementInteractor.swift +++ b/fearless/Modules/StakingPool/StakingPoolManagement/StakingPoolManagementInteractor.swift @@ -1,6 +1,7 @@ import UIKit import RobinHood import SSFModels +import SSFRuntimeCodingService final class StakingPoolManagementInteractor: RuntimeConstantFetching { // MARK: - Private properties From b78bfde42ae50a07d2a79cbb54573b5157866bae Mon Sep 17 00:00:00 2001 From: Alex Lebedko Date: Fri, 3 May 2024 14:35:55 +0700 Subject: [PATCH 005/115] liquidity pools list --- Podfile | 54 - Podfile.lock | 242 +--- fearless.xcodeproj/project.pbxproj | 1161 ++++++++++------- .../xcshareddata/swiftpm/Package.resolved | 110 +- .../Alchemy/Model/AlchemySortOrder.swift | 2 +- .../Alchemy/Model/AlchemyTokenCategory.swift | 2 +- .../Model/Request/AlchemyHistoryRequest.swift | 4 +- .../Model/SoraSubqueryPriceResponse.swift | 17 + .../Pricing/SoraSubqueryPriceFetcher.swift | 70 + .../Pricing/SubqueryPriceFetcher.swift | 10 + .../Services/HistoryService.swift | 2 +- .../Services/SearchService.swift | 2 +- .../SoraFiatService/SoraFiatService.swift | 73 ++ .../SubqueryFiatInfoOperation.swift | 57 + .../WalletConnect/WalletConnectSigner.swift | 17 +- .../WalletConnectSocketEngine.swift | 2 +- .../iconChevronRight.imageset/Chevron.pdf | Bin 0 -> 1018 bytes .../iconChevronRight.imageset/Contents.json | 12 + .../Common/Configs/ApplicationConfigs.swift | 4 + .../Common/Crypto/AccountImportWrapper.swift | 1 + fearless/Common/Crypto/DummySigner.swift | 1 + .../Common/Crypto/KeystoreExportWrapper.swift | 3 +- fearless/Common/Crypto/SigningWrapper.swift | 1 + .../Crypto/SigningWrapperProtocol.swift | 3 +- .../JsonDataProviderFactory.swift | 7 +- .../DataProvider/PriceProviderFactory.swift | 3 +- ...chainStakingLocalSubscriptionFactory.swift | 2 +- .../Sources/PriceDataSource.swift | 153 ++- .../Rewards/SubqueryRewardSource.swift | 1 - ...ingAnalyticsLocalSubscriptionFactory.swift | 10 +- .../JsonLocalStorageSubscriber.swift | 6 +- ...akingAnalyticsLocalStorageSubscriber.swift | 6 +- .../Errors/WalletErrorContentProtocol.swift | 15 + .../MultiSignature+CryptoType.swift | 1 + .../Foundation/NSPredicate+Filter.swift | 1 + .../Model/CryptoType+Extrinsic.swift | 1 + .../Extension/Model/CryptoType+Keystore.swift | 1 + .../Extension/Model/CryptoType+Utils.swift | 26 - .../Model/CryptoType+ViewModel.swift | 1 + .../CDAccountInfo+CoreDataCodable.swift | 1 + .../CDAccountItem+CoreDataDecodable.swift | 1 + .../CDSingleValue+CoreDataCodable.swift | 1 + .../Storage/SortDescriptor+Storage.swift | 1 + .../Common/Extension/UIControl+Disable.swift | 13 + .../Extension/UIKit/PolkadotIcon+Image.swift | 2 +- .../Extension/UIKit/SkeletonRow+View.swift | 7 + .../AssetTransactionData+AlchemyHistory.swift | 2 +- ...setTransactionData+ArrowsquidHistory.swift | 2 +- ...ssetTransactionData+EtherscanHistory.swift | 2 +- ...setTransactionData+GiantsquidHistory.swift | 2 +- .../AssetTransactionData+OklinkHistory.swift | 2 +- ...tTransactionData+SoraSubsquidHistory.swift | 2 +- ...AssetTransactionData+SubqueryHistory.swift | 2 +- ...AssetTransactionData+SubsquidHistory.swift | 2 +- .../Wallet/AssetTransactionData+Zeta.swift | 2 +- .../Wallet/ErrorContent+Wallet.swift | 1 - .../Extension/Wallet/Logger+Wallet.swift | 1 - .../Wallet/SearchData+Contacts.swift | 2 +- .../TransactionHistoryItem+Status.swift | 1 - .../Helpers/AccountProviderFactory.swift | 1 + .../Helpers/AmountFormatterFactory.swift | 2 +- .../Common/Helpers/ChainAccountFetching.swift | 18 - .../ChainSettingsRepositoryFactory.swift | 1 + .../NumberFormatterFactoryProtocol.swift | 67 + .../Helpers/SubstrateRepositoryFactory.swift | 10 +- .../TransactionHistoryMergeManager.swift | 2 +- .../Common/Helpers/WalletLoggerProtocol.swift | 9 + fearless/Common/Localization/L10n.swift | 309 +++++ .../Common/Localization/WalletLanguage.swift | 35 + fearless/Common/Model/AccountItem.swift | 46 - fearless/Common/Model/AmountDecimal.swift | 42 + .../Common/Model/AssetTransactionData.swift | 86 ++ .../Common/Model/AssetTransactionFee.swift | 15 + fearless/Common/Model/EmptyAccountIcon.swift | 2 +- .../Common/Model/ManagedAccountItem.swift | 1 + fearless/Common/Model/Pagination.swift | 13 + .../Model/ParachainStakingCandidate.swift | 1 - .../Model/ParachainStakingDelegation.swift | 2 +- fearless/Common/Model/ReceiveInfo.swift | 15 + fearless/Common/Model/SearchData.swift | 20 + fearless/Common/Model/TotalRewardItem.swift | 1 - fearless/Common/Model/WalletAsset.swift | 51 + .../Common/Model/WalletHistoryRequest.swift | 15 + .../Common/Model/WalletTransactionType.swift | 46 + .../Giantsquid/GiantsquidBond.swift | 2 +- .../Giantsquid/GiantsquidExtrinsic.swift | 2 +- .../Giantsquid/GiantsquidReward.swift | 2 +- .../Giantsquid/GiantsquidSlash.swift | 2 +- .../Giantsquid/GiantsquidTransfer.swift | 2 +- .../Data/SubqueryDelegatorHistoryItem.swift | 2 +- .../Subquery/Data/SubqueryHistory.swift | 2 +- .../Data/SubqueryHistoryElement.swift | 2 +- ...Item+WalletRemoteHistoryItemProtocol.swift | 2 +- .../Subsquid/ArrowsquidHistoryResponse.swift | 1 - .../Subsquid/ReefSubsquidHistory.swift | 2 +- .../SoraSubsquidHistoryResponse.swift | 2 +- .../Subsquid/SubsquidHistoryResponse.swift | 2 +- .../Network/JSONRPC/PagedKeysRequest.swift | 2 +- .../Common/Network/JSONRPC/StorageQuery.swift | 2 +- .../Subscan/SubscanHistoryItem+Wallet.swift | 2 +- .../Wallet/AccountOperationFactory.swift | 2 +- .../Wallet/WalletRemoteHistoryProtocols.swift | 2 +- .../MetaAccountOperationFactory.swift | 1 + .../ApplicationSettingsPresentable.swift | 1 - .../Common/Protocols/FeeViewProtocol.swift | 308 +++++ .../Common/QRCoding/BokoloCashQRInfo.swift | 2 +- fearless/Common/QRCoding/CexQRInfo.swift | 2 +- .../Common/QRCoding/QRCreationOperation.swift | 2 +- fearless/Common/QRCoding/QRService.swift | 2 +- fearless/Common/QRCoding/SoraQRInfo.swift | 8 +- .../ChainRegistry/ChainRegistry.swift | 5 - .../ChainRegistry/ChainRegistryError.swift | 1 + .../ChainRegistry/ChainRegistryFacade.swift | 3 +- .../ChainRegistry/ChainRegistryFactory.swift | 4 +- .../ChainRegistry/ChainSyncService.swift | 2 +- .../PhishingCheckExecutor.swift | 66 - .../Common/Services/ServiceCoordinator.swift | 32 +- .../EntityToModel/ChainModelMapper.swift | 10 +- .../EntityToModel/ChainSettingsMapper.swift | 1 + .../ManagedMetaAccountMapper.swift | 1 + .../EntityToModel/MetaAccountMapper.swift | 1 + .../contents | 6 +- .../.xccurrentversion | 5 +- .../Storage/UserDataStorageFacade.swift | 2 +- .../Substrate/Types/EraRewardPoints.swift | 1 + fearless/Common/View/AccessoryView.swift | 77 ++ .../View/NetworkFeeFooterView+Protocol.swift | 2 +- .../Common/View/NetworkFeeView+Protocol.swift | 2 +- fearless/Common/View/ShimmeredLabel.swift | 17 +- fearless/Common/View/ShimmeredProtocol.swift | 4 + fearless/Common/View/SkeletonLabel.swift | 39 + .../Common/View/SkeletonLoadableView.swift | 52 + fearless/Common/View/TokenPairIconsView.swift | 4 +- .../ModalPicker/ModalInfoFactory.swift | 77 -- .../ModalPicker/ModalPickerFactory.swift | 1 + .../Common/ViewModel/AccessoryViewModel.swift | 32 + .../ViewModel/ExtrinsicConfirmViewModel.swift | 2 +- fearless/Common/ViewModel/FeeViewModel.swift | 22 + .../ViewModel/TokenPairsIconViewModel.swift | 4 +- .../WalletImageViewModelProtocol.swift | 7 + .../CoreComponents/QR/QRParser.swift | 6 +- .../AsyncStorageRequestFactoryDefault.swift | 4 +- .../JSONRPCWorkerDefault.swift | 2 +- .../MapKeyEncodingWorker.swift | 2 +- .../NMapKeyEncodingWorker.swift | 2 +- .../Storage/StorageRequestPerformer.swift | 2 +- .../Main/AlchemyHistoryOperationFactory.swift | 2 +- .../ArrowsquidHistoryOperationFactory.swift | 2 +- .../EtherscanHistoryOperationFactory.swift | 2 +- .../GiantsquidHistoryOperationFactory.swift | 2 +- .../Main/HistoryOperationFactory.swift | 2 +- .../Main/OklinkHistoryOperationFactory.swift | 2 +- .../ReefSubsquidHistoryOperationFactory.swift | 2 +- .../SoraSubsquidHistoryOperationFactory.swift | 2 +- .../SubqueryHistoryOperationFactory.swift | 2 +- .../SubsquidHistoryOperationFactory.swift | 2 +- .../Main/ZetaHistoryOperationFactory.swift | 2 +- .../BlockExplorerNFTOperationFactory.swift | 2 +- .../SoraHistoryOperationFactory.swift | 2 +- .../BaseDataProviderFactory.swift | 12 +- .../HistoryDataProviderFactory.swift | 13 +- fearless/Modules/About/AboutProtocols.swift | 2 +- .../Modules/About/AboutViewController.swift | 1 - .../AccountCreatePresenter.swift | 3 +- .../AccountCreateProtocols.swift | 1 + .../AccountCreateWireframe.swift | 1 + .../Model/AccountCreateChainType.swift | 2 + .../Model/AccountCreationRequest.swift | 1 + .../Model/MetaAccountCreationMetadata.swift | 1 + .../AccountImportProtocols.swift | 1 + .../AccountImportWireframe.swift | 1 + .../BaseAccountImportInteractor.swift | 1 + .../Model/AccountImportJsonFactory.swift | 3 +- .../Model/MetaAccountImportMetadata.swift | 1 + .../MetaAccountImportPreferredInfo.swift | 1 + .../AccountManagementViewFactory.swift | 1 + .../AddAccount+AccountCreateWireframe.swift | 1 + .../AddAccount+AccountImportWireframe.swift | 1 + .../BackupCreatePasswordInteractor.swift | 22 +- .../BackupPasswordInteractor.swift | 6 +- .../BackupPasswordPresenter.swift | 1 + .../BackupSelectWalletInteractor.swift | 4 +- .../BackupWallet/BackupWalletInteractor.swift | 4 +- .../BackupWallet/BackupWalletPresenter.swift | 2 +- .../BackupWalletImportedInteractor.swift | 2 +- .../Modules/Contacts/ContactsInteractor.swift | 2 +- .../Modules/Contacts/ContactsPresenter.swift | 2 +- .../CrossChainDepsContainer.swift | 5 +- .../CrowdloanContributionInteractor.swift | 2 +- ...rowdloanContributionViewModelFactory.swift | 2 +- .../CrowdloanContributionSetupProtocols.swift | 2 +- ...dloanContributionSetupViewController.swift | 2 +- .../CrowdloanListInteractor.swift | 2 +- .../ViewModel/CrowdloanViewModel.swift | 1 - .../CrowdloansViewModelFactory.swift | 2 +- .../Astar/AstarBonusService.swift | 2 +- .../CrowdloanDataValidatorFactory.swift | 2 +- .../GetPreinstalledWalletPresenter.swift | 2 +- .../Common/LiquidityPools+ViewModel.swift | 35 + .../LiquidityPoolDetailsAssembly.swift | 24 + .../LiquidityPoolDetailsInteractor.swift | 13 + .../LiquidityPoolDetailsPresenter.swift | 40 + .../LiquidityPoolDetailsProtocols.swift | 19 + .../LiquidityPoolDetailsRouter.swift | 3 + .../LiquidityPoolDetailsViewController.swift | 44 + .../LiquidityPoolDetailsViewLayout.swift | 13 + ...vailableLiquidityPoolsListInteractor.swift | 111 ++ ...AvailableLiquidityPoolsListPresenter.swift | 110 ++ ...leLiquidityPoolsListViewModelFactory.swift | 102 ++ .../UserLiquidityPoolsListInteractor.swift | 92 ++ .../UserLiquidityPoolsListPresenter.swift | 110 ++ ...erLiquidityPoolsListViewModelFactory.swift | 102 ++ .../LiquidityPoolsListAssembly.swift | 75 ++ .../LiquidityPoolsListProtocols.swift | 9 +- .../LiquidityPoolsListRouter.swift | 0 .../LiquidityPoolsListViewController.swift | 39 +- .../LiquidityPoolsListViewLayout.swift | 62 +- .../View/LiquidityPoolListCell.swift | 66 +- .../LiquidityPoolListCellModel.swift | 5 +- .../LiquidityPoolListViewModel.swift | 2 +- .../LiquidityPoolsOverviewAssembly.swift | 48 + .../LiquidityPoolsOverviewInteractor.swift | 15 + .../LiquidityPoolsOverviewPresenter.swift | 45 + .../LiquidityPoolsOverviewProtocols.swift | 19 + .../LiquidityPoolsOverviewRouter.swift | 3 + ...LiquidityPoolsOverviewViewController.swift | 81 ++ .../LiquidityPoolsOverviewViewLayout.swift | 72 + .../LiquidityPoolsListAssembly.swift | 24 - .../LiquidityPoolsListInteractor.swift | 15 - .../LiquidityPoolsListPresenter.swift | 45 - .../MainTabBar/MainTabBarInteractor.swift | 2 +- .../MainTabBar/MainTabBarProtocol.swift | 1 - .../MainTabBar/MainTabBarViewFactory.swift | 2 +- .../MainTabBar/MainTabBarWireframe.swift | 2 +- .../ChainAccount/ChainAccountWireframe.swift | 2 +- .../WalletTransactionDetailsViewModel.swift | 2 +- ...etTransactionDetailsViewModelFactory.swift | 2 +- .../WalletTransactionDetailsInteractor.swift | 1 - .../WalletTransactionDetailsPresenter.swift | 2 +- .../WalletTransactionDetailsProtocols.swift | 2 +- ...lletTransactionDetailsViewController.swift | 2 +- .../WalletTransactionDetailsViewFactory.swift | 2 +- .../WalletTransactionDetailsViewLayout.swift | 2 +- .../WalletTransactionHistoryDataState.swift | 2 +- ...alletTransactionHistoryCellViewModel.swift | 1 - ...etTransactionHistoryViewModelFactory.swift | 2 +- ...ransactionHistoryDependencyContainer.swift | 4 +- .../WalletTransactionHistoryInteractor.swift | 2 +- .../WalletTransactionHistoryPresenter.swift | 2 +- .../WalletTransactionHistoryProtocols.swift | 2 +- ...lletTransactionHistoryViewController.swift | 2 +- .../WalletTransactionHistoryViewFactory.swift | 2 - .../WalletTransactionHistoryWireframe.swift | 2 +- .../OnboardingMainInteractor.swift | 6 +- .../PolkaswapAdjustmentProtocols.swift | 2 +- .../PolkaswapAdjustmentViewController.swift | 2 +- .../Modules/Purchase/PurchaseProtocols.swift | 4 +- .../Purchase/PurchaseViewFactory.swift | 22 - .../Modules/Purchase/PurchaseWireframe.swift | 2 +- .../ReceiveAndRequestAssetPresenter.swift | 3 +- .../Modules/ScanQR/ScanQRInteractor.swift | 1 - fearless/Modules/ScanQR/ScanQRPresenter.swift | 2 +- .../SelectCurrencyInteractor.swift | 2 +- fearless/Modules/Send/SendProtocols.swift | 2 +- .../Modules/Send/SendViewController.swift | 2 +- .../Stake/AnalyticsStakeInteractor.swift | 1 + .../ControllerAccountViewFactory.swift | 1 + ...rollerAccountConfirmationViewFactory.swift | 1 + .../SelectValidatorsConfirmationModel.swift | 1 - .../SelectValidatorsConfirmPresenter.swift | 2 +- .../SelectedValidatorListViewModel.swift | 1 + .../ValidatorListFilterPresenter.swift | 2 +- .../ViewModel/ValidatorSearchViewModel.swift | 1 + .../View/YourValidatorTableCell.swift | 1 + .../YourValidatorListViewFactory.swift | 1 + ...kingAmountRelaychainViewModelFactory.swift | 2 +- ...takingAmountRelaychainViewModelState.swift | 2 +- .../Flow/StakingAmountFlow.swift | 2 +- .../StakingAmountPresenter.swift | 2 +- .../StakingAmountProtocols.swift | 2 +- .../StakingAmountViewController.swift | 1 - .../StakingAmountViewFactory.swift | 1 + .../RewardDestinationViewModelFactory.swift | 2 +- .../ViewModel/StakingAmountViewModel.swift | 1 - .../StakingBalanceViewFactory.swift | 1 + .../StakingBondMorePresenter.swift | 2 +- .../StakingBondMoreProtocols.swift | 2 +- .../StakingBondMoreViewController.swift | 1 - .../StakingBondMoreViewFactory.swift | 2 +- ...MoreConfirmParachainViewModelFactory.swift | 2 +- ...MoreConfirmationPoolViewModelFactory.swift | 2 +- ...oreConfirmRelaychainViewModelFactory.swift | 2 +- ...StakingBondMoreConfirmationProtocols.swift | 2 +- ...akingBondMoreConfirmationViewFactory.swift | 1 + .../StakingMainInteractor+Subscription.swift | 4 +- .../StakingMain/StakingMainPresenter.swift | 2 +- .../StakingMain/StakingMainProtocols.swift | 2 +- .../StakingMainViewController.swift | 2 +- .../View/RewardEstimationView.swift | 2 +- .../StakingEstimationViewModel.swift | 2 +- .../StakingStateViewModelFactory.swift | 2 +- .../StakingPayoutConfirmationInteractor.swift | 2 +- .../StakingPayoutConfirmationPresenter.swift | 2 +- ...StakingPayoutConfirmationViewFactory.swift | 1 + ...StakingRebondConfirmationViewFactory.swift | 1 + .../StakingRebondSetupProtocols.swift | 2 +- .../StakingRebondSetupViewController.swift | 1 - .../StakingRebondSetupViewFactory.swift | 1 + .../StakingRedeemViewFactory.swift | 1 + ...StakingRedeemConfirmationViewFactory.swift | 1 + .../StakingRewardDestConfirmViewFactory.swift | 1 + ...StakingRewardDestSetupViewController.swift | 2 +- .../StakingRewardDestSetupViewFactory.swift | 1 + .../StakingUnbondConfirmViewFactory.swift | 1 + .../StakingUnbondSetupProtocols.swift | 2 +- .../StakingUnbondSetupViewController.swift | 1 - .../StakingUnbondSetupViewFactory.swift | 1 + .../View/StakingRewardCalculatorView.swift | 1 - .../StakingPoolJoinConfigPresenter.swift | 2 +- .../StakingPoolJoinConfigProtocols.swift | 2 +- .../StakingPoolJoinConfigViewController.swift | 1 - .../PoolRolesConfirmAssembly.swift | 1 + .../StakingPoolCreateProtocols.swift | 2 +- .../StakingPoolCreateViewController.swift | 2 +- .../SwapTransactionDetailAssembly.swift | 2 +- .../SwapTransactionDetailPresenter.swift | 2 +- .../SwapTransactionViewModelFactory.swift | 2 +- ...SwitchAccount+AccountCreateWireframe.swift | 1 + ...SwitchAccount+AccountImportWireframe.swift | 1 + .../AccountList/TransferMetadataContext.swift | 38 - .../AccountList/View/AssetStyleFactory.swift | 31 - .../AccountList/View/WalletActionsCell.swift | 59 - .../AccountList/View/WalletActionsCell.xib | 254 ---- .../AccountList/View/WalletAssetCell.swift | 53 - .../AccountList/View/WalletAssetCell.xib | 159 --- .../View/WalletTotalPriceCell.swift | 38 - .../AccountList/View/WalletTotalPriceCell.xib | 113 -- .../ViewModel/WalletActionsViewModel.swift | 52 - .../ViewModel/WalletAssetViewModel.swift | 47 - .../ViewModel/WalletTotalPriceViewModel.swift | 37 - .../WalletAccountListConstants.swift | 12 - .../ExistentialDepositInfoCommand.swift | 2 +- .../Commands/TransferConfirmCommand.swift | 2 +- .../TransferConfirmCommandProxy.swift | 2 +- .../Wallet/Commands/WalletBuyCommand.swift | 1 - .../Wallet/Commands/WalletCopyCommand.swift | 1 - .../Commands/WalletSelectAccountCommand.swift | 1 - .../WalletSelectAccountCommandFactory.swift | 2 +- .../WalletSelectPurchaseProviderCommand.swift | 1 - .../Wallet/Contacts/ContactViewModel.swift | 31 - .../Contacts/ContactsConfigurator.swift | 102 -- .../ContactsListViewModelFactory.swift | 101 -- .../Contacts/ContactsLocalSearchEngine.swift | 60 - .../Contacts/ContactsViewModelFactory.swift | 75 -- .../Contacts/View/ContactTableViewCell.swift | 134 -- .../Contacts/View/ContactsConstants.swift | 6 - .../View/WalletHistoryBackgroundView.swift | 14 - .../History/WalletHistoryFilterEditor.swift | 20 - .../Wallet/History/WalletHistoryStyle.swift | 27 - .../WalletHistoryViewFactoryOverriding.swift | 11 - .../WalletHistoryFilterViewModel.swift | 48 - .../WalletHistoryFilterPresenter.swift | 59 - .../WalletHistoryFilterProtocols.swift | 25 - .../WalletHistoryFilterViewController.swift | 167 --- .../WalletHistoryFilterViewFactory.swift | 30 - .../WalletHistoryFilterViewLayout.swift | 64 - .../WalletHistoryFilterWireframe.swift | 28 - .../InvoiceScan/InvoiceScanConfigurator.swift | 33 - .../InvoiceScanLocalSearchEngine.swift | 32 - .../Wallet/Model/TransferConstants.swift | 5 - .../Style/WalletCommonStyleConfigurator.swift | 63 - .../Style/WalletFormCellStyle+Internal.swift | 27 - .../Wallet/View/DummySelectedAssetView.swift | 24 - .../View/ViewModel/FeePriceViewModel.swift | 11 - .../RichAmountDisplayViewModel.swift | 29 - .../ViewModel/RichAmountInputViewModel.swift | 108 -- .../WalletCompoundDetailsViewModel.swift | 25 - .../View/ViewModel/WalletTokenViewModel.swift | 28 - .../Modules/Wallet/View/WalletAmountView.xib | 163 --- .../Wallet/View/WalletBaseAmountView.swift | 73 -- .../Wallet/View/WalletBaseTokenView.swift | 50 - .../View/WalletCompoundDetailsView.swift | 105 -- .../Wallet/View/WalletCompoundDetailsView.xib | 143 -- .../Wallet/View/WalletDisplayAmountView.swift | 39 - .../Wallet/View/WalletDisplayTokenView.swift | 32 - .../View/WalletFearlessDefinition.swift | 34 - .../WalletFearlessDefinitionFactory.swift | 15 - .../View/WalletFearlessFormDefining.swift | 8 - .../WalletSingleActionAccessoryFactory.swift | 19 - .../WalletSingleActionAccessoryView.swift | 29 - .../View/WalletSingleActionAccessoryView.xib | 82 -- .../Modules/Wallet/View/WalletTokenView.xib | 189 --- .../Wallet/View/WalletTransferTokenView.swift | 38 - .../Wallet/WalletCommandDecorator.swift | 38 - .../Modules/Wallet/WalletContextFactory.swift | 86 -- .../WalletSingleProviderIdFactory.swift | 39 - .../Wallet/WalletStaticImageViewModel.swift | 2 +- .../WalletDetailsInteractor.swift | 1 + fearless/Resources/chains.json | 1083 +++++++++++++-- .../LiquidityPoolDetailsTests.swift | 16 + .../LiquidityPoolsOverviewTests.swift | 16 + 401 files changed, 5058 insertions(+), 5119 deletions(-) create mode 100644 fearless/ApplicationLayer/Pricing/Model/SoraSubqueryPriceResponse.swift create mode 100644 fearless/ApplicationLayer/Pricing/SoraSubqueryPriceFetcher.swift create mode 100644 fearless/ApplicationLayer/Pricing/SubqueryPriceFetcher.swift create mode 100644 fearless/ApplicationLayer/Services/SoraFiatService/SoraFiatService.swift create mode 100644 fearless/ApplicationLayer/Services/SoraFiatService/SubqueryFiatInfoOperation.swift create mode 100644 fearless/Assets.xcassets/iconChevronRight.imageset/Chevron.pdf create mode 100644 fearless/Assets.xcassets/iconChevronRight.imageset/Contents.json create mode 100644 fearless/Common/Errors/WalletErrorContentProtocol.swift delete mode 100644 fearless/Common/Extension/Model/CryptoType+Utils.swift create mode 100644 fearless/Common/Extension/UIControl+Disable.swift create mode 100644 fearless/Common/Helpers/NumberFormatterFactoryProtocol.swift create mode 100644 fearless/Common/Helpers/WalletLoggerProtocol.swift create mode 100644 fearless/Common/Localization/L10n.swift create mode 100644 fearless/Common/Localization/WalletLanguage.swift delete mode 100644 fearless/Common/Model/AccountItem.swift create mode 100644 fearless/Common/Model/AmountDecimal.swift create mode 100644 fearless/Common/Model/AssetTransactionData.swift create mode 100644 fearless/Common/Model/AssetTransactionFee.swift create mode 100644 fearless/Common/Model/Pagination.swift create mode 100644 fearless/Common/Model/ReceiveInfo.swift create mode 100644 fearless/Common/Model/SearchData.swift create mode 100644 fearless/Common/Model/WalletAsset.swift create mode 100644 fearless/Common/Model/WalletHistoryRequest.swift create mode 100644 fearless/Common/Model/WalletTransactionType.swift create mode 100644 fearless/Common/Protocols/FeeViewProtocol.swift delete mode 100644 fearless/Common/Services/GitHubPhishingService/PhishingCheckExecutor.swift create mode 100644 fearless/Common/View/AccessoryView.swift create mode 100644 fearless/Common/View/SkeletonLabel.swift create mode 100644 fearless/Common/View/SkeletonLoadableView.swift create mode 100644 fearless/Common/ViewModel/AccessoryViewModel.swift create mode 100644 fearless/Common/ViewModel/FeeViewModel.swift create mode 100644 fearless/Common/ViewModel/WalletImageViewModelProtocol.swift create mode 100644 fearless/Modules/LiquidityPools/Common/LiquidityPools+ViewModel.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsAssembly.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsInteractor.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsProtocols.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsRouter.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewController.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListInteractor.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListPresenter.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListViewModelFactory.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListInteractor.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListViewModelFactory.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListAssembly.swift rename fearless/Modules/{ => LiquidityPools}/LiquidityPoolsList/LiquidityPoolsListProtocols.swift (77%) rename fearless/Modules/{ => LiquidityPools}/LiquidityPoolsList/LiquidityPoolsListRouter.swift (100%) rename fearless/Modules/{ => LiquidityPools}/LiquidityPoolsList/LiquidityPoolsListViewController.swift (63%) rename fearless/Modules/{ => LiquidityPools}/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift (60%) rename fearless/Modules/{ => LiquidityPools}/LiquidityPoolsList/View/LiquidityPoolListCell.swift (63%) rename fearless/Modules/{ => LiquidityPools}/LiquidityPoolsList/ViewModel/LiquidityPoolListCellModel.swift (68%) rename fearless/Modules/{ => LiquidityPools}/LiquidityPoolsList/ViewModel/LiquidityPoolListViewModel.swift (68%) create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewAssembly.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewInteractor.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewPresenter.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewProtocols.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewRouter.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewController.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewLayout.swift delete mode 100644 fearless/Modules/LiquidityPoolsList/LiquidityPoolsListAssembly.swift delete mode 100644 fearless/Modules/LiquidityPoolsList/LiquidityPoolsListInteractor.swift delete mode 100644 fearless/Modules/LiquidityPoolsList/LiquidityPoolsListPresenter.swift delete mode 100644 fearless/Modules/Wallet/AccountList/TransferMetadataContext.swift delete mode 100644 fearless/Modules/Wallet/AccountList/View/AssetStyleFactory.swift delete mode 100644 fearless/Modules/Wallet/AccountList/View/WalletActionsCell.swift delete mode 100644 fearless/Modules/Wallet/AccountList/View/WalletActionsCell.xib delete mode 100644 fearless/Modules/Wallet/AccountList/View/WalletAssetCell.swift delete mode 100644 fearless/Modules/Wallet/AccountList/View/WalletAssetCell.xib delete mode 100644 fearless/Modules/Wallet/AccountList/View/WalletTotalPriceCell.swift delete mode 100644 fearless/Modules/Wallet/AccountList/View/WalletTotalPriceCell.xib delete mode 100644 fearless/Modules/Wallet/AccountList/ViewModel/WalletActionsViewModel.swift delete mode 100644 fearless/Modules/Wallet/AccountList/ViewModel/WalletAssetViewModel.swift delete mode 100644 fearless/Modules/Wallet/AccountList/ViewModel/WalletTotalPriceViewModel.swift delete mode 100644 fearless/Modules/Wallet/AccountList/WalletAccountListConstants.swift delete mode 100644 fearless/Modules/Wallet/Contacts/ContactViewModel.swift delete mode 100644 fearless/Modules/Wallet/Contacts/ContactsConfigurator.swift delete mode 100644 fearless/Modules/Wallet/Contacts/ContactsListViewModelFactory.swift delete mode 100644 fearless/Modules/Wallet/Contacts/ContactsLocalSearchEngine.swift delete mode 100644 fearless/Modules/Wallet/Contacts/ContactsViewModelFactory.swift delete mode 100644 fearless/Modules/Wallet/Contacts/View/ContactTableViewCell.swift delete mode 100644 fearless/Modules/Wallet/Contacts/View/ContactsConstants.swift delete mode 100644 fearless/Modules/Wallet/History/View/WalletHistoryBackgroundView.swift delete mode 100644 fearless/Modules/Wallet/History/WalletHistoryFilterEditor.swift delete mode 100644 fearless/Modules/Wallet/History/WalletHistoryStyle.swift delete mode 100644 fearless/Modules/Wallet/History/WalletHistoryViewFactoryOverriding.swift delete mode 100644 fearless/Modules/Wallet/HistoryFilter/ViewModel/WalletHistoryFilterViewModel.swift delete mode 100644 fearless/Modules/Wallet/HistoryFilter/WalletHistoryFilterPresenter.swift delete mode 100644 fearless/Modules/Wallet/HistoryFilter/WalletHistoryFilterProtocols.swift delete mode 100644 fearless/Modules/Wallet/HistoryFilter/WalletHistoryFilterViewController.swift delete mode 100644 fearless/Modules/Wallet/HistoryFilter/WalletHistoryFilterViewFactory.swift delete mode 100644 fearless/Modules/Wallet/HistoryFilter/WalletHistoryFilterViewLayout.swift delete mode 100644 fearless/Modules/Wallet/HistoryFilter/WalletHistoryFilterWireframe.swift delete mode 100644 fearless/Modules/Wallet/InvoiceScan/InvoiceScanConfigurator.swift delete mode 100644 fearless/Modules/Wallet/InvoiceScan/InvoiceScanLocalSearchEngine.swift delete mode 100644 fearless/Modules/Wallet/Model/TransferConstants.swift delete mode 100644 fearless/Modules/Wallet/Style/WalletCommonStyleConfigurator.swift delete mode 100644 fearless/Modules/Wallet/Style/WalletFormCellStyle+Internal.swift delete mode 100644 fearless/Modules/Wallet/View/DummySelectedAssetView.swift delete mode 100644 fearless/Modules/Wallet/View/ViewModel/FeePriceViewModel.swift delete mode 100644 fearless/Modules/Wallet/View/ViewModel/RichAmountDisplayViewModel.swift delete mode 100644 fearless/Modules/Wallet/View/ViewModel/RichAmountInputViewModel.swift delete mode 100644 fearless/Modules/Wallet/View/ViewModel/WalletCompoundDetailsViewModel.swift delete mode 100644 fearless/Modules/Wallet/View/ViewModel/WalletTokenViewModel.swift delete mode 100644 fearless/Modules/Wallet/View/WalletAmountView.xib delete mode 100644 fearless/Modules/Wallet/View/WalletBaseAmountView.swift delete mode 100644 fearless/Modules/Wallet/View/WalletBaseTokenView.swift delete mode 100644 fearless/Modules/Wallet/View/WalletCompoundDetailsView.swift delete mode 100644 fearless/Modules/Wallet/View/WalletCompoundDetailsView.xib delete mode 100644 fearless/Modules/Wallet/View/WalletDisplayAmountView.swift delete mode 100644 fearless/Modules/Wallet/View/WalletDisplayTokenView.swift delete mode 100644 fearless/Modules/Wallet/View/WalletFearlessDefinition.swift delete mode 100644 fearless/Modules/Wallet/View/WalletFearlessDefinitionFactory.swift delete mode 100644 fearless/Modules/Wallet/View/WalletFearlessFormDefining.swift delete mode 100644 fearless/Modules/Wallet/View/WalletSingleActionAccessoryFactory.swift delete mode 100644 fearless/Modules/Wallet/View/WalletSingleActionAccessoryView.swift delete mode 100644 fearless/Modules/Wallet/View/WalletSingleActionAccessoryView.xib delete mode 100644 fearless/Modules/Wallet/View/WalletTokenView.xib delete mode 100644 fearless/Modules/Wallet/View/WalletTransferTokenView.swift delete mode 100644 fearless/Modules/Wallet/WalletCommandDecorator.swift delete mode 100644 fearless/Modules/Wallet/WalletContextFactory.swift delete mode 100644 fearless/Modules/Wallet/WalletSingleProviderIdFactory.swift create mode 100644 fearlessTests/Modules/LiquidityPoolDetails/LiquidityPoolDetailsTests.swift create mode 100644 fearlessTests/Modules/LiquidityPoolsOverview/LiquidityPoolsOverviewTests.swift diff --git a/Podfile b/Podfile index 3d5da79bed..efffe1fc2f 100644 --- a/Podfile +++ b/Podfile @@ -10,9 +10,6 @@ abstract_target 'fearlessAll' do pod 'R.swift', '6.1.0', :inhibit_warnings => true pod 'SoraKeystore', :git => 'https://github.com/soramitsu/keystore-iOS.git', :tag => '1.0.1' pod 'SoraUI', '~> 1.10.3' - pod 'IrohaCrypto' - pod 'RobinHood', '2.6.8' - pod 'CommonWallet/Core' pod 'SoraFoundation', '~> 1.0.0' pod 'SwiftyBeaver' pod 'Starscream', :git => 'https://github.com/soramitsu/fearless-starscream.git' , :tag => '4.0.8' @@ -22,57 +19,10 @@ abstract_target 'fearlessAll' do pod 'Sourcery', '~> 1.4' pod 'Kingfisher', '7.10.2' , :inhibit_warnings => true pod 'SVGKit' - pod 'keccak.c' pod 'Charts', '~> 4.1.0' pod 'XNetworking', :podspec => 'https://raw.githubusercontent.com/soramitsu/x-networking/0.0.37/AppCommonNetworking/XNetworking/XNetworking.podspec' pod 'MediaView', :git => 'https://github.com/bnsports/MediaView.git', :branch => 'dev' pod 'FearlessKeys', '0.1.3' - pod 'MPQRCoreSDK', :configurations => ['Release'] - - def pods_with_configurations - if %r{^true$}i.match ENV['F_DEV'] - pod 'SSFXCM', :configurations => ['DEBUG'] - else - pod 'SSFXCM', '0.1.25' - pod 'SSFExtrinsicKit', '0.1.31' - pod 'SSFCrypto' - pod 'SSFSigner' - pod 'SSFModels', '0.1.32' - pod 'SSFEraKit' - pod 'SSFLogger' - pod 'SSFRuntimeCodingService', '0.1.29' - pod 'SSFStorageQueryKit' - pod 'SSFChainConnection', '0.1.24' - pod 'SSFNetwork' - pod 'SSFChainRegistry', '0.1.29' - pod 'SSFUtils', '0.1.31' - pod 'SSFHelpers', '0.1.31' - pod 'SSFCloudStorage' - end - end - - pods_with_configurations - - # Development pods -# pod 'MediaView', :path => '../MediaView-fork' -# pod 'SSFXCM', :path => '../soramitsu-shared-features-ios/SSFXCM' -# pod 'SSFExtrinsicKit', :path => '../soramitsu-shared-features-ios/SSFExtrinsicKit' -# pod 'SSFCrypto', :path => '../soramitsu-shared-features-ios/SSFCrypto' -# pod 'SSFSigner', :path => '../soramitsu-shared-features-ios/SSFSigner' -# pod 'SSFModels', :path => '../soramitsu-shared-features-ios/SSFModels' -# pod 'SSFEraKit', :path => '../soramitsu-shared-features-ios/SSFEraKit' -# pod 'SSFLogger', :path => '../soramitsu-shared-features-ios/SSFLogger' -# pod 'SSFRuntimeCodingService', :path => '../soramitsu-shared-features-ios/SSFRuntimeCodingService' -# pod 'SSFStorageQueryKit', :path => '../soramitsu-shared-features-ios/SSFStorageQueryKit' -# pod 'SSFChainConnection', :path => '../soramitsu-shared-features-ios/SSFChainConnection' -# pod 'SSFNetwork', :path => '../soramitsu-shared-features-ios/SSFNetwork' -# pod 'SSFUtils', :path => '../soramitsu-shared-features-ios/SSFUtils' -# pod 'SSFChainRegistry', :path => '../soramitsu-shared-features-ios/SSFChainRegistry' -# pod 'SSFHelpers', :path => '../soramitsu-shared-features-ios/SSFHelpers' -# pod 'web3swift-bnsports', :path => '../web3swift-bnsports' -# pod 'SSFCloudStorage', :path => '../soramitsu-shared-features-ios/SSFCloudStorage' -# pod 'SSFKeyPair', :path => '../soramitsu-shared-features-ios/SSFKeyPair' -# pod 'RobinHood', :path => '../robinhood-ios' target 'fearlessTests' do inherit! :search_paths @@ -82,11 +32,7 @@ abstract_target 'fearlessAll' do pod 'R.swift', '6.1.0', :inhibit_warnings => true pod 'FireMock', :inhibit_warnings => true pod 'SoraKeystore', :git => 'https://github.com/soramitsu/keystore-iOS.git', :tag => '1.0.1' - pod 'IrohaCrypto' - pod 'RobinHood', '2.6.8' - pod 'CommonWallet/Core' pod 'Sourcery', '~> 1.4' - pod 'keccak.c' end diff --git a/Podfile.lock b/Podfile.lock index fc89d92270..9fa084f207 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,11 +1,4 @@ PODS: - - AppAuth (1.6.2): - - AppAuth/Core (= 1.6.2) - - AppAuth/ExternalUserAgent (= 1.6.2) - - AppAuth/Core (1.6.2) - - AppAuth/ExternalUserAgent (1.6.2): - - AppAuth/Core - - BigInt (5.2.0) - Charts (4.1.0): - Charts/Core (= 4.1.0) - Charts/Core (4.1.0): @@ -13,74 +6,17 @@ PODS: - CocoaLumberjack (3.8.4): - CocoaLumberjack/Core (= 3.8.4) - CocoaLumberjack/Core (3.8.4) - - CommonWallet/Core (1.16.0): - - RobinHood (~> 2.6.1) - - SoraFoundation/DateProcessing (~> 1.0.0) - - SoraFoundation/Localization (~> 1.0.0) - - SoraFoundation/NotificationHandlers (~> 1.0.0) - - SoraFoundation/NumberProcessing (~> 1.0.0) - - SoraUI (~> 1.10.0) - Cuckoo (1.10.4): - Cuckoo/Swift (= 1.10.4) - Cuckoo/Swift (1.10.4) - FearlessKeys (0.1.3) - FireMock (3.1) - - GoogleAPIClientForREST/Core (1.2.1): - - GTMSessionFetcher (>= 1.1.7) - - GoogleAPIClientForREST/Drive (1.2.1): - - GoogleAPIClientForREST/Core - - GTMSessionFetcher (>= 1.1.7) - - GoogleSignIn (7.0.0): - - AppAuth (~> 1.5) - - GTMAppAuth (< 3.0, >= 1.3) - - GTMSessionFetcher/Core (< 4.0, >= 1.1) - - GTMAppAuth (2.0.0): - - AppAuth/Core (~> 1.6) - - GTMSessionFetcher/Core (< 4.0, >= 1.5) - - GTMSessionFetcher (3.3.1): - - GTMSessionFetcher/Full (= 3.3.1) - - GTMSessionFetcher/Core (3.3.1) - - GTMSessionFetcher/Full (3.3.1): - - GTMSessionFetcher/Core - - IrohaCrypto (0.10.0): - - IrohaCrypto/BIP39 (= 0.10.0) - - IrohaCrypto/blake2 (= 0.10.0) - - IrohaCrypto/Common (= 0.10.0) - - IrohaCrypto/ed25519 (= 0.10.0) - - IrohaCrypto/Scrypt (= 0.10.0) - - IrohaCrypto/secp256k1 (= 0.10.0) - - IrohaCrypto/sr25519 (= 0.10.0) - - IrohaCrypto/ss58 (= 0.10.0) - - IrohaCrypto/BIP39 (0.10.0): - - IrohaCrypto/Common - - IrohaCrypto/blake2 (0.10.0) - - IrohaCrypto/Common (0.10.0) - - IrohaCrypto/ed25519 (0.10.0): - - IrohaCrypto/Common - - IrohaCrypto/Scrypt (0.10.0): - - IrohaCrypto/Common - - scrypt.c (= 0.1.1) - - IrohaCrypto/secp256k1 (0.10.0): - - IrohaCrypto/Common - - secp256k1.c (~> 0.1) - - IrohaCrypto/sr25519 (0.10.0): - - IrohaCrypto/BIP39 - - IrohaCrypto/blake2 - - IrohaCrypto/Common - - IrohaCrypto/ss58 (0.10.0): - - IrohaCrypto/blake2 - - IrohaCrypto/Common - - keccak.c (0.1.3) - Kingfisher (7.10.2) - MediaView (0.2.0) - - MPQRCoreSDK (2.0.7) - R.swift (6.1.0): - R.swift.Library (~> 5.3.0) - R.swift.Library (5.3.0) - ReachabilitySwift (5.0.0) - - RobinHood (2.6.8) - - scrypt.c (0.1.1) - - secp256k1.c (0.1.2) - SnapKit (5.0.1) - SoraFoundation (1.0.0): - SoraFoundation/DateProcessing (= 1.0.0) @@ -136,100 +72,6 @@ PODS: - Sourcery (1.9.2): - Sourcery/CLI-Only (= 1.9.2) - Sourcery/CLI-Only (1.9.2) - - SSFChainConnection (0.1.24): - - SSFUtils - - SSFChainRegistry (0.1.29): - - RobinHood - - SSFChainConnection - - SSFLogger - - SSFModels - - SSFNetwork - - SSFRuntimeCodingService - - SSFUtils - - SSFCloudStorage (0.1.23): - - GoogleAPIClientForREST/Drive (~> 1.2.1) - - GoogleSignIn (~> 7.0.0) - - IrohaCrypto/Scrypt - - SSFModels - - SSFUtils - - TweetNacl (~> 1.0.0) - - SSFCrypto (0.1.17): - - IrohaCrypto/ed25519 - - IrohaCrypto/Scrypt - - IrohaCrypto/secp256k1 - - IrohaCrypto/sr25519 - - IrohaCrypto/ss58 - - keccak.c - - SSFModels - - SSFUtils - - xxHash-Swift (~> 1.0.0) - - SSFEraKit (0.1.0): - - BigInt - - RobinHood - - SSFModels - - SSFRuntimeCodingService - - SSFStorageQueryKit - - SSFUtils - - SSFExtrinsicKit (0.1.31): - - IrohaCrypto - - RobinHood - - SSFCrypto - - SSFEraKit - - SSFModels - - SSFRuntimeCodingService - - SSFSigner - - SSFUtils - - SSFHelpers (0.1.31): - - SSFModels - - SSFUtils - - SSFLogger (0.1.0): - - SwiftyBeaver - - SSFModels (0.1.32): - - IrohaCrypto - - SSFNetwork (0.1.17): - - RobinHood - - SSFRuntimeCodingService (0.1.29): - - RobinHood - - SSFModels - - SSFUtils - - SSFSigner (0.1.0): - - IrohaCrypto/ed25519 - - IrohaCrypto/Scrypt - - IrohaCrypto/secp256k1 - - IrohaCrypto/sr25519 - - IrohaCrypto/ss58 - - SSFCrypto - - SSFStorageQueryKit (0.1.1): - - SSFChainConnection - - SSFCrypto - - SSFRuntimeCodingService - - SSFUtils - - SSFUtils (0.1.31): - - BigInt (~> 5.0) - - IrohaCrypto/ed25519 - - IrohaCrypto/Scrypt - - IrohaCrypto/secp256k1 - - IrohaCrypto/sr25519 - - IrohaCrypto/ss58 - - ReachabilitySwift (~> 5.0) - - RobinHood - - SSFModels - - Starscream - - TweetNacl (~> 1.0.0) - - xxHash-Swift (~> 1.0.0) - - SSFXCM (0.1.25): - - IrohaCrypto - - RobinHood - - SSFChainConnection - - SSFChainRegistry - - SSFCrypto - - SSFExtrinsicKit - - SSFModels - - SSFNetwork - - SSFRuntimeCodingService - - SSFSigner - - SSFStorageQueryKit - - SSFUtils - Starscream (4.0.8) - SVGKit (3.0.0): - CocoaLumberjack (~> 3.0) @@ -237,44 +79,22 @@ PODS: - SwiftFormat/CLI (0.47.13) - SwiftLint (0.54.0) - SwiftyBeaver (2.0.0) - - TweetNacl (1.0.2) - XNetworking (0.0.37) - - xxHash-Swift (1.0.13) DEPENDENCIES: - Charts (~> 4.1.0) - - CommonWallet/Core - Cuckoo - FearlessKeys (= 0.1.3) - FireMock - - IrohaCrypto - - keccak.c - Kingfisher (= 7.10.2) - MediaView (from `https://github.com/bnsports/MediaView.git`, branch `dev`) - - MPQRCoreSDK - R.swift (= 6.1.0) - ReachabilitySwift - - RobinHood (= 2.6.8) - SnapKit (~> 5.0.0) - SoraFoundation (~> 1.0.0) - SoraKeystore (from `https://github.com/soramitsu/keystore-iOS.git`, tag `1.0.1`) - SoraUI (~> 1.10.3) - Sourcery (~> 1.4) - - SSFChainConnection (= 0.1.24) - - SSFChainRegistry (= 0.1.29) - - SSFCloudStorage - - SSFCrypto - - SSFEraKit - - SSFExtrinsicKit (= 0.1.31) - - SSFHelpers (= 0.1.31) - - SSFLogger - - SSFModels (= 0.1.32) - - SSFNetwork - - SSFRuntimeCodingService (= 0.1.29) - - SSFSigner - - SSFStorageQueryKit - - SSFUtils (= 0.1.31) - - SSFXCM (= 0.1.25) - Starscream (from `https://github.com/soramitsu/fearless-starscream.git`, tag `4.0.8`) - SVGKit - SwiftFormat/CLI (~> 0.47.13) @@ -284,27 +104,14 @@ DEPENDENCIES: SPEC REPOS: https://github.com/cocoapods/Specs.git: - - AppAuth - - BigInt - Charts - CocoaLumberjack - - CommonWallet - Cuckoo - FireMock - - GoogleAPIClientForREST - - GoogleSignIn - - GTMAppAuth - - GTMSessionFetcher - - IrohaCrypto - - keccak.c - Kingfisher - - MPQRCoreSDK - R.swift - R.swift.Library - ReachabilitySwift - - RobinHood - - scrypt.c - - secp256k1.c - SnapKit - SoraFoundation - SoraUI @@ -314,25 +121,8 @@ SPEC REPOS: - SwiftFormat - SwiftLint - SwiftyBeaver - - TweetNacl - - xxHash-Swift https://github.com/soramitsu/SSFSpecs.git: - FearlessKeys - - SSFChainConnection - - SSFChainRegistry - - SSFCloudStorage - - SSFCrypto - - SSFEraKit - - SSFExtrinsicKit - - SSFHelpers - - SSFLogger - - SSFModels - - SSFNetwork - - SSFRuntimeCodingService - - SSFSigner - - SSFStorageQueryKit - - SSFUtils - - SSFXCM EXTERNAL SOURCES: MediaView: @@ -359,59 +149,29 @@ CHECKOUT OPTIONS: :tag: 4.0.8 SPEC CHECKSUMS: - AppAuth: 3bb1d1cd9340bd09f5ed189fb00b1cc28e1e8570 - BigInt: f668a80089607f521586bbe29513d708491ef2f7 Charts: ce0768268078eee0336f122c3c4ca248e4e204c5 CocoaLumberjack: df59726690390bb8aaaa585938564ba1c8dbbb44 - CommonWallet: 3c67d5fd85593a20f6b55777c2660ff886fc4148 Cuckoo: 20b8aed94022e0e43e90f7c9e4fb0c86f0926a01 FearlessKeys: c86874e021dbc11c035f68a83f35569cd21d2e6c FireMock: 3eed872059c12f94855413347da83b9d6d1a6fac - GoogleAPIClientForREST: a8b95a252014ce2e618df6b75dc72eca2c00b4af - GoogleSignIn: b232380cf495a429b8095d3178a8d5855b42e842 - GTMAppAuth: 99fb010047ba3973b7026e45393f51f27ab965ae - GTMSessionFetcher: 8a1b34ad97ebe6f909fb8b9b77fba99943007556 - IrohaCrypto: eb3772088068d8938198205b62b0a9dc5b8ce005 - keccak.c: 859583afdaccb4e4fcc0f0096064d101580313f4 Kingfisher: 99edc495d3b7607e6425f0d6f6847b2abd6d716d MediaView: 10ff6a5c7950a7c72c5da9e9b89cc85a981e6abc - MPQRCoreSDK: ea95312df244a90b5041cc8bafe089d07cac2bd2 R.swift: ec98ff71c4ab2f6fd01dd077e5afd15e63a4834c R.swift.Library: 0fc583cb55a99e28901299cc451614cad1161962 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 - RobinHood: b775a72cc0ae55cee3dfb4a5668597aca20ba244 - scrypt.c: b42ae06183251329d2b2c620c226fb541a4a3592 - secp256k1.c: db47b726585d80f027423682eb369729e61b3b20 SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb SoraFoundation: 988d90ee3159311b02e42aeba0cf7e85d8bc724c SoraKeystore: e1789fe41412606d8a1116b86bd00d46d4cb9ccb SoraUI: 1ec71151eb962591eeb898bcdd98bded59745f2d Sourcery: 179539341c2261068528cd15a31837b7238fd901 - SSFChainConnection: 3f1452832b534410c5e94f80f6407ed09bbe88fd - SSFChainRegistry: 540c5724802b7df503e1712994736c23e1cbac4f - SSFCloudStorage: c4123454b3084015ea8d88af92f51de79fce7f2c - SSFCrypto: 7ea544e61213538744ef8fd075e5e4487b7cf502 - SSFEraKit: a6f2a8bdefcdfa3ad8ff8f48b242000ea50744cd - SSFExtrinsicKit: ada245d45f9943e6efa7d65049d76fcf542f90b7 - SSFHelpers: e3fd81339805fac6e9b2930fdf01698ffb7e5a06 - SSFLogger: e6c887014ab0e0c0a67bc742d71ecb83a46be143 - SSFModels: 8d6d7daa95b5491a43483c99dae953011bb65b2e - SSFNetwork: 734ad619a28f5b37599c4e1fb599b899536f0d65 - SSFRuntimeCodingService: 61b75ff14f48a96300e313c7f6861efb07840e71 - SSFSigner: 2ff97e574d16a5e0d87afc6f50177c5ad14c38bf - SSFStorageQueryKit: 3063fdf0a84c9b7c1c2a2a08b862162bd0d38694 - SSFUtils: 1b0bbf20c0d3577bd17f56e4f8d7107925f49c76 - SSFXCM: 1ec10bb1d9d86d846be8f2f53b9c7eecd4a265c8 Starscream: b676ee89781677a2d8d36029a78c970710e2d3eb SVGKit: 1ad7513f8c74d9652f94ed64ddecda1a23864dea SwiftAlgorithms: 38dda4731d19027fdeee1125f973111bf3386b53 SwiftFormat: 73573b89257437c550b03d934889725fbf8f75e5 SwiftLint: c1de071d9d08c8aba837545f6254315bc900e211 SwiftyBeaver: 014b0c12065026b731bac80305294f27d63e27f6 - TweetNacl: 3abf4d1d2082b0114e7a67410e300892448951e6 XNetworking: 516d982bd74ff208222381d27e07052a5d897aaf - xxHash-Swift: 30bd6a7507b3b7348a277c49b1cb6346c2905ec7 -PODFILE CHECKSUM: 4949d659e83fabe2ba207aa9a33c8ff14a7b2a43 +PODFILE CHECKSUM: 0770b928833b595ca2513a2a32218039b8f0e068 COCOAPODS: 1.15.2 diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index d94a4de0e4..09b6a5ac1a 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -9,8 +9,8 @@ /* Begin PBXBuildFile section */ 002561414AF1F8F3B4B65538 /* WalletTransactionDetailsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7416F3AFA5F4D1130B1C410 /* WalletTransactionDetailsWireframe.swift */; }; 0077B6854FDCA7ECCD557B09 /* StakingPoolCreateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D651E438F86F37CC07D6D3F /* StakingPoolCreateViewController.swift */; }; + 02EC6059AEE8C92B4EDD09C0 /* LiquidityPoolsOverviewViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC0B2D0A77F4B0F7CC9E7C1D /* LiquidityPoolsOverviewViewLayout.swift */; }; 02FA6FDF7212F1F8D056BC18 /* SwapTransactionDetailAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = C597BAB74DF23914B68FDC39 /* SwapTransactionDetailAssembly.swift */; }; - 04724AE7FCB7364D3AE0248F /* LiquidityPoolsListInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7045FDDE48C27A858389B302 /* LiquidityPoolsListInteractor.swift */; }; 04A17615051F4A1AE0E63BF8 /* PolkaswapSwapConfirmationViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F67BB672040911E4A165446 /* PolkaswapSwapConfirmationViewLayout.swift */; }; 04F9CF04588676D79EFB8570 /* ClaimCrowdloanRewardsAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBAA28C551344F1457D76DD5 /* ClaimCrowdloanRewardsAssembly.swift */; }; 054C4BCDEC29ED5F74A36E8B /* ExportMnemonicPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61EBE466BDCF77E65FDCDF81 /* ExportMnemonicPresenter.swift */; }; @@ -209,7 +209,6 @@ 07FBC9E228BE24E900ED65B4 /* MissingAccountFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07FBC9E128BE24E900ED65B4 /* MissingAccountFetcher.swift */; }; 07FBC9E628BE29C900ED65B4 /* ChainIssuesCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07FBC9E528BE29C900ED65B4 /* ChainIssuesCenter.swift */; }; 07FD95C027F4384900F07064 /* UIFont+dynamicSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07FD95BF27F4384900F07064 /* UIFont+dynamicSize.swift */; }; - 085DBF0F0FE7856B5C52815E /* LiquidityPoolsListAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6627C06C19A177744E32BA35 /* LiquidityPoolsListAssembly.swift */; }; 093C10C88C0A209158EA75D1 /* UsernameSetupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5EF68BE0E29D2305CB7337 /* UsernameSetupTests.swift */; }; 0A4820F04EC9DA9DD515EC3A /* MainNftContainerRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1CA1EF5BF1151E0DFB298C /* MainNftContainerRouter.swift */; }; 0AAFEFA17F249F4BEF051F6B /* ControllerAccountConfirmationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54FB887490A8B33890B4E0E4 /* ControllerAccountConfirmationPresenter.swift */; }; @@ -252,6 +251,7 @@ 1BFC90E1D8646F7429FFD5E6 /* ExportMnemonicProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF3AD755B2B3DCFB3D14DF91 /* ExportMnemonicProtocols.swift */; }; 1CBFE5F285223EA5D5300C49 /* WalletMainContainerProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8312AF0B70350EE27DB5B4A /* WalletMainContainerProtocols.swift */; }; 1DE31955634007BAC3B63158 /* ChainAccountTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AEA7ECB8434DF494D2B25B9 /* ChainAccountTests.swift */; }; + 1E59CE2953F8835954A4E5A7 /* LiquidityPoolsOverviewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BA8DC8007FC0A322C6DF00E /* LiquidityPoolsOverviewPresenter.swift */; }; 1E766A1656C2117F3F64769A /* NetworkManagementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FFEB823BE0057CFB78CC033 /* NetworkManagementTests.swift */; }; 1EF031DB5316E1D180089C7B /* PolkaswapAdjustmentInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F9F3C33EF043B3EA5FFBC45 /* PolkaswapAdjustmentInteractor.swift */; }; 1F6A32CBF7B43390AF412B1A /* NodeSelectionWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80809FE46E7B8EBDE3680706 /* NodeSelectionWireframe.swift */; }; @@ -260,8 +260,10 @@ 20B2942A4241F6713A1C70D9 /* StakingRewardDetailsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2377F8FB07B47637346249F5 /* StakingRewardDetailsViewFactory.swift */; }; 20F28EF4AD17FC56A5A6697B /* NftSendPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C62522E202A1C5EE60D25122 /* NftSendPresenter.swift */; }; 21322B4A297840739C389F17 /* AccountManagementInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3558BD7D1B8CA1409BE74879 /* AccountManagementInteractor.swift */; }; + 225493AF467A54A56F74FFF5 /* LiquidityPoolsOverviewProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = A692D227372B24F922EFA058 /* LiquidityPoolsOverviewProtocols.swift */; }; 23398AEC756086FEEEE91E65 /* WalletsManagmentRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B55A4C7E8BD87A7C8634ADD /* WalletsManagmentRouter.swift */; }; 237AD34CD1C2778834D7B330 /* AnalyticsValidatorsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6F8BBBA9EABA266B288333F /* AnalyticsValidatorsViewFactory.swift */; }; + 23E30A21620831C600CBC1D6 /* LiquidityPoolDetailsRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28E3B4BA1160B87C435C2AAF /* LiquidityPoolDetailsRouter.swift */; }; 2510425D74B7318818B57B0F /* PolkaswapTransaktionSettingsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC6DBA90336C1C8A40F3601 /* PolkaswapTransaktionSettingsProtocols.swift */; }; 2515863D26C9F862AB800C4C /* ContactsViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D39CEB720F336B3A400477E /* ContactsViewLayout.swift */; }; 25381484F16FB930B8A90CE3 /* SelectValidatorsConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FE5F01FC9364788A91EFA5 /* SelectValidatorsConfirmProtocols.swift */; }; @@ -282,7 +284,6 @@ 2A753CE585E2F0DA4BAD7CF4 /* StakingRedeemFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74547DAC8B04C2A1A7FD625 /* StakingRedeemFlow.swift */; }; 2A84E87825D425750006FE9C /* AlertControllerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A84E87725D425750006FE9C /* AlertControllerFactory.swift */; }; 2AB7A7FF25CD0E8000767D87 /* GitHubPhishingAPIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AB7A7FE25CD0E7F00767D87 /* GitHubPhishingAPIService.swift */; }; - 2AD0A16A25D3854700312428 /* TransferConfirmCommandProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AD0A16925D3854700312428 /* TransferConfirmCommandProxy.swift */; }; 2AD0A19025D3D1E100312428 /* GitHubPhishingServiceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AD0A18F25D3D1E100312428 /* GitHubPhishingServiceFactory.swift */; }; 2AD0A19525D3D3EC00312428 /* GitHubOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AD0A19425D3D3EC00312428 /* GitHubOperationFactory.swift */; }; 2B0FC94B4AE9AFE9532F493F /* ReferralCrowdloanViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71285CF636B32ACD8EB5519E /* ReferralCrowdloanViewFactory.swift */; }; @@ -314,6 +315,7 @@ 38BFEDD4B9F31EF2532962BD /* NetworkIssuesNotificationAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB89A1483E4601F427056B3 /* NetworkIssuesNotificationAssembly.swift */; }; 39218CF5AA701518BD3B0103 /* ExportMnemonicInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200C6B2C85846AED8CA9451A /* ExportMnemonicInteractor.swift */; }; 39DA5795FB9DBF626B72B5C6 /* StakingPoolInfoAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC516FE0E7682210D0F07FB2 /* StakingPoolInfoAssembly.swift */; }; + 3A4026E96615A1D53DAF12D8 /* LiquidityPoolDetailsViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4900562AFFD45F29F4C5DEF /* LiquidityPoolDetailsViewLayout.swift */; }; 3A7BF8FD79B7130241222C35 /* WalletTransactionHistoryProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AEC13E6950006302AC51B96 /* WalletTransactionHistoryProtocols.swift */; }; 3B0F51B1D1590FAAE73CD36C /* SwapTransactionDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32FDB8843EB793C85B222FDB /* SwapTransactionDetailViewController.swift */; }; 3B9314DE6AFC01CA7EF0DAAA /* SelectValidatorsConfirmFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 364C90F7AD36FD6F6E690D7D /* SelectValidatorsConfirmFlow.swift */; }; @@ -335,7 +337,6 @@ 41040A200CF5E77C291128DA /* SwapTransactionDetailInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3EB4CF4E3E4B486D16BDE5C /* SwapTransactionDetailInteractor.swift */; }; 41831354E275074C7BBC4C28 /* NodeSelectionViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BDDF1911884C819F63DCA80 /* NodeSelectionViewFactory.swift */; }; 41B29C1C9239BB2DCB7903A7 /* SelectValidatorsStartViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C391292F22D16427C77CD9 /* SelectValidatorsStartViewFactory.swift */; }; - 423DD1059C62CFEF51A0CFCF /* LiquidityPoolsListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23BF570839C35B8FA1F7CAF6 /* LiquidityPoolsListTests.swift */; }; 42B79A8D0D9540C1D97D991C /* AccountConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 599FEDBD7E8B665F1A93BA70 /* AccountConfirmWireframe.swift */; }; 436D8B9651C88360A6D72E90 /* ChainSelectionWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = C829A981119FEA0EAE4E96E9 /* ChainSelectionWireframe.swift */; }; 4448B591D4A193DBC9E2E3BF /* AccountCreateInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A7B39A61DB0D2F0F1B1DBA1 /* AccountCreateInteractor.swift */; }; @@ -351,6 +352,7 @@ 496AC83B6434378501B657E0 /* StakingPoolCreateViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733A49D80BA2BF400AF23A6 /* StakingPoolCreateViewLayout.swift */; }; 4A520B7081BE2D7604B69354 /* AccountImportWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F45A5C6145F863760F4409 /* AccountImportWireframe.swift */; }; 4A63ECA587C601999AAEB974 /* StakingPoolCreateProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EBB77A32A59568B0DACFE5 /* StakingPoolCreateProtocols.swift */; }; + 4A957B3BAC231B70CBC00EC3 /* LiquidityPoolsOverviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8B69E9E18C11EAEC9284B3 /* LiquidityPoolsOverviewViewController.swift */; }; 4C7AC4E214171478AC98A898 /* StakingRewardDestConfirmTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9403C5F9C88A4690C62A204B /* StakingRewardDestConfirmTests.swift */; }; 4D822D169784790EBF173EEE /* WalletMainContainerPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D738FE711DD760B47C0BA65 /* WalletMainContainerPresenter.swift */; }; 4E5CD7B8821FA5298EA1598E /* CrowdloanContributionSetupWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3574BADE9CF77599048C7010 /* CrowdloanContributionSetupWireframe.swift */; }; @@ -379,19 +381,17 @@ 59745D3C9602745E1417D2F6 /* ChainSelectionInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83AB0AD3A7CECD061611F60C /* ChainSelectionInteractor.swift */; }; 5980BDE494C9E473E5959C71 /* NftCollectionProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD845193EDFC3A1D0BC73719 /* NftCollectionProtocols.swift */; }; 59838EECC194BB0E6E0AEAA2 /* PolkaswapAdjustmentPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B5D683E7DE3533CA418BD21 /* PolkaswapAdjustmentPresenter.swift */; }; - 5A403C06012CE3DD5AA0415C /* LiquidityPoolsListRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BB81B27EBBDD4D246767C4A /* LiquidityPoolsListRouter.swift */; }; 5C796EF8ED29F564B5D1126B /* CrowdloanContributionConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F75722D2F921FD1C2D4105D /* CrowdloanContributionConfirmViewController.swift */; }; 5D0665A581B9F8DFDBD0CF7B /* WalletChainAccountDashboardWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885A9E2D3619FEFC5ED0C093 /* WalletChainAccountDashboardWireframe.swift */; }; 5DDD2206DF795CF205610455 /* AccountExportPasswordPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31226053044986BC828AA912 /* AccountExportPasswordPresenter.swift */; }; 5E9402965D385607E04156DC /* NftDetailsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DEDC1A0ED429DD43EC621E /* NftDetailsPresenter.swift */; }; 5F5825D27863628412B672CA /* NftSendConfirmRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 803E71983CD61FFBFE98DA7A /* NftSendConfirmRouter.swift */; }; - 5FE687B860FC10AB08518A6E /* WalletHistoryFilterPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB9150FEC66FC503CF1BD1D0 /* WalletHistoryFilterPresenter.swift */; }; - 60461B20A5DA9E9E3AF0BB84 /* WalletHistoryFilterWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78670B0926E92B75088D2D7B /* WalletHistoryFilterWireframe.swift */; }; 607699C7CEEDA3598613DCA0 /* NetworkInfoViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EDA19BC3280F1838C687EC8 /* NetworkInfoViewFactory.swift */; }; + 60C22E112CA857A2EA5A129E /* LiquidityPoolsOverviewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC76E7D99A98423180BC572F /* LiquidityPoolsOverviewTests.swift */; }; + 61B5A91FBEF633FCC8D965B6 /* LiquidityPoolsOverviewAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC863A9CE29C63B740C6E4D9 /* LiquidityPoolsOverviewAssembly.swift */; }; 61B9688494251703A6373A1B /* StakingAmountWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6216F6F1B91F798F07695FB6 /* StakingAmountWireframe.swift */; }; 61E0DC83C1D60D677274D7CE /* AccountExportPasswordViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11575D8B4F64C2E805372A5 /* AccountExportPasswordViewFactory.swift */; }; 62843B73F8616209F57A66FC /* AssetNetworksProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14611E105279789A149B3755 /* AssetNetworksProtocols.swift */; }; - 640A79BD1335394818E70366 /* WalletHistoryFilterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6884DFC1AA1B995C21C274C /* WalletHistoryFilterViewController.swift */; }; 64B508A1A3D820AA8DBCFAA3 /* AccountExportPasswordWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F47E5832513985B89D06155 /* AccountExportPasswordWireframe.swift */; }; 64B7826F78B8AE649B1EF08F /* CrowdloanContributionSetupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C01DCD4DA014E8FB50B9F11 /* CrowdloanContributionSetupTests.swift */; }; 65909D701527D99837B439D9 /* StakingRewardDetailsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 638A65DAC86BAF9EB4D2F2F8 /* StakingRewardDetailsWireframe.swift */; }; @@ -408,6 +408,7 @@ 6C0289728544A96852CCBF20 /* AddCustomNodeProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06A0B252CCD6CAE8C5EDC16 /* AddCustomNodeProtocols.swift */; }; 6C56AB4AE63AB2DC73DE98E0 /* AccountImportInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CC1FB277A878E9C9B7EAEB /* AccountImportInteractor.swift */; }; 6C846ECBBC99A97D1D15C523 /* ReferralCrowdloanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9272FED2C00908028F223E5C /* ReferralCrowdloanTests.swift */; }; + 6D0E50CEBCB73C23A75A7F46 /* LiquidityPoolDetailsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CF682B92176E0FED5D7B4DB /* LiquidityPoolDetailsTests.swift */; }; 6D315EFF2B664235D297674E /* AccountImportProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = B29514E516CEAAB159851D95 /* AccountImportProtocols.swift */; }; 6D47EAB127FAB7559A9FA107 /* StakingPayoutConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3BACB7E24BC87F9218DBBC4 /* StakingPayoutConfirmationViewController.swift */; }; 6D5851FB5F830D55EFDB8B7D /* StakingUnbondSetupProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23A74BDB54D503FA2BFBEF35 /* StakingUnbondSetupProtocols.swift */; }; @@ -415,7 +416,6 @@ 6D6C6FD2F13603BCE83CFC65 /* ExportMnemonicConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC566D6EACB81469B926611 /* ExportMnemonicConfirmInteractor.swift */; }; 6DA57B4E2F3390624BEA03C1 /* StakingPoolCreateConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA8ECADDA809DE7932B7A17C /* StakingPoolCreateConfirmProtocols.swift */; }; 6ECD0116CD39D8F55D246864 /* SelectValidatorsConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B5626189788682A84D4E9D7 /* SelectValidatorsConfirmPresenter.swift */; }; - 6F0CFDAB9D0C35075BD74A77 /* WalletHistoryFilterProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41DFB2757D029FB5DF3CEBC2 /* WalletHistoryFilterProtocols.swift */; }; 6FA6FA6944AD6519FB8A2AC0 /* WalletMainContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961642321C9AD1885325A3D7 /* WalletMainContainerViewController.swift */; }; 705F5EEDD70D6941D138D3F9 /* ContactsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E4DF941DE0EDEF99A843A9D /* ContactsInteractor.swift */; }; 709ABA5647D7DFF36EBCE73E /* WarningAlertViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B7FA75791904AF541BE380 /* WarningAlertViewFactory.swift */; }; @@ -446,6 +446,8 @@ 7C93FA82996A426E7B8CA06E /* AccountExportPasswordViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27D5AF2F7609ADE855308089 /* AccountExportPasswordViewController.swift */; }; 7CBE9FFAF8394786CA131D4D /* CustomValidatorListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = F28EDDF9277242505FDDECA1 /* CustomValidatorListProtocols.swift */; }; 7D281FEA78E2E5F44990C184 /* AccountImportPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB8605FD90D8C3553A9897B4 /* AccountImportPresenter.swift */; }; + 7D8D644C5E1695288A0E86C0 /* LiquidityPoolDetailsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53DFFFB3B5B48DB51692EFA /* LiquidityPoolDetailsInteractor.swift */; }; + 7DCC939E882247E141B1DE34 /* LiquidityPoolDetailsAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F6F7AE5AFF3F2E7BADA02BB /* LiquidityPoolDetailsAssembly.swift */; }; 7DFB3D265846A31807E1A663 /* StakingPoolCreateConfirmRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0EE58376751B23A9CEAEE1A /* StakingPoolCreateConfirmRouter.swift */; }; 7E1A03082260E0D31AD394CA /* StakingRewardDetailsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF891BE39D442C2D06DDF3BB /* StakingRewardDetailsProtocols.swift */; }; 7E3FB57A93AFAE39CF3030C8 /* ClaimCrowdloanRewardsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CCB1AFB751075497345C3E7 /* ClaimCrowdloanRewardsViewController.swift */; }; @@ -483,13 +485,6 @@ 8406B5AD26FBE69200635B61 /* SelectableIconDetailsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8406B5AC26FBE69200635B61 /* SelectableIconDetailsListViewModel.swift */; }; 8406B5AF26FBE7EF00635B61 /* SelectionIconDetailsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8406B5AE26FBE7EF00635B61 /* SelectionIconDetailsTableViewCell.swift */; }; 840882B02514024800177E20 /* SelectedConnectionChanged.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840882AF2514024800177E20 /* SelectedConnectionChanged.swift */; }; - 8409C264252210A70049B5C8 /* WalletDisplayAmountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8409C263252210A70049B5C8 /* WalletDisplayAmountView.swift */; }; - 8409C2662522110F0049B5C8 /* WalletAmountView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8409C2652522110F0049B5C8 /* WalletAmountView.xib */; }; - 8409C268252218FF0049B5C8 /* WalletFearlessDefinitionFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8409C267252218FF0049B5C8 /* WalletFearlessDefinitionFactory.swift */; }; - 8409C26A2522192B0049B5C8 /* WalletFearlessDefinition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8409C2692522192B0049B5C8 /* WalletFearlessDefinition.swift */; }; - 8409C27025227C1E0049B5C8 /* WalletSingleActionAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8409C26F25227C1E0049B5C8 /* WalletSingleActionAccessoryView.swift */; }; - 8409C27225227CA00049B5C8 /* WalletSingleActionAccessoryView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8409C27125227CA00049B5C8 /* WalletSingleActionAccessoryView.xib */; }; - 8409C27425227DC60049B5C8 /* WalletSingleActionAccessoryFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8409C27325227DC60049B5C8 /* WalletSingleActionAccessoryFactory.swift */; }; 840BF22526C2653100E3A955 /* ChainSyncServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840BF22426C2653100E3A955 /* ChainSyncServiceTests.swift */; }; 840BF22726C2C8A600E3A955 /* ChainSyncEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840BF22626C2C8A600E3A955 /* ChainSyncEvents.swift */; }; 840C71B826821C0600B6D9C2 /* StakingRebondMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840C71B726821C0600B6D9C2 /* StakingRebondMock.swift */; }; @@ -540,7 +535,6 @@ 8425EA9525EA82CE00C307C9 /* AccountIdentity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8425EA9425EA82CE00C307C9 /* AccountIdentity.swift */; }; 8425EA9A25EA83FA00C307C9 /* ChainData+Value.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8425EA9925EA83FA00C307C9 /* ChainData+Value.swift */; }; 8425EB1B25EADD8600C307C9 /* StakingConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8425EB1A25EADD8600C307C9 /* StakingConstants.swift */; }; - 84265E042523D20A005EEE2D /* WalletBaseAmountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84265E032523D20A005EEE2D /* WalletBaseAmountView.swift */; }; 84282FBD26D05A54002CA322 /* ChainRegistryFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84282FBC26D05A54002CA322 /* ChainRegistryFacade.swift */; }; 8428765224ADDE0200D91AD8 /* ProfilePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8428764324ADDE0200D91AD8 /* ProfilePresenter.swift */; }; 8428765424ADDE0200D91AD8 /* ProfileViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8428764724ADDE0200D91AD8 /* ProfileViewModelFactory.swift */; }; @@ -619,7 +613,6 @@ 8434C9EA2540AE51009E4191 /* ExtrinsicEraTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8434C9E92540AE51009E4191 /* ExtrinsicEraTests.swift */; }; 8436E94426C853E4003D4EA7 /* RuntimeSnapshotOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8436E94326C853E4003D4EA7 /* RuntimeSnapshotOperationFactory.swift */; }; 8436E94626C85405003D4EA7 /* RuntimeSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8436E94526C85405003D4EA7 /* RuntimeSnapshot.swift */; }; - 8436EDDC25893FA0004D9E97 /* WalletBuyCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8436EDDB25893FA0004D9E97 /* WalletBuyCommand.swift */; }; 8436EDE225895804004D9E97 /* RampProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8436EDE125895804004D9E97 /* RampProvider.swift */; }; 8436EDE725895846004D9E97 /* PurchaseProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8436EDE625895846004D9E97 /* PurchaseProvider.swift */; }; 8436EDEF25896722004D9E97 /* PurchaseAggregator+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8436EDEE25896722004D9E97 /* PurchaseAggregator+Default.swift */; }; @@ -718,7 +711,6 @@ 84563D0924F46B7F0055591D /* ManagedAccountItemMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84563D0824F46B7F0055591D /* ManagedAccountItemMapperTests.swift */; }; 84585A2F251BFC8400390F7A /* TriangularedButton+Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84585A2E251BFC8400390F7A /* TriangularedButton+Style.swift */; }; 84585A31251C0B9300390F7A /* NetworkInfoMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84585A30251C0B9300390F7A /* NetworkInfoMode.swift */; }; - 84585A34251CE2E300390F7A /* ContactsLocalSearchEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84585A33251CE2E300390F7A /* ContactsLocalSearchEngine.swift */; }; 845B821526EF657700D25C72 /* PersistentValueSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845B821426EF657700D25C72 /* PersistentValueSettings.swift */; }; 845B821726EF7FED00D25C72 /* SelectedWalletSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845B821626EF7FED00D25C72 /* SelectedWalletSettings.swift */; }; 845B821926EF808D00D25C72 /* MetaAccountMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845B821826EF808D00D25C72 /* MetaAccountMapper.swift */; }; @@ -748,12 +740,8 @@ 846042C02666E2C800CFFCFC /* KaruraResultData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846042BF2666E2C800CFFCFC /* KaruraResultData.swift */; }; 8460516D25536C4800A1F0B4 /* ExportOption+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8460516C25536C4800A1F0B4 /* ExportOption+ViewModel.swift */; }; 8460517225536E3F00A1F0B4 /* ExportGenericViewModelBinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8460517125536E3F00A1F0B4 /* ExportGenericViewModelBinder.swift */; }; - 8460E1162541E03400826F55 /* WalletFormCellStyle+Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8460E1152541E03400826F55 /* WalletFormCellStyle+Internal.swift */; }; 8460E11D2542011200826F55 /* DetailsTriangularedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8460E11C2542011200826F55 /* DetailsTriangularedView.swift */; }; 8460E11F25420A2C00826F55 /* DetailsTriangularedView+Inspectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8460E11E25420A2C00826F55 /* DetailsTriangularedView+Inspectable.swift */; }; - 8460E1252542293A00826F55 /* WalletCompoundDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8460E1242542293A00826F55 /* WalletCompoundDetailsView.swift */; }; - 8460E12725422A6D00826F55 /* WalletCompoundDetailsView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8460E12625422A6D00826F55 /* WalletCompoundDetailsView.xib */; }; - 8460E12925422C5A00826F55 /* WalletCompoundDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8460E12825422C5A00826F55 /* WalletCompoundDetailsViewModel.swift */; }; 8461CC7F26BBFEEA007460E4 /* ExtrinsicEraOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8461CC7E26BBFEEA007460E4 /* ExtrinsicEraOperationFactory.swift */; }; 8461CC8126BBFF70007460E4 /* ImmortalEraOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8461CC8026BBFF70007460E4 /* ImmortalEraOperationFactory.swift */; }; 8461CC8326BC0006007460E4 /* MortalEraOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8461CC8226BC0006007460E4 /* MortalEraOperationFactory.swift */; }; @@ -774,7 +762,6 @@ 84644A2B2567222A004EAA4B /* TriangularedBlurButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84644A2A2567222A004EAA4B /* TriangularedBlurButton.swift */; }; 84644A30256722D2004EAA4B /* TriangularedBlurButton+Inspectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84644A2F256722D2004EAA4B /* TriangularedBlurButton+Inspectable.swift */; }; 84644AC52567EC05004EAA4B /* MultilineTriangularedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84644AC42567EC05004EAA4B /* MultilineTriangularedView.swift */; }; - 84644B3325685915004EAA4B /* WalletHistoryBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84644B3225685915004EAA4B /* WalletHistoryBackgroundView.swift */; }; 8466BB42263F501000E065A8 /* StakingUnbondConfirmViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8466BB41263F501000E065A8 /* StakingUnbondConfirmViewModel.swift */; }; 8467F4C326B1DFA500C5B6F4 /* StakingDurationOperationFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8467F4C226B1DFA500C5B6F4 /* StakingDurationOperationFactoryTests.swift */; }; 8467F4C526B1E4E200C5B6F4 /* StakingDurationFetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8467F4C426B1E4E200C5B6F4 /* StakingDurationFetching.swift */; }; @@ -818,16 +805,11 @@ 846A2606267C792000429A7F /* CrowdloanContributionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846A2605267C792000429A7F /* CrowdloanContributionResponse.swift */; }; 846A2C4325271CDE00731018 /* TransactionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846A2C4225271CDE00731018 /* TransactionType.swift */; }; 846A2C4525271F0100731018 /* DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846A2C4425271F0100731018 /* DateFormatter.swift */; }; - 846A2C492527490700731018 /* WalletSelectAccountCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846A2C482527490700731018 /* WalletSelectAccountCommand.swift */; }; 846A2C4B2529F99400731018 /* AccountRepositoryFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846A2C4A2529F99400731018 /* AccountRepositoryFactory.swift */; }; 846A2C4D2529FBB700731018 /* NSPredicate+Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846A2C4C2529FBB700731018 /* NSPredicate+Filter.swift */; }; - 846A2C50252A063800731018 /* WalletSelectAccountCommandFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846A2C4F252A063800731018 /* WalletSelectAccountCommandFactory.swift */; }; 846A2C54252A0FDF00731018 /* FilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846A2C53252A0FDF00731018 /* FilterTests.swift */; }; 846AC7EF2638D9200075F7DA /* YourValidatorTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846AC7EE2638D9200075F7DA /* YourValidatorTableCell.swift */; }; 846AF8442525BE0100868F37 /* Price.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846AF8432525BE0100868F37 /* Price.swift */; }; - 846AF8462525C93A00868F37 /* BalanceContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846AF8452525C93A00868F37 /* BalanceContext.swift */; }; - 846AF8482525E23C00868F37 /* WalletTotalPriceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846AF8472525E23C00868F37 /* WalletTotalPriceCell.swift */; }; - 846AF84A2525E24C00868F37 /* WalletTotalPriceCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 846AF8492525E24C00868F37 /* WalletTotalPriceCell.xib */; }; 846B32A126DCDB7300250E89 /* SubqueryRewardSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846B32A026DCDB7300250E89 /* SubqueryRewardSource.swift */; }; 846B32A526DD07AF00250E89 /* rewardErrorResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = 846B32A426DD07AF00250E89 /* rewardErrorResponse.json */; }; 846C372E26B199D10098F303 /* StakingDurationOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846C372D26B199D10098F303 /* StakingDurationOperationFactory.swift */; }; @@ -881,7 +863,6 @@ 84754CA02513D83E00854599 /* AccountPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84754C9F2513D83E00854599 /* AccountPickerViewModel.swift */; }; 84754CA22513DB8800854599 /* EmptyAccountIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84754CA12513DB8800854599 /* EmptyAccountIcon.swift */; }; 84754CA42513E6DC00854599 /* PrimitiveContextWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84754CA32513E6DC00854599 /* PrimitiveContextWrapper.swift */; }; - 8475AE1F258B7F4A00B058F3 /* TransferMetadataContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8475AE1E258B7F4A00B058F3 /* TransferMetadataContext.swift */; }; 8475AE39258B958300B058F3 /* ErrorContent+Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8475AE38258B958300B058F3 /* ErrorContent+Wallet.swift */; }; 8476C9E025EF94F2003EEAE1 /* StakingViewModelFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8476C9DF25EF94F2003EEAE1 /* StakingViewModelFacade.swift */; }; 8476CA0625EFB072003EEAE1 /* SingleValueProviderFactoryStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8476CA0525EFB072003EEAE1 /* SingleValueProviderFactoryStub.swift */; }; @@ -906,7 +887,6 @@ 847C966325536455002D288F /* ExportRestoreJsonViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847C966225536455002D288F /* ExportRestoreJsonViewFactory.swift */; }; 847CD46626416D0900E1542F /* SlashesOperationFactoryStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847CD46526416D0900E1542F /* SlashesOperationFactoryStub.swift */; }; 847DD8DC26034B99003DE053 /* LocalizableViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847DD8DB26034B99003DE053 /* LocalizableViewProtocol.swift */; }; - 8485446546C43C075408F3B7 /* LiquidityPoolsListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = E137263F032552C463560C91 /* LiquidityPoolsListProtocols.swift */; }; 84873AFF26028E2B000A83EE /* StakingStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84873AFE26028E2B000A83EE /* StakingStateMachine.swift */; }; 84873B0426029B75000A83EE /* StakingEstimationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84873B0326029B75000A83EE /* StakingEstimationViewModel.swift */; }; 84873B0926029CBD000A83EE /* StakingStateViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84873B0826029CBD000A83EE /* StakingStateViewModelFactory.swift */; }; @@ -1002,16 +982,11 @@ 849014E024AA8F60008F705E /* MainTabBarWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849014D224AA8F60008F705E /* MainTabBarWireframe.swift */; }; 849014E824AA925C008F705E /* ScrollsToTop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849014E724AA925C008F705E /* ScrollsToTop.swift */; }; 849014FC24AA9939008F705E /* Charset+Mnemonic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849014FB24AA9939008F705E /* Charset+Mnemonic.swift */; }; - 849014FF24AB69A4008F705E /* WalletContextFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849014FE24AB69A4008F705E /* WalletContextFactory.swift */; }; 8490150F24AB8A3A008F705E /* WalletEmptyStateDataSource+Module.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8490150824AB8A3A008F705E /* WalletEmptyStateDataSource+Module.swift */; }; 8490151324AB8A3A008F705E /* WalletEmptyStateDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8490150E24AB8A3A008F705E /* WalletEmptyStateDataSource.swift */; }; - 8490151824AB8C6D008F705E /* WalletCommonStyleConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8490151724AB8C6D008F705E /* WalletCommonStyleConfigurator.swift */; }; - 8490151D24ABC57A008F705E /* AssetStyleFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8490151C24ABC57A008F705E /* AssetStyleFactory.swift */; }; 8490152124ABC721008F705E /* WalletStaticImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8490152024ABC721008F705E /* WalletStaticImageViewModel.swift */; }; - 8490152324ABC77E008F705E /* WalletAssetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8490152224ABC77E008F705E /* WalletAssetViewModel.swift */; }; 8490152524ABCBF3008F705E /* AmountFormatterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8490152424ABCBF3008F705E /* AmountFormatterFactory.swift */; }; 8490152724ABCC40008F705E /* NumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8490152624ABCC40008F705E /* NumberFormatter.swift */; }; - 8490152924ABCFDA008F705E /* WalletCommandDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8490152824ABCFDA008F705E /* WalletCommandDecorator.swift */; }; 8490152E24ABD0F5008F705E /* Logger+Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8490152D24ABD0F5008F705E /* Logger+Wallet.swift */; }; 8490386B262E22DC0016D541 /* NominatorPayoutInfoFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8490386A262E22DC0016D541 /* NominatorPayoutInfoFactory.swift */; }; 849244922514EDE800477C1B /* SelectableViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849244912514EDE800477C1B /* SelectableViewModel.swift */; }; @@ -1023,11 +998,6 @@ 8493D3E927059B6700157009 /* StakingServiceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8493D3E827059B6700157009 /* StakingServiceFactory.swift */; }; 8494424A265306BD0016E7BD /* ChangeRewardDestinationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84944249265306BD0016E7BD /* ChangeRewardDestinationViewModel.swift */; }; 8494425F265330650016E7BD /* StakingRewardDestinationSetupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8494425E265330650016E7BD /* StakingRewardDestinationSetupTests.swift */; }; - 8494D8552524633300614D8F /* WalletTransferTokenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8494D8542524633300614D8F /* WalletTransferTokenView.swift */; }; - 8494D857252465AC00614D8F /* WalletDisplayTokenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8494D856252465AC00614D8F /* WalletDisplayTokenView.swift */; }; - 8494D85E25246E9A00614D8F /* WalletTokenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8494D85D25246E9A00614D8F /* WalletTokenViewModel.swift */; }; - 8494D860252470F400614D8F /* WalletFearlessFormDefining.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8494D85F252470F400614D8F /* WalletFearlessFormDefining.swift */; }; - 8494D8632524753400614D8F /* WalletCopyCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8494D8622524753400614D8F /* WalletCopyCommand.swift */; }; 8494D86B25247F9600614D8F /* Decimal+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8494D86A25247F9600614D8F /* Decimal+String.swift */; }; 8494D8702525321700614D8F /* SubscanDefinitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8494D86F2525321700614D8F /* SubscanDefinitions.swift */; }; 8494D8782525343500614D8F /* SubscanOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8494D8772525343500614D8F /* SubscanOperationFactory.swift */; }; @@ -1040,7 +1010,6 @@ 84963D6E26F91826003FE8E4 /* RemoteSubscriptionRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84963D6D26F91826003FE8E4 /* RemoteSubscriptionRequests.swift */; }; 84963D7026F92F18003FE8E4 /* WalletRemoteSubscriptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84963D6F26F92F18003FE8E4 /* WalletRemoteSubscriptionService.swift */; }; 84969732251CE71500C39524 /* WalletAssetId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84969731251CE71500C39524 /* WalletAssetId.swift */; }; - 84969734251CE9CD00C39524 /* ContactsConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84969733251CE9CD00C39524 /* ContactsConfigurator.swift */; }; 8497FC6026317783002FEAA7 /* AccountInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8497FC5F26317783002FEAA7 /* AccountInfoViewModel.swift */; }; 84981CC32666D95F00C4C691 /* GradientButton+Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84981CC22666D95F00C4C691 /* GradientButton+Style.swift */; }; 849842E626587573006BBB9F /* MultilineTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849842E526587573006BBB9F /* MultilineTableViewCell.swift */; }; @@ -1069,26 +1038,20 @@ 849DF02D26C40B7900B702F4 /* RuntimeFilesOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849DF02C26C40B7900B702F4 /* RuntimeFilesOperationFactory.swift */; }; 849DF02F26C53DB900B702F4 /* RuntimeSyncEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849DF02E26C53DB900B702F4 /* RuntimeSyncEvents.swift */; }; 849E0CD025CFDDB700B33506 /* StorageUpdateData+Decoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849E0CCF25CFDDB700B33506 /* StorageUpdateData+Decoding.swift */; }; - 849E2332254AEFB400B1F6D4 /* InvoiceScanConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849E2331254AEFB400B1F6D4 /* InvoiceScanConfigurator.swift */; }; - 849E2351254AF44200B1F6D4 /* InvoiceScanLocalSearchEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849E2350254AF44200B1F6D4 /* InvoiceScanLocalSearchEngine.swift */; }; 849E689526AF388500E0E7BE /* ElectedValidatorInfo+Selected.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849E689426AF388500E0E7BE /* ElectedValidatorInfo+Selected.swift */; }; 849ECD3526DE70B900F542A3 /* SingleToMultiassetUserMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849ECD3426DE70B900F542A3 /* SingleToMultiassetUserMigrationTests.swift */; }; 849ECD3926DF792800F542A3 /* SkeletonRow+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849ECD3826DF792800F542A3 /* SkeletonRow+View.swift */; }; 84A15489262888CA0050D557 /* IdentityOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A15488262888CA0050D557 /* IdentityOperationFactory.swift */; }; 84A259F82555C8C9001E91BC /* CryptoType+Keystore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A259F72555C8C9001E91BC /* CryptoType+Keystore.swift */; }; - 84A2C90224E07E440020D3B7 /* CryptoType+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A2C90124E07E440020D3B7 /* CryptoType+Utils.swift */; }; 84A2C90424E07F400020D3B7 /* AccountOperationFactoryError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A2C90324E07F400020D3B7 /* AccountOperationFactoryError.swift */; }; 84A2C90724E09DC20020D3B7 /* AccountOperationFactoryError+Presentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A2C90624E09DC20020D3B7 /* AccountOperationFactoryError+Presentable.swift */; }; 84A2C90C24E192F50020D3B7 /* ShakeAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A2C90B24E192F50020D3B7 /* ShakeAnimator.swift */; }; - 84A2CD5325685A4100A9FB0D /* WalletHistoryViewFactoryOverriding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A2CD5225685A4100A9FB0D /* WalletHistoryViewFactoryOverriding.swift */; }; 84A3034926A834F900E64382 /* ValidatorInfoViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A3034826A834F900E64382 /* ValidatorInfoViewLayout.swift */; }; 84A6171B2625AC3E007B75E1 /* CallCodingPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A6171A2625AC3E007B75E1 /* CallCodingPath.swift */; }; 84A617262625AF51007B75E1 /* RuntimeMetadata+Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A617252625AF51007B75E1 /* RuntimeMetadata+Internal.swift */; }; 84A8FD8E265FDA76002ADB58 /* CrowdloanContributionConfirmData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A8FD8D265FDA76002ADB58 /* CrowdloanContributionConfirmData.swift */; }; 84AA004326C5DFD800BCB4DC /* RuntimeSyncServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AA004226C5DFD800BCB4DC /* RuntimeSyncServiceTests.swift */; }; - 84ACEBF2261E664900AAE665 /* WalletHistoryFilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84ACEBF1261E664900AAE665 /* WalletHistoryFilterViewModel.swift */; }; 84ACEBFA261E684A00AAE665 /* WalletHistoryFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84ACEBF9261E684A00AAE665 /* WalletHistoryFilter.swift */; }; - 84ACEBFF261E6C7C00AAE665 /* WalletHistoryFilterViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84ACEBFE261E6C7C00AAE665 /* WalletHistoryFilterViewLayout.swift */; }; 84B018AC26E01A4100C75E28 /* StakingStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B018AB26E01A4100C75E28 /* StakingStateView.swift */; }; 84B018AE26E03FB500C75E28 /* NominatorStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B018AD26E03FB500C75E28 /* NominatorStateView.swift */; }; 84B018B026E0450F00C75E28 /* ValidatorStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B018AF26E0450F00C75E28 /* ValidatorStateView.swift */; }; @@ -1118,11 +1081,7 @@ 84BEE22325646AC000D05EB3 /* SelectedUsernameChanged.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BEE22225646ABF00D05EB3 /* SelectedUsernameChanged.swift */; }; 84BEE22D2564765F00D05EB3 /* UIAlertViewController+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BEE22C2564765F00D05EB3 /* UIAlertViewController+Account.swift */; }; 84BEE63026CBFDBC00757009 /* ChainRegistryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BEE62F26CBFDBC00757009 /* ChainRegistryTests.swift */; }; - 84BEF601258938C1002A22E2 /* WalletActionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BEF600258938C1002A22E2 /* WalletActionsViewModel.swift */; }; 84C07EDC24AF1924008FC35B /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 84C07EDA24AF1924008FC35B /* InfoPlist.strings */; }; - 84C07EE824AF2E4D008FC35B /* WalletHistoryStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C07EE724AF2E4D008FC35B /* WalletHistoryStyle.swift */; }; - 84C07EEA24AF303D008FC35B /* WalletActionsCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 84C07EE924AF303D008FC35B /* WalletActionsCell.xib */; }; - 84C07EEC24AF30F2008FC35B /* WalletActionsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C07EEB24AF30F2008FC35B /* WalletActionsCell.swift */; }; 84C1B98424F5245700FE5470 /* ManagedAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C1B98324F5245700FE5470 /* ManagedAccountViewModel.swift */; }; 84C1B98624F5424700FE5470 /* ManagedAccountViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C1B98524F5424700FE5470 /* ManagedAccountViewModelFactory.swift */; }; 84C2F27725E296CD0050A4AD /* RewardDestinationViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C2F27625E296CD0050A4AD /* RewardDestinationViewModelFactory.swift */; }; @@ -1145,7 +1104,6 @@ 84C74365251E4D60009576C6 /* SigningWrapperProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C74364251E4D60009576C6 /* SigningWrapperProtocol.swift */; }; 84C7DA5425EE2DF000F8C318 /* StakingErrorPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C7DA5325EE2DF000F8C318 /* StakingErrorPresentable.swift */; }; 84C91FAA261E724F002796B9 /* SwitchTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C91FA9261E724F002796B9 /* SwitchTableViewCell.swift */; }; - 84C91FAF261E7CDD002796B9 /* WalletHistoryFilterEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C91FAE261E7CDD002796B9 /* WalletHistoryFilterEditor.swift */; }; 84CA68CF26BD6872003B9453 /* RuntimeSyncService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CA68CE26BD6872003B9453 /* RuntimeSyncService.swift */; }; 84CA68D126BE99ED003B9453 /* RuntimeProviderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CA68D026BE99ED003B9453 /* RuntimeProviderFactory.swift */; }; 84CA68D326BE9A35003B9453 /* RuntimeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CA68D226BE9A35003B9453 /* RuntimeProvider.swift */; }; @@ -1159,10 +1117,6 @@ 84CB2250270360AC0041C8C1 /* RelaychainStakingLocalSubscriptionFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CB224F270360AC0041C8C1 /* RelaychainStakingLocalSubscriptionFactory.swift */; }; 84CCBFBC2509709500180F4F /* UIBarButtonItem+Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCBFBB2509709500180F4F /* UIBarButtonItem+Style.swift */; }; 84CD3570252620FB0081BC0B /* CryptoType+Extrinsic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CD356F252620FB0081BC0B /* CryptoType+Extrinsic.swift */; }; - 84CD357325264F640081BC0B /* WalletTotalPriceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CD357225264F640081BC0B /* WalletTotalPriceViewModel.swift */; }; - 84CD357525264FA30081BC0B /* WalletAccountListConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CD357425264FA30081BC0B /* WalletAccountListConstants.swift */; }; - 84CD3577252654EA0081BC0B /* WalletAssetCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CD3576252654EA0081BC0B /* WalletAssetCell.swift */; }; - 84CD35792526554A0081BC0B /* WalletAssetCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 84CD35782526554A0081BC0B /* WalletAssetCell.xib */; }; 84CD82AE263C1452001A6F01 /* SubstrateProviderSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CD82AD263C1452001A6F01 /* SubstrateProviderSubscriber.swift */; }; 84CD82B3263C30BC001A6F01 /* SubstrateProviderSubscriptionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CD82B2263C30BC001A6F01 /* SubstrateProviderSubscriptionHandler.swift */; }; 84CE69DD2565BB6400559427 /* AboutViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE69DC2565BB6400559427 /* AboutViewModel.swift */; }; @@ -1214,12 +1168,9 @@ 84D8F17124D856D300AF43E9 /* SNAddressType+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D8F17024D856D300AF43E9 /* SNAddressType+ViewModel.swift */; }; 84D97EC1251FEE1E00F07405 /* WalletAssetId+Display.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D97EC0251FEE1E00F07405 /* WalletAssetId+Display.swift */; }; 84D97EC82520D32000F07405 /* PolkadotIcon+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D97EC72520D32000F07405 /* PolkadotIcon+Image.swift */; }; - 84D97ECF2521CA2F00F07405 /* WalletBaseTokenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D97ECE2521CA2F00F07405 /* WalletBaseTokenView.swift */; }; - 84D97ED12521CA5200F07405 /* WalletTokenView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 84D97ED02521CA5200F07405 /* WalletTokenView.xib */; }; 84D9C41126CD361C004AB2AB /* SpecVersionSubscriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D9C41026CD361C004AB2AB /* SpecVersionSubscriptionTests.swift */; }; 84DA3B1224C6D29100B5E27F /* RuntimeVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DA3B1124C6D29100B5E27F /* RuntimeVersion.swift */; }; 84DA3B1424C6D7C700B5E27F /* RuntimeDispatchInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DA3B1324C6D7C700B5E27F /* RuntimeDispatchInfo.swift */; }; - 84DA3B1624C81ECE00B5E27F /* AccountItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DA3B1524C81ECE00B5E27F /* AccountItem.swift */; }; 84DA3B1924C8200E00B5E27F /* ConnectionItem+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DA3B1824C8200E00B5E27F /* ConnectionItem+Default.swift */; }; 84DAC198268D3DD9002D0DF4 /* SNAddressType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DAC197268D3DD9002D0DF4 /* SNAddressType.swift */; }; 84DB4E1E25E93B1700A6DF41 /* Identity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DB4E1D25E93B1700A6DF41 /* Identity.swift */; }; @@ -1253,8 +1204,6 @@ 84DF21A125347031005454AE /* DetailsDisplayTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DF21A025347031005454AE /* DetailsDisplayTableViewCell.swift */; }; 84DF21A325347042005454AE /* DetailsDisplayTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 84DF21A225347042005454AE /* DetailsDisplayTableViewCell.xib */; }; 84DF21A5253473B0005454AE /* ModalInfoFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DF21A4253473B0005454AE /* ModalInfoFactory.swift */; }; - 84DF21A92535AA8F005454AE /* ExistentialDepositInfoCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DF21A82535AA8F005454AE /* ExistentialDepositInfoCommand.swift */; }; - 84DF21B12536DDC1005454AE /* TransferConfirmCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DF21B02536DDC1005454AE /* TransferConfirmCommand.swift */; }; 84DF21B525388B35005454AE /* Scheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DF21B425388B35005454AE /* Scheduler.swift */; }; 84E1CCF5260DCB91001E81B5 /* SwitchAccount+AccountManagementWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CCF4260DCB91001E81B5 /* SwitchAccount+AccountManagementWireframe.swift */; }; 84E1CCFA260DCBF9001E81B5 /* SwitchAccount+UsernameSetupWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CCF9260DCBF9001E81B5 /* SwitchAccount+UsernameSetupWireframe.swift */; }; @@ -1332,11 +1281,6 @@ 84FAB0652542CA4200319F74 /* CDContactItem+CoreDataDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FAB0642542CA4200319F74 /* CDContactItem+CoreDataDecodable.swift */; }; 84FAB0672542D06B00319F74 /* WalletContactOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FAB0662542D06B00319F74 /* WalletContactOperationFactory.swift */; }; 84FAB0692542EBDE00319F74 /* SearchData+Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FAB0682542EBDE00319F74 /* SearchData+Contacts.swift */; }; - 84FAB06B2542F2C700319F74 /* ContactsViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FAB06A2542F2C700319F74 /* ContactsViewModelFactory.swift */; }; - 84FAB06D254303DA00319F74 /* ContactViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FAB06C254303DA00319F74 /* ContactViewModel.swift */; }; - 84FAB072254366ED00319F74 /* ContactTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FAB071254366ED00319F74 /* ContactTableViewCell.swift */; }; - 84FAB07425436D6300319F74 /* ContactsConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FAB07325436D6300319F74 /* ContactsConstants.swift */; }; - 84FAB076254378FC00319F74 /* ContactsListViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FAB075254378FC00319F74 /* ContactsListViewModelFactory.swift */; }; 84FAB0782543791A00319F74 /* ContactContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FAB0772543791A00319F74 /* ContactContext.swift */; }; 84FACB1725F559F200F32ED4 /* WestendStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FACB1625F559F200F32ED4 /* WestendStub.swift */; }; 84FACB1F25F55EC900F32ED4 /* RuntimeCodingServiceStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FACB1E25F55EC900F32ED4 /* RuntimeCodingServiceStub.swift */; }; @@ -1346,7 +1290,6 @@ 84FACB6A25F5759C00F32ED4 /* AccountCreationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8467FD3824EACE08005D486C /* AccountCreationHelper.swift */; }; 84FACB7125F57E4400F32ED4 /* SubstrateStorageTestFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F4387E25D9D61300AEDA56 /* SubstrateStorageTestFacade.swift */; }; 84FACCD925F8C22A00F32ED4 /* BigInt+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FACCD825F8C22A00F32ED4 /* BigInt+Hex.swift */; }; - 84FB1F652526879200E0242B /* WalletSingleProviderIdFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FB1F642526879200E0242B /* WalletSingleProviderIdFactory.swift */; }; 84FB1F672526920B00E0242B /* SubscanTransferData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FB1F662526920B00E0242B /* SubscanTransferData.swift */; }; 84FB1F69252694B600E0242B /* HistoryInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FB1F68252694B600E0242B /* HistoryInfo.swift */; }; 84FB1F6D2526987D00E0242B /* TransactionHistoryContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FB1F6C2526987D00E0242B /* TransactionHistoryContext.swift */; }; @@ -1398,7 +1341,6 @@ 94B0F0C84AF74B3CD7223C3A /* AccountConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7306D50F278F6CC90DC88F27 /* AccountConfirmPresenter.swift */; }; 94EB0971EDA635A626CA8B72 /* StakingPoolCreateInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D9DD27C76FE239728ED5F8 /* StakingPoolCreateInteractor.swift */; }; 9565BEB636E6D386B0C0FBE5 /* StakingPayoutConfirmationViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BE0492B98AB9C1540846B39 /* StakingPayoutConfirmationViewFactory.swift */; }; - 95F3EB368D1DF5503DF40D4F /* LiquidityPoolsListViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D491B2698446FA356D6D800B /* LiquidityPoolsListViewLayout.swift */; }; 9659B32D4622C8D9679298DF /* ChainSelectionViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15D1C78B2844216802DA000 /* ChainSelectionViewFactory.swift */; }; 96B2C3B29C0EA1A068ED5FB1 /* WalletSendConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF834BF779244B8AF7746B04 /* WalletSendConfirmProtocols.swift */; }; 96EBE5F57C97C7A7A525E864 /* SwapTransactionDetailProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC265B5F209B038633AE0E3F /* SwapTransactionDetailProtocols.swift */; }; @@ -1422,9 +1364,9 @@ 9F0457A013858A7ADEB41234 /* NftSendConfirmTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE7D061906CF67230BF5393A /* NftSendConfirmTests.swift */; }; 9F4A48B1BE3A1110A0CF9F36 /* ReferralCrowdloanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DDDB2B35CD3299F50613141 /* ReferralCrowdloanViewController.swift */; }; 9F664DA212363F5F2C2B530B /* SelectMarketRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C05A688EA7379572BBCE545 /* SelectMarketRouter.swift */; }; - A06CF2CB8E8AEC2E67B6A496 /* LiquidityPoolsListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65D8653B07D92DFE321CEF1E /* LiquidityPoolsListPresenter.swift */; }; A090FF206B56A0E465C62072 /* CrowdloanListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86F7A369E31DCB9ABD556EE9 /* CrowdloanListPresenter.swift */; }; A14F444FA808457C16EF826C /* WalletOptionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193908913A5B241C82672D8A /* WalletOptionViewController.swift */; }; + A1C05D0028CD04C16AB6082F /* LiquidityPoolDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67BDE520860A67C800E7F4AB /* LiquidityPoolDetailsViewController.swift */; }; A29F1452BF0AEB885E6460E2 /* SelectMarketPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E07F60661001B2C505D9C8B /* SelectMarketPresenter.swift */; }; A32E1373E3671D518FFC3BC2 /* YourValidatorListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B90CEC70F101AA25A4C00021 /* YourValidatorListViewController.swift */; }; A3DF7ECA1FB8F458F60D7C8A /* StakingPoolCreateAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = F312CA3A7087424A540614DD /* StakingPoolCreateAssembly.swift */; }; @@ -1562,14 +1504,12 @@ AEF50586261EE6230098574D /* PurchaseProviderPickerTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEF50585261EE6230098574D /* PurchaseProviderPickerTableViewCell.swift */; }; AEF5058B261EF27B0098574D /* PurchaseProviderPickerTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = AEF5058A261EF27B0098574D /* PurchaseProviderPickerTableViewCell.xib */; }; AEF505A92620249F0098574D /* UIColor+HEX.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEF505A82620249F0098574D /* UIColor+HEX.swift */; }; - AEF50716262359DC0098574D /* WalletSelectPurchaseProviderCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEF50715262359DC0098574D /* WalletSelectPurchaseProviderCommand.swift */; }; AEF5071E262369C00098574D /* PurchaseProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEF5071D262369C00098574D /* PurchaseProviderProtocol.swift */; }; AEF5072A26236F9E0098574D /* WalletPurchaseProvidersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEF5072926236F9E0098574D /* WalletPurchaseProvidersTests.swift */; }; AEF507AF262423FD0098574D /* HmacSigner.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEF507AE262423FD0098574D /* HmacSigner.swift */; }; AEF507BA262486F80098574D /* MoonpayProviderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEF507B9262486F80098574D /* MoonpayProviderFactory.swift */; }; AEF507F226259DF00098574D /* ValidationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEF507F126259DF00098574D /* ValidationViewModel.swift */; }; AEF507F72625A3280098574D /* ValidatorState+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEF507F62625A3280098574D /* ValidatorState+Status.swift */; }; - AEF73FB525DBA24300407D41 /* PhishingCheckExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEF73FB425DBA24300407D41 /* PhishingCheckExecutor.swift */; }; AEF7404E25E6DC9400407D41 /* RewardCalculatorEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEF7404D25E6DC9400407D41 /* RewardCalculatorEngine.swift */; }; AEFA82BC4285117096BCBB16 /* StakingPayoutConfirmationInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBFDF844248CD43AAD13139F /* StakingPayoutConfirmationInteractor.swift */; }; AEFC6D622600A772000BD310 /* StoriesPreviewCollectionItem.xib in Resources */ = {isa = PBXBuildFile; fileRef = AEFC6D612600A772000BD310 /* StoriesPreviewCollectionItem.xib */; }; @@ -1607,7 +1547,6 @@ C3C7D60B36C778DA0A307BCC /* AddCustomNodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CD8A37F219A0BCC0C6063E /* AddCustomNodeViewController.swift */; }; C3D915637D26E807B85957CF /* NodeSelectionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABDA8952766DE724CD078D6 /* NodeSelectionPresenter.swift */; }; C4427244A22EA7BA7F7C9E9F /* StakingRedeemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403D8D690B564EDC04996945 /* StakingRedeemTests.swift */; }; - C45A034C51D3FE788E304291 /* LiquidityPoolsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 101F1829F8CB980B39A7F14A /* LiquidityPoolsListViewController.swift */; }; C46EEF6A9A9A601694E72DB1 /* StakingMainWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F09665083031502F9693F8 /* StakingMainWireframe.swift */; }; C481665C170F4D9523DC73AF /* WarningAlertViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55EEBFA4A54B3AAF9F52855 /* WarningAlertViewLayout.swift */; }; C4A4D40A08DAB4A71C21C1A8 /* StakingRedeemInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560C48D7A83F51F001622D71 /* StakingRedeemInteractor.swift */; }; @@ -1731,11 +1670,13 @@ CDDAAAD299F26A8B39DABB65 /* WalletHistoryFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2648EEF96694A7FEC94520E8 /* WalletHistoryFilterTests.swift */; }; CE2792E78B14CE02394D8CF4 /* ReferralCrowdloanViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 594BC61689EC942ED0A64A4A /* ReferralCrowdloanViewLayout.swift */; }; CEB3D7F9A5DDA211758DC9BF /* ChainSelectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A64FCFC95E3841032F910B /* ChainSelectionTests.swift */; }; + CF96BE0B636DA8227E689DDA /* LiquidityPoolDetailsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FED48DE9B681995E6E4A581 /* LiquidityPoolDetailsProtocols.swift */; }; CFAC59D34B15A2FAC4CD8256 /* WalletMainContainerInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC72ACCA21471C6A9580180 /* WalletMainContainerInteractor.swift */; }; D1C6EABB48DC3EE254E5A095 /* CrowdloanContributionConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28F5B57A24265C36A5F19B78 /* CrowdloanContributionConfirmPresenter.swift */; }; D1E085712E7BC0EBF2F4F020 /* WalletTransactionHistoryPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8F8199B216D810B36CD4043 /* WalletTransactionHistoryPresenter.swift */; }; D22D29F8EC4C7522345C8772 /* StakingPoolCreateConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9416E68AE4D5613E9434226 /* StakingPoolCreateConfirmViewController.swift */; }; D23DD717E3EA941FA78B59C9 /* WalletChainAccountDashboardViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB719C069A26244D194C4374 /* WalletChainAccountDashboardViewFactory.swift */; }; + D2A85A5EE89EAAA856EA5C0F /* LiquidityPoolDetailsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81E9DD6EB14A352635BAC711 /* LiquidityPoolDetailsPresenter.swift */; }; D344C6DAC1F8BB6152BA8DD0 /* RecommendedValidatorListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C6573C52692E4A56E35FF9 /* RecommendedValidatorListProtocols.swift */; }; D3B48F82A875E301D749AC0B /* StakingUnbondConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5674162035C7D9F226FA9964 /* StakingUnbondConfirmViewController.swift */; }; D3BB5CACD4790A601BF01AF5 /* NftCollectionInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C7AAA265DB437D2CDDC165E /* NftCollectionInteractor.swift */; }; @@ -1759,7 +1700,6 @@ DBA6A0A26D77E7A587C51792 /* PolkaswapSwapConfirmationProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA6FC016067C632AF256EB62 /* PolkaswapSwapConfirmationProtocols.swift */; }; DBBD1651F45FECA1B17AAF40 /* CreateContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 480AE579B48B3DA9C247CCB5 /* CreateContactViewController.swift */; }; DCDAE9E8C805B71A8F8CEFBD /* YourValidatorListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890203CBFFCBF517C0BAA396 /* YourValidatorListTests.swift */; }; - DD090C2ED91726FF7779F6C7 /* WalletHistoryFilterViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AB10C8DA56E92C280D66BE8 /* WalletHistoryFilterViewFactory.swift */; }; DD1ADD4F777B0C6398C73805 /* NftDetailsRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B08B264C09E63A936384E2A /* NftDetailsRouter.swift */; }; DD96B37BBEE1535951802B55 /* AllDoneProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C90AA5852CFA841CED20631 /* AllDoneProtocols.swift */; }; DDEEF4532805420415471B6A /* NftDetailsAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194E6EDD65CA3FB0B1F6727C /* NftDetailsAssembly.swift */; }; @@ -1767,6 +1707,7 @@ DE6DC04509EFEE35FFBADC59 /* NftSendAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49C564052FAA3160AA8975CB /* NftSendAssembly.swift */; }; DE9FA0FA5A6B685CBD593025 /* AllDoneInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C705CA1083C1EE58426D90CD /* AllDoneInteractor.swift */; }; DFD5EDA2F7C096DB3A5368E4 /* CustomValidatorListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9A16451B21451996CAA31F8 /* CustomValidatorListTests.swift */; }; + DFF0320CF3AA41142DEAC5F2 /* LiquidityPoolsOverviewInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6717FF1B7777400B62F028C3 /* LiquidityPoolsOverviewInteractor.swift */; }; E14F809C3917EFA4B5388AC8 /* AccountConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A14CA4551FCC2EBD078E2242 /* AccountConfirmViewFactory.swift */; }; E1772980B5A4EB33D1801204 /* PolkaswapAdjustmentViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25FF82C2FD912021A1F20876 /* PolkaswapAdjustmentViewLayout.swift */; }; E2645EB7614F4C7A60B48777 /* PolkaswapAdjustmentProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A9EBD3B7AEA5EF594DFEB49 /* PolkaswapAdjustmentProtocols.swift */; }; @@ -1944,6 +1885,7 @@ F4FDA0F826A57626003D753B /* EraCountdownOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4FDA0F726A57626003D753B /* EraCountdownOperationFactory.swift */; }; F4FDA0FD26A57860003D753B /* EraCountdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4FDA0FC26A57860003D753B /* EraCountdown.swift */; }; F50D9C2CCA49D396E0D6EFDD /* WalletTransactionDetailsViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80247ADAAB061D1A10856B2 /* WalletTransactionDetailsViewLayout.swift */; }; + F52C9FF9ABB4ED034D177CF8 /* LiquidityPoolsOverviewRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F25F3A88B4BEB4DE498220C /* LiquidityPoolsOverviewRouter.swift */; }; F59DC31DD3D95BC0A6620773 /* NftDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA7427142A4B905B5DB15498 /* NftDetailsViewController.swift */; }; F643D1682E6787E13385F432 /* NftSendTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C8249F3F41DC0FFCF27EFF /* NftSendTests.swift */; }; F6CD0ED1CF9A605222745BEC /* ContactsRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B53E901B1475DE858A2C99 /* ContactsRouter.swift */; }; @@ -1961,6 +1903,8 @@ FA004899282CCFCD0032FF49 /* SelectValidatorsStartParachainViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA004898282CCFCD0032FF49 /* SelectValidatorsStartParachainViewModelFactory.swift */; }; FA00489B282CCFDC0032FF49 /* SelectValidatorsStartRelaychainViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA00489A282CCFDC0032FF49 /* SelectValidatorsStartRelaychainViewModelFactory.swift */; }; FA0066E92935D07D0068FC61 /* RecommendedValidatorListPoolStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA0066E82935D07D0068FC61 /* RecommendedValidatorListPoolStrategy.swift */; }; + FA054A9A2BCD1FA3007B8F6D /* AvailableLiquidityPoolsListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA054A992BCD1FA3007B8F6D /* AvailableLiquidityPoolsListPresenter.swift */; }; + FA054A9C2BCD1FAF007B8F6D /* AvailableLiquidityPoolsListViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA054A9B2BCD1FAF007B8F6D /* AvailableLiquidityPoolsListViewModelFactory.swift */; }; FA072C14277AE2FE00731718 /* QRCaptureService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA072C13277AE2FE00731718 /* QRCaptureService.swift */; }; FA072C16277B023D00731718 /* QRExtractionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA072C15277B023D00731718 /* QRExtractionService.swift */; }; FA072C21277B19A900731718 /* ImageGalleryProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA072C20277B19A900731718 /* ImageGalleryProtocols.swift */; }; @@ -1995,6 +1939,26 @@ FA17B4D427E9CF2D006E0735 /* UtilityConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA17B4D327E9CF2C006E0735 /* UtilityConstants.swift */; }; FA1A023C274F51A900DA07CB /* ChainAccountBalanceTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1A023B274F51A900DA07CB /* ChainAccountBalanceTableCell.swift */; }; FA1A023E274F55D900DA07CB /* HorizontalKeyValueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1A023D274F55D900DA07CB /* HorizontalKeyValueView.swift */; }; + FA1D01F92BBE713D005B7071 /* LiquidityPoolListCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1D01EE2BBE713D005B7071 /* LiquidityPoolListCellModel.swift */; }; + FA1D01FA2BBE713D005B7071 /* LiquidityPoolListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1D01EF2BBE713D005B7071 /* LiquidityPoolListViewModel.swift */; }; + FA1D01FB2BBE713D005B7071 /* LiquidityPoolsListViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1D01F02BBE713D005B7071 /* LiquidityPoolsListViewLayout.swift */; }; + FA1D01FC2BBE713D005B7071 /* LiquidityPoolsListRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1D01F12BBE713D005B7071 /* LiquidityPoolsListRouter.swift */; }; + FA1D01FD2BBE713D005B7071 /* LiquidityPoolsListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1D01F22BBE713D005B7071 /* LiquidityPoolsListProtocols.swift */; }; + FA1D01FF2BBE713D005B7071 /* LiquidityPoolsListAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1D01F42BBE713D005B7071 /* LiquidityPoolsListAssembly.swift */; }; + FA1D02002BBE713D005B7071 /* LiquidityPoolListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1D01F62BBE713D005B7071 /* LiquidityPoolListCell.swift */; }; + FA1D02012BBE713D005B7071 /* LiquidityPoolsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1D01F72BBE713D005B7071 /* LiquidityPoolsListViewController.swift */; }; + FA1D02042BBE71F2005B7071 /* TokenPairIconsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1D02032BBE71F2005B7071 /* TokenPairIconsView.swift */; }; + FA1D02062BBE71F9005B7071 /* TokenPairsIconViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1D02052BBE71F9005B7071 /* TokenPairsIconViewModel.swift */; }; + FA1D02092BBE74B7005B7071 /* AvailableLiquidityPoolsListInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1D02082BBE74B7005B7071 /* AvailableLiquidityPoolsListInteractor.swift */; }; + FA1D020D2BBE7690005B7071 /* UserLiquidityPoolsListInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1D020C2BBE7690005B7071 /* UserLiquidityPoolsListInteractor.swift */; }; + FA1D51D72BCFD445001353E7 /* LiquidityPools+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1D51D62BCFD445001353E7 /* LiquidityPools+ViewModel.swift */; }; + FA1D51D92BCFE353001353E7 /* SoraFiatService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1D51D82BCFE353001353E7 /* SoraFiatService.swift */; }; + FA1D51DC2BCFE38D001353E7 /* SubqueryFiatInfoOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1D51DB2BCFE38D001353E7 /* SubqueryFiatInfoOperation.swift */; }; + FA22228D2BD237910031DE04 /* SubqueryPriceFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA22228C2BD237910031DE04 /* SubqueryPriceFetcher.swift */; }; + FA22228F2BD237CF0031DE04 /* SoraSubqueryPriceFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA22228E2BD237CF0031DE04 /* SoraSubqueryPriceFetcher.swift */; }; + FA2222912BD239500031DE04 /* SoraSubqueryPriceResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA2222902BD239500031DE04 /* SoraSubqueryPriceResponse.swift */; }; + FA2222942BD2726F0031DE04 /* SkeletonLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA2222932BD2726F0031DE04 /* SkeletonLabel.swift */; }; + FA2222962BD272A30031DE04 /* SkeletonLoadableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA2222952BD272A30031DE04 /* SkeletonLoadableView.swift */; }; FA24FEFE2B95C32200CD9E04 /* Decimal+DoubleValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA24FEFD2B95C32200CD9E04 /* Decimal+DoubleValue.swift */; }; FA256984274CE5A500875A53 /* BalanceLockType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA256982274CE5A400875A53 /* BalanceLockType.swift */; }; FA256985274CE5A500875A53 /* BalanceLocks+Sort.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA256983274CE5A500875A53 /* BalanceLocks+Sort.swift */; }; @@ -2045,11 +2009,6 @@ FA256A45274CE8BD00875A53 /* StoriesCollectionItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA256A43274CE8BC00875A53 /* StoriesCollectionItem.swift */; }; FA256A46274CE8BD00875A53 /* StoriesCollectionItem.xib in Resources */ = {isa = PBXBuildFile; fileRef = FA256A44274CE8BD00875A53 /* StoriesCollectionItem.xib */; }; FA256A48274CE8C200875A53 /* StakingMainInteractor+Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA256A47274CE8C200875A53 /* StakingMainInteractor+Subscription.swift */; }; - FA256A4B274CE93800875A53 /* TransferConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA256A4A274CE93800875A53 /* TransferConstants.swift */; }; - FA256A4D274CEB9100875A53 /* DummySelectedAssetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA256A4C274CEB9000875A53 /* DummySelectedAssetView.swift */; }; - FA256A51274CEB9800875A53 /* FeePriceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA256A4E274CEB9600875A53 /* FeePriceViewModel.swift */; }; - FA256A52274CEB9800875A53 /* RichAmountDisplayViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA256A4F274CEB9700875A53 /* RichAmountDisplayViewModel.swift */; }; - FA256A53274CEB9800875A53 /* RichAmountInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA256A50274CEB9700875A53 /* RichAmountInputViewModel.swift */; }; FA286AF52A3043C3008BD527 /* ConvenienceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA286AF42A3043C3008BD527 /* ConvenienceError.swift */; }; FA286B0B2A3043DB008BD527 /* CrossChainConfirmationViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA286AF72A3043DB008BD527 /* CrossChainConfirmationViewLayout.swift */; }; FA286B0C2A3043DB008BD527 /* CrossChainConfirmationRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA286AF82A3043DB008BD527 /* CrossChainConfirmationRouter.swift */; }; @@ -2508,6 +2467,37 @@ FA8800682B31A335000AE5EB /* StakingAccountSubscriptionV13.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA8800672B31A335000AE5EB /* StakingAccountSubscriptionV13.swift */; }; FA88006A2B31A33E000AE5EB /* StakingAccountSubscriptionV14.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA8800692B31A33E000AE5EB /* StakingAccountSubscriptionV14.swift */; }; FA88006C2B31A46D000AE5EB /* StakingAccountSubscriptionAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA88006B2B31A46D000AE5EB /* StakingAccountSubscriptionAssembly.swift */; }; + FA8810982BDCAF260084CC4B /* IrohaCrypto in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810972BDCAF260084CC4B /* IrohaCrypto */; }; + FA88109A2BDCAF260084CC4B /* RobinHood in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810992BDCAF260084CC4B /* RobinHood */; }; + FA88109C2BDCAF260084CC4B /* SSFAccountManagment in Frameworks */ = {isa = PBXBuildFile; productRef = FA88109B2BDCAF260084CC4B /* SSFAccountManagment */; }; + FA88109E2BDCAF260084CC4B /* SSFAccountManagmentStorage in Frameworks */ = {isa = PBXBuildFile; productRef = FA88109D2BDCAF260084CC4B /* SSFAccountManagmentStorage */; }; + FA8810A02BDCAF260084CC4B /* SSFAssetManagment in Frameworks */ = {isa = PBXBuildFile; productRef = FA88109F2BDCAF260084CC4B /* SSFAssetManagment */; }; + FA8810A22BDCAF260084CC4B /* SSFChainConnection in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810A12BDCAF260084CC4B /* SSFChainConnection */; }; + FA8810A42BDCAF260084CC4B /* SSFChainRegistry in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810A32BDCAF260084CC4B /* SSFChainRegistry */; }; + FA8810A62BDCAF260084CC4B /* SSFCloudStorage in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810A52BDCAF260084CC4B /* SSFCloudStorage */; }; + FA8810A82BDCAF260084CC4B /* SSFCrypto in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810A72BDCAF260084CC4B /* SSFCrypto */; }; + FA8810AA2BDCAF260084CC4B /* SSFEraKit in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810A92BDCAF260084CC4B /* SSFEraKit */; }; + FA8810AC2BDCAF260084CC4B /* SSFExtrinsicKit in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810AB2BDCAF260084CC4B /* SSFExtrinsicKit */; }; + FA8810AE2BDCAF260084CC4B /* SSFHelpers in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810AD2BDCAF260084CC4B /* SSFHelpers */; }; + FA8810B02BDCAF260084CC4B /* SSFKeyPair in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810AF2BDCAF260084CC4B /* SSFKeyPair */; }; + FA8810B22BDCAF260084CC4B /* SSFLogger in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810B12BDCAF260084CC4B /* SSFLogger */; }; + FA8810B42BDCAF260084CC4B /* SSFModels in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810B32BDCAF260084CC4B /* SSFModels */; }; + FA8810B62BDCAF260084CC4B /* SSFNetwork in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810B52BDCAF260084CC4B /* SSFNetwork */; }; + FA8810B82BDCAF260084CC4B /* SSFPolkaswap in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810B72BDCAF260084CC4B /* SSFPolkaswap */; }; + FA8810BA2BDCAF260084CC4B /* SSFPools in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810B92BDCAF260084CC4B /* SSFPools */; }; + FA8810BC2BDCAF260084CC4B /* SSFPoolsStorage in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810BB2BDCAF260084CC4B /* SSFPoolsStorage */; }; + FA8810BE2BDCAF260084CC4B /* SSFQRService in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810BD2BDCAF260084CC4B /* SSFQRService */; }; + FA8810C02BDCAF260084CC4B /* SSFRuntimeCodingService in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810BF2BDCAF260084CC4B /* SSFRuntimeCodingService */; }; + FA8810C22BDCAF260084CC4B /* SSFSigner in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810C12BDCAF260084CC4B /* SSFSigner */; }; + FA8810C42BDCAF260084CC4B /* SSFSingleValueCache in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810C32BDCAF260084CC4B /* SSFSingleValueCache */; }; + FA8810C62BDCAF260084CC4B /* SSFStorageQueryKit in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810C52BDCAF260084CC4B /* SSFStorageQueryKit */; }; + FA8810C82BDCAF260084CC4B /* SSFTransferService in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810C72BDCAF260084CC4B /* SSFTransferService */; }; + FA8810CA2BDCAF260084CC4B /* SSFUtils in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810C92BDCAF260084CC4B /* SSFUtils */; }; + FA8810CC2BDCAF260084CC4B /* SSFXCM in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810CB2BDCAF260084CC4B /* SSFXCM */; }; + FA8810CE2BDCAF260084CC4B /* SoraKeystore in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810CD2BDCAF260084CC4B /* SoraKeystore */; }; + FA8810D02BDCAF260084CC4B /* keccak in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810CF2BDCAF260084CC4B /* keccak */; }; + FA8810D32BDCCF7E0084CC4B /* UserLiquidityPoolsListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA8810D22BDCCF7E0084CC4B /* UserLiquidityPoolsListPresenter.swift */; }; + FA8810D52BDCD19D0084CC4B /* UserLiquidityPoolsListViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA8810D42BDCD19D0084CC4B /* UserLiquidityPoolsListViewModelFactory.swift */; }; FA8ED43328FD960F00EBB712 /* YourValidatorListFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA8ED43228FD960F00EBB712 /* YourValidatorListFlow.swift */; }; FA8ED43628FD983A00EBB712 /* YourValidatorListRelaychainStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA8ED43528FD983A00EBB712 /* YourValidatorListRelaychainStrategy.swift */; }; FA8ED43828FD984500EBB712 /* YourValidatorListRelaychainViewModelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA8ED43728FD984500EBB712 /* YourValidatorListRelaychainViewModelState.swift */; }; @@ -2626,7 +2616,6 @@ FA9A8F262A72579D008FA99F /* AlchemySortOrder.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA9A8F242A72579D008FA99F /* AlchemySortOrder.swift */; }; FA9A8F272A72579D008FA99F /* AlchemyTokenCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA9A8F252A72579D008FA99F /* AlchemyTokenCategory.swift */; }; FA9A8F2B2A725B30008FA99F /* SubstrateAccountInfoFetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA9A8F2A2A725B30008FA99F /* SubstrateAccountInfoFetching.swift */; }; - FA9A8F3E2A776DFD008FA99F /* SubstrateDataModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FA9A8F382A776DFD008FA99F /* SubstrateDataModel.xcdatamodeld */; }; FA9A8F432A78C03D008FA99F /* EthereumTransferService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA9A8F422A78C03D008FA99F /* EthereumTransferService.swift */; }; FA9A8F452A78C045008FA99F /* SubstrateTransferService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA9A8F442A78C045008FA99F /* SubstrateTransferService.swift */; }; FA9A8F472A82005F008FA99F /* EthereumWalletRemoteSubscriptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA9A8F462A82005F008FA99F /* EthereumWalletRemoteSubscriptionService.swift */; }; @@ -2778,13 +2767,9 @@ FAB707622BB317D300A1131C /* CrossChainViewLoadingCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB707612BB317D300A1131C /* CrossChainViewLoadingCollector.swift */; }; FAB707652BB3C06900A1131C /* AssetsAccountRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB707642BB3C06900A1131C /* AssetsAccountRequest.swift */; }; FAB707672BB3C1A400A1131C /* AssetAccountInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB707662BB3C1A400A1131C /* AssetAccountInfo.swift */; }; - FAB707C52BB6792700A1131C /* TokenPairIconsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB707C42BB6792700A1131C /* TokenPairIconsView.swift */; }; - FAB707C72BB67F7000A1131C /* TokenPairsIconViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB707C62BB67F7000A1131C /* TokenPairsIconViewModel.swift */; }; - FAB707C92BB68C4000A1131C /* LiquidityPoolListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB707C82BB68C4000A1131C /* LiquidityPoolListCell.swift */; }; FAB8B96E29F23FCB002E5F04 /* ChainAccountBalanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB8B96D29F23FCA002E5F04 /* ChainAccountBalanceViewModel.swift */; }; FABA161B2B0C941B001AF2F0 /* MultiassetV11MigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FABA161A2B0C941B001AF2F0 /* MultiassetV11MigrationPolicy.swift */; }; FABA161D2B0C94CA001AF2F0 /* UserDataModelV10toV11.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = FABA161C2B0C94C9001AF2F0 /* UserDataModelV10toV11.xcmappingmodel */; }; - FABA162B2B0C94DB001AF2F0 /* UserDataModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FABA161E2B0C94DB001AF2F0 /* UserDataModel.xcdatamodeld */; }; FABA162D2B0C9504001AF2F0 /* TabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = FABA162C2B0C9504001AF2F0 /* TabBar.swift */; }; FABA163B2B0C9510001AF2F0 /* NetworkManagmentPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FABA162F2B0C9510001AF2F0 /* NetworkManagmentPresenter.swift */; }; FABA163C2B0C9510001AF2F0 /* NetworkManagmentTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FABA16302B0C9510001AF2F0 /* NetworkManagmentTableCell.swift */; }; @@ -2824,6 +2809,27 @@ FAC0BBE4291D0EB000E6F106 /* SelectAssetProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC0BBCE291D0EB000E6F106 /* SelectAssetProtocols.swift */; }; FAC0BBE6291D11D600E6F106 /* SelectableAmountInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC0BBE5291D11D600E6F106 /* SelectableAmountInputView.swift */; }; FAC2175E293F41B600A8BA83 /* AllDonePresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC2175D293F41B600A8BA83 /* AllDonePresentable.swift */; }; + FAC6CD862BA7F9990013A17E /* Pagination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6CD852BA7F9990013A17E /* Pagination.swift */; }; + FAC6CD882BA7F9F70013A17E /* FeeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6CD872BA7F9F70013A17E /* FeeViewModel.swift */; }; + FAC6CD8A2BA7FA4B0013A17E /* AssetTransactionData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6CD892BA7FA4B0013A17E /* AssetTransactionData.swift */; }; + FAC6CD8C2BA7FA6C0013A17E /* AmountDecimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6CD8B2BA7FA6C0013A17E /* AmountDecimal.swift */; }; + FAC6CD8E2BA7FBD30013A17E /* WalletHistoryRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6CD8D2BA7FBD30013A17E /* WalletHistoryRequest.swift */; }; + FAC6CD902BA7FCA70013A17E /* AssetTransactionFee.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6CD8F2BA7FCA70013A17E /* AssetTransactionFee.swift */; }; + FAC6CD922BA8022D0013A17E /* WalletAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6CD912BA8022D0013A17E /* WalletAsset.swift */; }; + FAC6CD942BA802840013A17E /* NumberFormatterFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6CD932BA802840013A17E /* NumberFormatterFactoryProtocol.swift */; }; + FAC6CD962BA807B80013A17E /* AccessoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6CD952BA807B80013A17E /* AccessoryViewModel.swift */; }; + FAC6CD982BA807D30013A17E /* AccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6CD972BA807D30013A17E /* AccessoryView.swift */; }; + FAC6CD9A2BA809160013A17E /* ReceiveInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6CD992BA809160013A17E /* ReceiveInfo.swift */; }; + FAC6CD9D2BA8097C0013A17E /* L10n.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6CD9C2BA8097C0013A17E /* L10n.swift */; }; + FAC6CD9F2BA80AB70013A17E /* WalletLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6CD9E2BA80AB70013A17E /* WalletLanguage.swift */; }; + FAC6CDA12BA80CB10013A17E /* WalletTransactionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6CDA02BA80CB10013A17E /* WalletTransactionType.swift */; }; + FAC6CDA42BA80D6A0013A17E /* WalletErrorContentProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6CDA32BA80D6A0013A17E /* WalletErrorContentProtocol.swift */; }; + FAC6CDA72BA814020013A17E /* BalanceContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6CDA62BA814020013A17E /* BalanceContext.swift */; }; + FAC6CDA92BA814F20013A17E /* UIControl+Disable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6CDA82BA814F20013A17E /* UIControl+Disable.swift */; }; + FAC6CDAB2BA819540013A17E /* SearchData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6CDAA2BA819540013A17E /* SearchData.swift */; }; + FAC6CDAD2BA81D680013A17E /* FeeViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6CDAC2BA81D680013A17E /* FeeViewProtocol.swift */; }; + FAC6CDAF2BA81FA00013A17E /* WalletLoggerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6CDAE2BA81FA00013A17E /* WalletLoggerProtocol.swift */; }; + FAC6CDB12BA821B00013A17E /* WalletImageViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6CDB02BA821B00013A17E /* WalletImageViewModelProtocol.swift */; }; FACACE1127BCF105005422EE /* MetaAccountCreationMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACACE1027BCF104005422EE /* MetaAccountCreationMetadata.swift */; }; FACACE1427BCF10F005422EE /* MetaAccountImportMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACACE1227BCF10E005422EE /* MetaAccountImportMetadata.swift */; }; FACACE1527BCF10F005422EE /* MetaAccountImportPreferredInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACACE1327BCF10E005422EE /* MetaAccountImportPreferredInfo.swift */; }; @@ -2858,8 +2864,7 @@ FACD42BD2A5BE91E009975AA /* PortionRewardCalculatorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACD42BB2A5BE91D009975AA /* PortionRewardCalculatorService.swift */; }; FACD42BF2A5BE93D009975AA /* polkadot-9370metadata in Resources */ = {isa = PBXBuildFile; fileRef = FACD42BE2A5BE93D009975AA /* polkadot-9370metadata */; }; FACD42C12A5C10BB009975AA /* TransferService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACD42C02A5C10BB009975AA /* TransferService.swift */; }; - FACE6C232BB6C37900643CEF /* LiquidityPoolListCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACE6C222BB6C37900643CEF /* LiquidityPoolListCellModel.swift */; }; - FACE6C652BBBBC8C00643CEF /* LiquidityPoolListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACE6C642BBBBC8C00643CEF /* LiquidityPoolListViewModel.swift */; }; + FACE6C632BBAC3B100643CEF /* SubstrateDataModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FACE6C5D2BBAC3B000643CEF /* SubstrateDataModel.xcdatamodeld */; }; FAD0068027EA252400C97E09 /* AboutViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD0067E27EA252400C97E09 /* AboutViewModelFactory.swift */; }; FAD0068127EA252400C97E09 /* AboutViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD0067F27EA252400C97E09 /* AboutViewState.swift */; }; FAD0068427EA255900C97E09 /* AboutTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD0068227EA255900C97E09 /* AboutTableViewCell.swift */; }; @@ -3315,7 +3320,6 @@ 09373C708FE543066B943E45 /* NftDetailsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftDetailsInteractor.swift; sourceTree = ""; }; 095DEF513136A26593FB421F /* AnalyticsRewardDetailsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AnalyticsRewardDetailsViewController.swift; sourceTree = ""; }; 0960D0A04ACDF443B5C5E185 /* WalletsManagmentViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletsManagmentViewLayout.swift; sourceTree = ""; }; - 0AB10C8DA56E92C280D66BE8 /* WalletHistoryFilterViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletHistoryFilterViewFactory.swift; sourceTree = ""; }; 0B5072E250B7277F605855B3 /* AccountManagementProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountManagementProtocols.swift; sourceTree = ""; }; 0B556386CEEB76C95ED59897 /* ControllerAccountViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ControllerAccountViewController.swift; sourceTree = ""; }; 0BCBF6D6D48704B27B2DBB79 /* MainNftContainerViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainNftContainerViewLayout.swift; sourceTree = ""; }; @@ -3326,7 +3330,6 @@ 0E07F60661001B2C505D9C8B /* SelectMarketPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectMarketPresenter.swift; sourceTree = ""; }; 0F48467D97F9D06F83F70894 /* Pods-fearlessAll-fearless.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-fearlessAll-fearless.dev.xcconfig"; path = "Target Support Files/Pods-fearlessAll-fearless/Pods-fearlessAll-fearless.dev.xcconfig"; sourceTree = ""; }; 0F67BB672040911E4A165446 /* PolkaswapSwapConfirmationViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PolkaswapSwapConfirmationViewLayout.swift; sourceTree = ""; }; - 101F1829F8CB980B39A7F14A /* LiquidityPoolsListViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsListViewController.swift; sourceTree = ""; }; 1211B723BB656770C4EA5BC2 /* WalletTransactionDetailsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletTransactionDetailsViewFactory.swift; sourceTree = ""; }; 12441689D2AF47D508D16CCF /* WalletSendConfirmViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletSendConfirmViewFactory.swift; sourceTree = ""; }; 1366336078BCA34EFB4C6FF9 /* CrowdloanContributionConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionConfirmInteractor.swift; sourceTree = ""; }; @@ -3363,7 +3366,6 @@ 2377F8FB07B47637346249F5 /* StakingRewardDetailsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRewardDetailsViewFactory.swift; sourceTree = ""; }; 23A74BDB54D503FA2BFBEF35 /* StakingUnbondSetupProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondSetupProtocols.swift; sourceTree = ""; }; 23BC71941B91D3E372CDB11C /* CrowdloanContributionSetupViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionSetupViewLayout.swift; sourceTree = ""; }; - 23BF570839C35B8FA1F7CAF6 /* LiquidityPoolsListTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsListTests.swift; sourceTree = ""; }; 23CF7E56EC624BDB60290387 /* CreateContactPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateContactPresenter.swift; sourceTree = ""; }; 25B80FDDB5C3032A0BBBD826 /* NftCollectionPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftCollectionPresenter.swift; sourceTree = ""; }; 25FF82C2FD912021A1F20876 /* PolkaswapAdjustmentViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PolkaswapAdjustmentViewLayout.swift; sourceTree = ""; }; @@ -3373,6 +3375,7 @@ 270B309EC85D8897A4ADD98A /* CustomValidatorListViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CustomValidatorListViewController.swift; sourceTree = ""; }; 27D5AF2F7609ADE855308089 /* AccountExportPasswordViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountExportPasswordViewController.swift; sourceTree = ""; }; 28CCDB1822156D69578A3042 /* StakingPoolInfoPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPoolInfoPresenter.swift; sourceTree = ""; }; + 28E3B4BA1160B87C435C2AAF /* LiquidityPoolDetailsRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolDetailsRouter.swift; sourceTree = ""; }; 28EF0F9F97C89137F642016E /* SelectValidatorsConfirmTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectValidatorsConfirmTests.swift; sourceTree = ""; }; 28F5B57A24265C36A5F19B78 /* CrowdloanContributionConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionConfirmPresenter.swift; sourceTree = ""; }; 29100320799A2B46836A257B /* AnalyticsRewardDetailsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AnalyticsRewardDetailsViewFactory.swift; sourceTree = ""; }; @@ -3383,13 +3386,13 @@ 2A84E87725D425750006FE9C /* AlertControllerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertControllerFactory.swift; sourceTree = ""; }; 2AB7A7FE25CD0E7F00767D87 /* GitHubPhishingAPIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubPhishingAPIService.swift; sourceTree = ""; }; 2ACA4A5B186EE6D40BFE9D66 /* ExportMnemonicWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExportMnemonicWireframe.swift; sourceTree = ""; }; - 2AD0A16925D3854700312428 /* TransferConfirmCommandProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferConfirmCommandProxy.swift; sourceTree = ""; }; 2AD0A18F25D3D1E100312428 /* GitHubPhishingServiceFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubPhishingServiceFactory.swift; sourceTree = ""; }; 2AD0A19425D3D3EC00312428 /* GitHubOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubOperationFactory.swift; sourceTree = ""; }; 2AEC13E6950006302AC51B96 /* WalletTransactionHistoryProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletTransactionHistoryProtocols.swift; sourceTree = ""; }; 2B55A4C7E8BD87A7C8634ADD /* WalletsManagmentRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletsManagmentRouter.swift; sourceTree = ""; }; 2BE0492B98AB9C1540846B39 /* StakingPayoutConfirmationViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPayoutConfirmationViewFactory.swift; sourceTree = ""; }; 2C542733CEFB871FCD23195E /* NftSendConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftSendConfirmInteractor.swift; sourceTree = ""; }; + 2CF682B92176E0FED5D7B4DB /* LiquidityPoolDetailsTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolDetailsTests.swift; sourceTree = ""; }; 2D9C58C9D53A7EA34E5CB00C /* NftSendConfirmAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftSendConfirmAssembly.swift; sourceTree = ""; }; 2E4B0600AFFB96A75CF98755 /* StakingRedeemProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRedeemProtocols.swift; sourceTree = ""; }; 2E57C70327E8AB3D00AF075A /* CrowdloanWikiTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloanWikiTableViewCell.swift; sourceTree = ""; }; @@ -3423,10 +3426,12 @@ 3B0E2EDF787BF82F16663215 /* NetworkInfoViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NetworkInfoViewController.swift; sourceTree = ""; }; 3B6244A9538B39AFCD3A6F3A /* SelectValidatorsStartPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectValidatorsStartPresenter.swift; sourceTree = ""; }; 3B8473AA386E1AD6F0F0C964 /* ControllerAccountConfirmationInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ControllerAccountConfirmationInteractor.swift; sourceTree = ""; }; + 3BA8DC8007FC0A322C6DF00E /* LiquidityPoolsOverviewPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsOverviewPresenter.swift; sourceTree = ""; }; 3D2C2FC3E31C03D08BDEC7A1 /* PolkaswapAdjustmentRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PolkaswapAdjustmentRouter.swift; sourceTree = ""; }; 3E992CCDC1D581F7E9D3F1CA /* AccountConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountConfirmInteractor.swift; sourceTree = ""; }; 3EDA19BC3280F1838C687EC8 /* NetworkInfoViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NetworkInfoViewFactory.swift; sourceTree = ""; }; 3F1D5849A2EBF462B32F3A9C /* ExportSeedProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExportSeedProtocols.swift; sourceTree = ""; }; + 3F25F3A88B4BEB4DE498220C /* LiquidityPoolsOverviewRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsOverviewRouter.swift; sourceTree = ""; }; 3F47E5832513985B89D06155 /* AccountExportPasswordWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountExportPasswordWireframe.swift; sourceTree = ""; }; 3F5C47F8D6CB97D16DFF06B7 /* WalletsManagmentInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletsManagmentInteractor.swift; sourceTree = ""; }; 3F7068913923A6DEEE9D8EA0 /* CrowdloanContributionSetupPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionSetupPresenter.swift; sourceTree = ""; }; @@ -3437,7 +3442,6 @@ 40B10454C90325D80CCBEC60 /* StakingUnbondConfirmFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondConfirmFlow.swift; sourceTree = ""; wrapsLines = 1; }; 40B47961B2254E8A4D8EC588 /* StakingAmountPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingAmountPresenter.swift; sourceTree = ""; }; 4191E0055768541F6A3D8A61 /* StakingRewardPayoutsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRewardPayoutsInteractor.swift; sourceTree = ""; }; - 41DFB2757D029FB5DF3CEBC2 /* WalletHistoryFilterProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletHistoryFilterProtocols.swift; sourceTree = ""; }; 41E19988955B1C159EDA2555 /* WalletsManagmentPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletsManagmentPresenter.swift; sourceTree = ""; }; 446B4A7184327D64AE8F0610 /* WarningAlertProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WarningAlertProtocols.swift; sourceTree = ""; }; 447D03660EFFD8CF46374D85 /* NftSendConfirmViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftSendConfirmViewController.swift; sourceTree = ""; }; @@ -3499,6 +3503,7 @@ 5D81DBDDD34EA20C3270EDB4 /* AddCustomNodeViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddCustomNodeViewFactory.swift; sourceTree = ""; }; 5E096A576B747C09B14FD38D /* WalletMainContainerAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletMainContainerAssembly.swift; sourceTree = ""; }; 5E11C7AF9A8DEC07246D5626 /* StakingMainTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingMainTests.swift; sourceTree = ""; }; + 5F6F7AE5AFF3F2E7BADA02BB /* LiquidityPoolDetailsAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolDetailsAssembly.swift; sourceTree = ""; }; 5F791FE1B479CE1DF936F79F /* CrowdloanContributionConfirmViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionConfirmViewFactory.swift; sourceTree = ""; }; 5FC428EB695A80397BDC621C /* PolkaswapAdjustmentAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PolkaswapAdjustmentAssembly.swift; sourceTree = ""; }; 5FCECA20E6DCA5D228F44477 /* StakingUnbondSetupPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondSetupPresenter.swift; sourceTree = ""; }; @@ -3517,11 +3522,11 @@ 63CCCC63389A70294F816143 /* NodeSelectionProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NodeSelectionProtocols.swift; sourceTree = ""; }; 63F4BE52D0625CD8C21D2460 /* CrowdloanListViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanListViewLayout.swift; sourceTree = ""; }; 65AD15693E21C869DE1FDD17 /* UsernameSetupWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UsernameSetupWireframe.swift; sourceTree = ""; }; - 65D8653B07D92DFE321CEF1E /* LiquidityPoolsListPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsListPresenter.swift; sourceTree = ""; }; - 6627C06C19A177744E32BA35 /* LiquidityPoolsListAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsListAssembly.swift; sourceTree = ""; }; 66FFB904E5A83F2EFBCCBBF8 /* StakingPoolInfoTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPoolInfoTests.swift; sourceTree = ""; }; + 6717FF1B7777400B62F028C3 /* LiquidityPoolsOverviewInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsOverviewInteractor.swift; sourceTree = ""; }; 67B1037AEC97DBEAF9FD50C1 /* AssetNetworksPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetNetworksPresenter.swift; sourceTree = ""; }; 67B3E1906EEBE32E71E82BB6 /* NodeSelectionViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NodeSelectionViewLayout.swift; sourceTree = ""; }; + 67BDE520860A67C800E7F4AB /* LiquidityPoolDetailsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolDetailsViewController.swift; sourceTree = ""; }; 6897929D244B5C29E3FD0727 /* StakingPoolCreateRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPoolCreateRouter.swift; sourceTree = ""; }; 69B00AC48FB7D11855875EB9 /* SwapTransactionDetailPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapTransactionDetailPresenter.swift; sourceTree = ""; }; 6A28799A82C0EC2F8ABDE831 /* Pods_fearlessAll_fearless.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_fearlessAll_fearless.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -3538,7 +3543,6 @@ 6DE4840EBB9892A5E35FB443 /* AccountExportPasswordViewController.xib */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file.xib; path = AccountExportPasswordViewController.xib; sourceTree = ""; }; 6EDB8BAE1FAE3C7502E9245E /* NftDetailsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftDetailsViewLayout.swift; sourceTree = ""; }; 6FA0310E7E7EA9A985602CCA /* ChainAccountListTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChainAccountListTests.swift; sourceTree = ""; }; - 7045FDDE48C27A858389B302 /* LiquidityPoolsListInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsListInteractor.swift; sourceTree = ""; }; 71285CF636B32ACD8EB5519E /* ReferralCrowdloanViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferralCrowdloanViewFactory.swift; sourceTree = ""; }; 7306D50F278F6CC90DC88F27 /* AccountConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountConfirmPresenter.swift; sourceTree = ""; }; 73078ED3B2642FEAF348DB2A /* WalletOptionProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletOptionProtocols.swift; sourceTree = ""; }; @@ -3551,7 +3555,6 @@ 779702BC0E9C9882BEA5C273 /* StakingPoolCreateConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPoolCreateConfirmInteractor.swift; sourceTree = ""; }; 782CC21A2F9EEF5DBA3AB1AA /* PurchaseProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PurchaseProtocols.swift; sourceTree = ""; }; 784D20E16EEE55C2CF7B319B /* StakingBondMoreFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingBondMoreFlow.swift; sourceTree = ""; }; - 78670B0926E92B75088D2D7B /* WalletHistoryFilterWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletHistoryFilterWireframe.swift; sourceTree = ""; }; 7911693957DFAF141EBDAFEC /* StakingRewardPayoutsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRewardPayoutsProtocols.swift; sourceTree = ""; }; 7931155840DACB340284ABBB /* PolkaswapTransaktionSettingsAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PolkaswapTransaktionSettingsAssembly.swift; sourceTree = ""; }; 7BCAF4A12D0F22D3C9035A1A /* WarningAlertPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WarningAlertPresenter.swift; sourceTree = ""; }; @@ -3567,6 +3570,7 @@ 80809FE46E7B8EBDE3680706 /* NodeSelectionWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NodeSelectionWireframe.swift; sourceTree = ""; }; 80C632A122CE8D02B84806C3 /* WalletMainContainerRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletMainContainerRouter.swift; sourceTree = ""; }; 80DE21374A7FA5AE4D26CEB2 /* ChainAccountBalanceListTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChainAccountBalanceListTests.swift; sourceTree = ""; }; + 81E9DD6EB14A352635BAC711 /* LiquidityPoolDetailsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolDetailsPresenter.swift; sourceTree = ""; }; 82072889A33037BFC9D8F574 /* NftDetailsTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftDetailsTests.swift; sourceTree = ""; }; 82CBCBA8BF2D753248238555 /* ContactsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ContactsViewController.swift; sourceTree = ""; }; 82FD4E0BB1ABA364AFD2E891 /* WalletTransactionDetailsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletTransactionDetailsPresenter.swift; sourceTree = ""; }; @@ -3598,13 +3602,6 @@ 8406B5AC26FBE69200635B61 /* SelectableIconDetailsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableIconDetailsListViewModel.swift; sourceTree = ""; }; 8406B5AE26FBE7EF00635B61 /* SelectionIconDetailsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionIconDetailsTableViewCell.swift; sourceTree = ""; }; 840882AF2514024800177E20 /* SelectedConnectionChanged.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedConnectionChanged.swift; sourceTree = ""; }; - 8409C263252210A70049B5C8 /* WalletDisplayAmountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletDisplayAmountView.swift; sourceTree = ""; }; - 8409C2652522110F0049B5C8 /* WalletAmountView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = WalletAmountView.xib; sourceTree = ""; }; - 8409C267252218FF0049B5C8 /* WalletFearlessDefinitionFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletFearlessDefinitionFactory.swift; sourceTree = ""; }; - 8409C2692522192B0049B5C8 /* WalletFearlessDefinition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletFearlessDefinition.swift; sourceTree = ""; }; - 8409C26F25227C1E0049B5C8 /* WalletSingleActionAccessoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletSingleActionAccessoryView.swift; sourceTree = ""; }; - 8409C27125227CA00049B5C8 /* WalletSingleActionAccessoryView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = WalletSingleActionAccessoryView.xib; sourceTree = ""; }; - 8409C27325227DC60049B5C8 /* WalletSingleActionAccessoryFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletSingleActionAccessoryFactory.swift; sourceTree = ""; }; 840BF22426C2653100E3A955 /* ChainSyncServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainSyncServiceTests.swift; sourceTree = ""; }; 840BF22626C2C8A600E3A955 /* ChainSyncEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainSyncEvents.swift; sourceTree = ""; }; 840C71B726821C0600B6D9C2 /* StakingRebondMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRebondMock.swift; sourceTree = ""; }; @@ -3655,7 +3652,6 @@ 8425EA9425EA82CE00C307C9 /* AccountIdentity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountIdentity.swift; sourceTree = ""; }; 8425EA9925EA83FA00C307C9 /* ChainData+Value.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChainData+Value.swift"; sourceTree = ""; }; 8425EB1A25EADD8600C307C9 /* StakingConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingConstants.swift; sourceTree = ""; }; - 84265E032523D20A005EEE2D /* WalletBaseAmountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletBaseAmountView.swift; sourceTree = ""; }; 84282FBC26D05A54002CA322 /* ChainRegistryFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainRegistryFacade.swift; sourceTree = ""; }; 8428764324ADDE0200D91AD8 /* ProfilePresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfilePresenter.swift; sourceTree = ""; }; 8428764724ADDE0200D91AD8 /* ProfileViewModelFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileViewModelFactory.swift; sourceTree = ""; }; @@ -3734,7 +3730,6 @@ 8434C9E92540AE51009E4191 /* ExtrinsicEraTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtrinsicEraTests.swift; sourceTree = ""; }; 8436E94326C853E4003D4EA7 /* RuntimeSnapshotOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeSnapshotOperationFactory.swift; sourceTree = ""; }; 8436E94526C85405003D4EA7 /* RuntimeSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeSnapshot.swift; sourceTree = ""; }; - 8436EDDB25893FA0004D9E97 /* WalletBuyCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletBuyCommand.swift; sourceTree = ""; }; 8436EDE125895804004D9E97 /* RampProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RampProvider.swift; sourceTree = ""; }; 8436EDE625895846004D9E97 /* PurchaseProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseProvider.swift; sourceTree = ""; }; 8436EDEE25896722004D9E97 /* PurchaseAggregator+Default.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PurchaseAggregator+Default.swift"; sourceTree = ""; }; @@ -3836,7 +3831,6 @@ 84563D0824F46B7F0055591D /* ManagedAccountItemMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedAccountItemMapperTests.swift; sourceTree = ""; }; 84585A2E251BFC8400390F7A /* TriangularedButton+Style.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TriangularedButton+Style.swift"; sourceTree = ""; }; 84585A30251C0B9300390F7A /* NetworkInfoMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkInfoMode.swift; sourceTree = ""; }; - 84585A33251CE2E300390F7A /* ContactsLocalSearchEngine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsLocalSearchEngine.swift; sourceTree = ""; }; 845B821426EF657700D25C72 /* PersistentValueSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentValueSettings.swift; sourceTree = ""; }; 845B821626EF7FED00D25C72 /* SelectedWalletSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedWalletSettings.swift; sourceTree = ""; }; 845B821826EF808D00D25C72 /* MetaAccountMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaAccountMapper.swift; sourceTree = ""; }; @@ -3866,12 +3860,8 @@ 846042BF2666E2C800CFFCFC /* KaruraResultData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KaruraResultData.swift; sourceTree = ""; }; 8460516C25536C4800A1F0B4 /* ExportOption+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ExportOption+ViewModel.swift"; sourceTree = ""; }; 8460517125536E3F00A1F0B4 /* ExportGenericViewModelBinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportGenericViewModelBinder.swift; sourceTree = ""; }; - 8460E1152541E03400826F55 /* WalletFormCellStyle+Internal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WalletFormCellStyle+Internal.swift"; sourceTree = ""; }; 8460E11C2542011200826F55 /* DetailsTriangularedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailsTriangularedView.swift; sourceTree = ""; }; 8460E11E25420A2C00826F55 /* DetailsTriangularedView+Inspectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DetailsTriangularedView+Inspectable.swift"; sourceTree = ""; }; - 8460E1242542293A00826F55 /* WalletCompoundDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletCompoundDetailsView.swift; sourceTree = ""; }; - 8460E12625422A6D00826F55 /* WalletCompoundDetailsView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = WalletCompoundDetailsView.xib; sourceTree = ""; }; - 8460E12825422C5A00826F55 /* WalletCompoundDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletCompoundDetailsViewModel.swift; sourceTree = ""; }; 8461CC7E26BBFEEA007460E4 /* ExtrinsicEraOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtrinsicEraOperationFactory.swift; sourceTree = ""; }; 8461CC8026BBFF70007460E4 /* ImmortalEraOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImmortalEraOperationFactory.swift; sourceTree = ""; }; 8461CC8226BC0006007460E4 /* MortalEraOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MortalEraOperationFactory.swift; sourceTree = ""; }; @@ -3892,7 +3882,6 @@ 84644A2A2567222A004EAA4B /* TriangularedBlurButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TriangularedBlurButton.swift; sourceTree = ""; }; 84644A2F256722D2004EAA4B /* TriangularedBlurButton+Inspectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TriangularedBlurButton+Inspectable.swift"; sourceTree = ""; }; 84644AC42567EC05004EAA4B /* MultilineTriangularedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineTriangularedView.swift; sourceTree = ""; }; - 84644B3225685915004EAA4B /* WalletHistoryBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletHistoryBackgroundView.swift; sourceTree = ""; }; 8466BB41263F501000E065A8 /* StakingUnbondConfirmViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingUnbondConfirmViewModel.swift; sourceTree = ""; }; 8467F4C226B1DFA500C5B6F4 /* StakingDurationOperationFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingDurationOperationFactoryTests.swift; sourceTree = ""; }; 8467F4C426B1E4E200C5B6F4 /* StakingDurationFetching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingDurationFetching.swift; sourceTree = ""; }; @@ -3936,16 +3925,11 @@ 846A2605267C792000429A7F /* CrowdloanContributionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionResponse.swift; sourceTree = ""; }; 846A2C4225271CDE00731018 /* TransactionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionType.swift; sourceTree = ""; }; 846A2C4425271F0100731018 /* DateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatter.swift; sourceTree = ""; }; - 846A2C482527490700731018 /* WalletSelectAccountCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletSelectAccountCommand.swift; sourceTree = ""; }; 846A2C4A2529F99400731018 /* AccountRepositoryFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountRepositoryFactory.swift; sourceTree = ""; }; 846A2C4C2529FBB700731018 /* NSPredicate+Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPredicate+Filter.swift"; sourceTree = ""; }; - 846A2C4F252A063800731018 /* WalletSelectAccountCommandFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletSelectAccountCommandFactory.swift; sourceTree = ""; }; 846A2C53252A0FDF00731018 /* FilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterTests.swift; sourceTree = ""; }; 846AC7EE2638D9200075F7DA /* YourValidatorTableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YourValidatorTableCell.swift; sourceTree = ""; }; 846AF8432525BE0100868F37 /* Price.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Price.swift; sourceTree = ""; }; - 846AF8452525C93A00868F37 /* BalanceContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceContext.swift; sourceTree = ""; }; - 846AF8472525E23C00868F37 /* WalletTotalPriceCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletTotalPriceCell.swift; sourceTree = ""; }; - 846AF8492525E24C00868F37 /* WalletTotalPriceCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = WalletTotalPriceCell.xib; sourceTree = ""; }; 846B32A026DCDB7300250E89 /* SubqueryRewardSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubqueryRewardSource.swift; sourceTree = ""; }; 846B32A426DD07AF00250E89 /* rewardErrorResponse.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = rewardErrorResponse.json; sourceTree = ""; }; 846C372D26B199D10098F303 /* StakingDurationOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingDurationOperationFactory.swift; sourceTree = ""; }; @@ -3999,7 +3983,6 @@ 84754C9F2513D83E00854599 /* AccountPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountPickerViewModel.swift; sourceTree = ""; }; 84754CA12513DB8800854599 /* EmptyAccountIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyAccountIcon.swift; sourceTree = ""; }; 84754CA32513E6DC00854599 /* PrimitiveContextWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimitiveContextWrapper.swift; sourceTree = ""; }; - 8475AE1E258B7F4A00B058F3 /* TransferMetadataContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferMetadataContext.swift; sourceTree = ""; }; 8475AE38258B958300B058F3 /* ErrorContent+Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ErrorContent+Wallet.swift"; sourceTree = ""; }; 8476C9DF25EF94F2003EEAE1 /* StakingViewModelFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingViewModelFacade.swift; sourceTree = ""; }; 8476CA0525EFB072003EEAE1 /* SingleValueProviderFactoryStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleValueProviderFactoryStub.swift; sourceTree = ""; }; @@ -4127,16 +4110,11 @@ 849014D224AA8F60008F705E /* MainTabBarWireframe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainTabBarWireframe.swift; sourceTree = ""; }; 849014E724AA925C008F705E /* ScrollsToTop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollsToTop.swift; sourceTree = ""; }; 849014FB24AA9939008F705E /* Charset+Mnemonic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Charset+Mnemonic.swift"; sourceTree = ""; }; - 849014FE24AB69A4008F705E /* WalletContextFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletContextFactory.swift; sourceTree = ""; }; 8490150824AB8A3A008F705E /* WalletEmptyStateDataSource+Module.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "WalletEmptyStateDataSource+Module.swift"; sourceTree = ""; }; 8490150E24AB8A3A008F705E /* WalletEmptyStateDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletEmptyStateDataSource.swift; sourceTree = ""; }; - 8490151724AB8C6D008F705E /* WalletCommonStyleConfigurator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletCommonStyleConfigurator.swift; sourceTree = ""; }; - 8490151C24ABC57A008F705E /* AssetStyleFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetStyleFactory.swift; sourceTree = ""; }; 8490152024ABC721008F705E /* WalletStaticImageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletStaticImageViewModel.swift; sourceTree = ""; }; - 8490152224ABC77E008F705E /* WalletAssetViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletAssetViewModel.swift; sourceTree = ""; }; 8490152424ABCBF3008F705E /* AmountFormatterFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmountFormatterFactory.swift; sourceTree = ""; }; 8490152624ABCC40008F705E /* NumberFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberFormatter.swift; sourceTree = ""; }; - 8490152824ABCFDA008F705E /* WalletCommandDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletCommandDecorator.swift; sourceTree = ""; }; 8490152D24ABD0F5008F705E /* Logger+Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+Wallet.swift"; sourceTree = ""; }; 8490386A262E22DC0016D541 /* NominatorPayoutInfoFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominatorPayoutInfoFactory.swift; sourceTree = ""; }; 849244912514EDE800477C1B /* SelectableViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableViewModel.swift; sourceTree = ""; }; @@ -4148,11 +4126,6 @@ 8493D3E827059B6700157009 /* StakingServiceFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingServiceFactory.swift; sourceTree = ""; }; 84944249265306BD0016E7BD /* ChangeRewardDestinationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeRewardDestinationViewModel.swift; sourceTree = ""; }; 8494425E265330650016E7BD /* StakingRewardDestinationSetupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardDestinationSetupTests.swift; sourceTree = ""; }; - 8494D8542524633300614D8F /* WalletTransferTokenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletTransferTokenView.swift; sourceTree = ""; }; - 8494D856252465AC00614D8F /* WalletDisplayTokenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletDisplayTokenView.swift; sourceTree = ""; }; - 8494D85D25246E9A00614D8F /* WalletTokenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletTokenViewModel.swift; sourceTree = ""; }; - 8494D85F252470F400614D8F /* WalletFearlessFormDefining.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletFearlessFormDefining.swift; sourceTree = ""; }; - 8494D8622524753400614D8F /* WalletCopyCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletCopyCommand.swift; sourceTree = ""; }; 8494D86A25247F9600614D8F /* Decimal+String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Decimal+String.swift"; sourceTree = ""; }; 8494D86F2525321700614D8F /* SubscanDefinitions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscanDefinitions.swift; sourceTree = ""; }; 8494D8772525343500614D8F /* SubscanOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscanOperationFactory.swift; sourceTree = ""; }; @@ -4165,7 +4138,6 @@ 84963D6D26F91826003FE8E4 /* RemoteSubscriptionRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSubscriptionRequests.swift; sourceTree = ""; }; 84963D6F26F92F18003FE8E4 /* WalletRemoteSubscriptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletRemoteSubscriptionService.swift; sourceTree = ""; }; 84969731251CE71500C39524 /* WalletAssetId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletAssetId.swift; sourceTree = ""; }; - 84969733251CE9CD00C39524 /* ContactsConfigurator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsConfigurator.swift; sourceTree = ""; }; 8497FC5F26317783002FEAA7 /* AccountInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountInfoViewModel.swift; sourceTree = ""; }; 84981CC22666D95F00C4C691 /* GradientButton+Style.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GradientButton+Style.swift"; sourceTree = ""; }; 849842E526587573006BBB9F /* MultilineTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineTableViewCell.swift; sourceTree = ""; }; @@ -4193,26 +4165,20 @@ 849DF02C26C40B7900B702F4 /* RuntimeFilesOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeFilesOperationFactory.swift; sourceTree = ""; }; 849DF02E26C53DB900B702F4 /* RuntimeSyncEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeSyncEvents.swift; sourceTree = ""; }; 849E0CCF25CFDDB700B33506 /* StorageUpdateData+Decoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StorageUpdateData+Decoding.swift"; sourceTree = ""; }; - 849E2331254AEFB400B1F6D4 /* InvoiceScanConfigurator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoiceScanConfigurator.swift; sourceTree = ""; }; - 849E2350254AF44200B1F6D4 /* InvoiceScanLocalSearchEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoiceScanLocalSearchEngine.swift; sourceTree = ""; }; 849E689426AF388500E0E7BE /* ElectedValidatorInfo+Selected.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ElectedValidatorInfo+Selected.swift"; sourceTree = ""; }; 849ECD3426DE70B900F542A3 /* SingleToMultiassetUserMigrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleToMultiassetUserMigrationTests.swift; sourceTree = ""; }; 849ECD3826DF792800F542A3 /* SkeletonRow+View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SkeletonRow+View.swift"; sourceTree = ""; }; 84A15488262888CA0050D557 /* IdentityOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityOperationFactory.swift; sourceTree = ""; }; 84A259F72555C8C9001E91BC /* CryptoType+Keystore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CryptoType+Keystore.swift"; sourceTree = ""; }; - 84A2C90124E07E440020D3B7 /* CryptoType+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CryptoType+Utils.swift"; sourceTree = ""; }; 84A2C90324E07F400020D3B7 /* AccountOperationFactoryError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountOperationFactoryError.swift; sourceTree = ""; }; 84A2C90624E09DC20020D3B7 /* AccountOperationFactoryError+Presentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountOperationFactoryError+Presentable.swift"; sourceTree = ""; }; 84A2C90B24E192F50020D3B7 /* ShakeAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShakeAnimator.swift; sourceTree = ""; }; - 84A2CD5225685A4100A9FB0D /* WalletHistoryViewFactoryOverriding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletHistoryViewFactoryOverriding.swift; sourceTree = ""; }; 84A3034826A834F900E64382 /* ValidatorInfoViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidatorInfoViewLayout.swift; sourceTree = ""; }; 84A6171A2625AC3E007B75E1 /* CallCodingPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallCodingPath.swift; sourceTree = ""; }; 84A617252625AF51007B75E1 /* RuntimeMetadata+Internal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RuntimeMetadata+Internal.swift"; sourceTree = ""; }; 84A8FD8D265FDA76002ADB58 /* CrowdloanContributionConfirmData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionConfirmData.swift; sourceTree = ""; }; 84AA004226C5DFD800BCB4DC /* RuntimeSyncServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeSyncServiceTests.swift; sourceTree = ""; }; - 84ACEBF1261E664900AAE665 /* WalletHistoryFilterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletHistoryFilterViewModel.swift; sourceTree = ""; }; 84ACEBF9261E684A00AAE665 /* WalletHistoryFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletHistoryFilter.swift; sourceTree = ""; }; - 84ACEBFE261E6C7C00AAE665 /* WalletHistoryFilterViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletHistoryFilterViewLayout.swift; sourceTree = ""; }; 84B018AB26E01A4100C75E28 /* StakingStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingStateView.swift; sourceTree = ""; }; 84B018AD26E03FB500C75E28 /* NominatorStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominatorStateView.swift; sourceTree = ""; }; 84B018AF26E0450F00C75E28 /* ValidatorStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidatorStateView.swift; sourceTree = ""; }; @@ -4242,12 +4208,8 @@ 84BEE22225646ABF00D05EB3 /* SelectedUsernameChanged.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedUsernameChanged.swift; sourceTree = ""; }; 84BEE22C2564765F00D05EB3 /* UIAlertViewController+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertViewController+Account.swift"; sourceTree = ""; }; 84BEE62F26CBFDBC00757009 /* ChainRegistryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainRegistryTests.swift; sourceTree = ""; }; - 84BEF600258938C1002A22E2 /* WalletActionsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletActionsViewModel.swift; sourceTree = ""; }; 84C07EDD24AF1935008FC35B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 84C07EE524AF1990008FC35B /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; - 84C07EE724AF2E4D008FC35B /* WalletHistoryStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletHistoryStyle.swift; sourceTree = ""; }; - 84C07EE924AF303D008FC35B /* WalletActionsCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = WalletActionsCell.xib; sourceTree = ""; }; - 84C07EEB24AF30F2008FC35B /* WalletActionsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletActionsCell.swift; sourceTree = ""; }; 84C1B98324F5245700FE5470 /* ManagedAccountViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedAccountViewModel.swift; sourceTree = ""; }; 84C1B98524F5424700FE5470 /* ManagedAccountViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedAccountViewModelFactory.swift; sourceTree = ""; }; 84C2F27625E296CD0050A4AD /* RewardDestinationViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RewardDestinationViewModelFactory.swift; sourceTree = ""; }; @@ -4270,7 +4232,6 @@ 84C74364251E4D60009576C6 /* SigningWrapperProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SigningWrapperProtocol.swift; sourceTree = ""; }; 84C7DA5325EE2DF000F8C318 /* StakingErrorPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingErrorPresentable.swift; sourceTree = ""; }; 84C91FA9261E724F002796B9 /* SwitchTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchTableViewCell.swift; sourceTree = ""; }; - 84C91FAE261E7CDD002796B9 /* WalletHistoryFilterEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletHistoryFilterEditor.swift; sourceTree = ""; }; 84CA68CE26BD6872003B9453 /* RuntimeSyncService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeSyncService.swift; sourceTree = ""; }; 84CA68D026BE99ED003B9453 /* RuntimeProviderFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeProviderFactory.swift; sourceTree = ""; }; 84CA68D226BE9A35003B9453 /* RuntimeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeProvider.swift; sourceTree = ""; }; @@ -4284,10 +4245,6 @@ 84CB224F270360AC0041C8C1 /* RelaychainStakingLocalSubscriptionFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaychainStakingLocalSubscriptionFactory.swift; sourceTree = ""; }; 84CCBFBB2509709500180F4F /* UIBarButtonItem+Style.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem+Style.swift"; sourceTree = ""; }; 84CD356F252620FB0081BC0B /* CryptoType+Extrinsic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CryptoType+Extrinsic.swift"; sourceTree = ""; }; - 84CD357225264F640081BC0B /* WalletTotalPriceViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletTotalPriceViewModel.swift; sourceTree = ""; }; - 84CD357425264FA30081BC0B /* WalletAccountListConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletAccountListConstants.swift; sourceTree = ""; }; - 84CD3576252654EA0081BC0B /* WalletAssetCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletAssetCell.swift; sourceTree = ""; }; - 84CD35782526554A0081BC0B /* WalletAssetCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = WalletAssetCell.xib; sourceTree = ""; }; 84CD82AD263C1452001A6F01 /* SubstrateProviderSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubstrateProviderSubscriber.swift; sourceTree = ""; }; 84CD82B2263C30BC001A6F01 /* SubstrateProviderSubscriptionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubstrateProviderSubscriptionHandler.swift; sourceTree = ""; }; 84CE69DC2565BB6400559427 /* AboutViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewModel.swift; sourceTree = ""; }; @@ -4340,12 +4297,9 @@ 84D8F17024D856D300AF43E9 /* SNAddressType+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SNAddressType+ViewModel.swift"; sourceTree = ""; }; 84D97EC0251FEE1E00F07405 /* WalletAssetId+Display.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WalletAssetId+Display.swift"; sourceTree = ""; }; 84D97EC72520D32000F07405 /* PolkadotIcon+Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PolkadotIcon+Image.swift"; sourceTree = ""; }; - 84D97ECE2521CA2F00F07405 /* WalletBaseTokenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletBaseTokenView.swift; sourceTree = ""; }; - 84D97ED02521CA5200F07405 /* WalletTokenView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = WalletTokenView.xib; sourceTree = ""; }; 84D9C41026CD361C004AB2AB /* SpecVersionSubscriptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecVersionSubscriptionTests.swift; sourceTree = ""; }; 84DA3B1124C6D29100B5E27F /* RuntimeVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeVersion.swift; sourceTree = ""; }; 84DA3B1324C6D7C700B5E27F /* RuntimeDispatchInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeDispatchInfo.swift; sourceTree = ""; }; - 84DA3B1524C81ECE00B5E27F /* AccountItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountItem.swift; sourceTree = ""; }; 84DA3B1824C8200E00B5E27F /* ConnectionItem+Default.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConnectionItem+Default.swift"; sourceTree = ""; }; 84DAC197268D3DD9002D0DF4 /* SNAddressType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SNAddressType.swift; sourceTree = ""; }; 84DB4E1D25E93B1700A6DF41 /* Identity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Identity.swift; sourceTree = ""; }; @@ -4379,8 +4333,6 @@ 84DF21A025347031005454AE /* DetailsDisplayTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailsDisplayTableViewCell.swift; sourceTree = ""; }; 84DF21A225347042005454AE /* DetailsDisplayTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DetailsDisplayTableViewCell.xib; sourceTree = ""; }; 84DF21A4253473B0005454AE /* ModalInfoFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalInfoFactory.swift; sourceTree = ""; }; - 84DF21A82535AA8F005454AE /* ExistentialDepositInfoCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExistentialDepositInfoCommand.swift; sourceTree = ""; }; - 84DF21B02536DDC1005454AE /* TransferConfirmCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferConfirmCommand.swift; sourceTree = ""; }; 84DF21B425388B35005454AE /* Scheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Scheduler.swift; sourceTree = ""; }; 84E1CCF4260DCB91001E81B5 /* SwitchAccount+AccountManagementWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwitchAccount+AccountManagementWireframe.swift"; sourceTree = ""; }; 84E1CCF9260DCBF9001E81B5 /* SwitchAccount+UsernameSetupWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwitchAccount+UsernameSetupWireframe.swift"; sourceTree = ""; }; @@ -4458,11 +4410,6 @@ 84FAB0642542CA4200319F74 /* CDContactItem+CoreDataDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CDContactItem+CoreDataDecodable.swift"; sourceTree = ""; }; 84FAB0662542D06B00319F74 /* WalletContactOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletContactOperationFactory.swift; sourceTree = ""; }; 84FAB0682542EBDE00319F74 /* SearchData+Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchData+Contacts.swift"; sourceTree = ""; }; - 84FAB06A2542F2C700319F74 /* ContactsViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsViewModelFactory.swift; sourceTree = ""; }; - 84FAB06C254303DA00319F74 /* ContactViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactViewModel.swift; sourceTree = ""; }; - 84FAB071254366ED00319F74 /* ContactTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTableViewCell.swift; sourceTree = ""; }; - 84FAB07325436D6300319F74 /* ContactsConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsConstants.swift; sourceTree = ""; }; - 84FAB075254378FC00319F74 /* ContactsListViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsListViewModelFactory.swift; sourceTree = ""; }; 84FAB0772543791A00319F74 /* ContactContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactContext.swift; sourceTree = ""; }; 84FACB1625F559F200F32ED4 /* WestendStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WestendStub.swift; sourceTree = ""; }; 84FACB1E25F55EC900F32ED4 /* RuntimeCodingServiceStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeCodingServiceStub.swift; sourceTree = ""; }; @@ -4470,7 +4417,6 @@ 84FACB3725F5630000F32ED4 /* RuntimeHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RuntimeHelper.swift; sourceTree = ""; }; 84FACB6525F56D5000F32ED4 /* CalculatorServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatorServiceTests.swift; sourceTree = ""; }; 84FACCD825F8C22A00F32ED4 /* BigInt+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BigInt+Hex.swift"; sourceTree = ""; }; - 84FB1F642526879200E0242B /* WalletSingleProviderIdFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletSingleProviderIdFactory.swift; sourceTree = ""; }; 84FB1F662526920B00E0242B /* SubscanTransferData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscanTransferData.swift; sourceTree = ""; }; 84FB1F68252694B600E0242B /* HistoryInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryInfo.swift; sourceTree = ""; }; 84FB1F6C2526987D00E0242B /* TransactionHistoryContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionHistoryContext.swift; sourceTree = ""; }; @@ -4535,15 +4481,17 @@ 9A9EBD3B7AEA5EF594DFEB49 /* PolkaswapAdjustmentProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PolkaswapAdjustmentProtocols.swift; sourceTree = ""; }; 9AEA7ECB8434DF494D2B25B9 /* ChainAccountTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChainAccountTests.swift; sourceTree = ""; }; 9B5626189788682A84D4E9D7 /* SelectValidatorsConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectValidatorsConfirmPresenter.swift; sourceTree = ""; }; - 9BB81B27EBBDD4D246767C4A /* LiquidityPoolsListRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsListRouter.swift; sourceTree = ""; }; 9C01DCD4DA014E8FB50B9F11 /* CrowdloanContributionSetupTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionSetupTests.swift; sourceTree = ""; }; 9C05A688EA7379572BBCE545 /* SelectMarketRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectMarketRouter.swift; sourceTree = ""; }; + 9FED48DE9B681995E6E4A581 /* LiquidityPoolDetailsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolDetailsProtocols.swift; sourceTree = ""; }; A14CA4551FCC2EBD078E2242 /* AccountConfirmViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountConfirmViewFactory.swift; sourceTree = ""; }; A3104ABC4BECF08B0BA836AA /* AccountConfirmViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountConfirmViewController.swift; sourceTree = ""; }; A31780E84948D7FE632ECB02 /* YourValidatorListProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = YourValidatorListProtocols.swift; sourceTree = ""; }; A3BACB7E24BC87F9218DBBC4 /* StakingPayoutConfirmationViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPayoutConfirmationViewController.swift; sourceTree = ""; }; A427660DDA1D882327F8FF5C /* AssetNetworksTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetNetworksTests.swift; sourceTree = ""; }; + A4900562AFFD45F29F4C5DEF /* LiquidityPoolDetailsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolDetailsViewLayout.swift; sourceTree = ""; }; A6543901A1EE819323DCD95D /* WalletChainAccountDashboardInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletChainAccountDashboardInteractor.swift; sourceTree = ""; }; + A692D227372B24F922EFA058 /* LiquidityPoolsOverviewProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsOverviewProtocols.swift; sourceTree = ""; }; A7219B81CEA13CD60BD8FAFE /* ClaimCrowdloanRewardsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ClaimCrowdloanRewardsProtocols.swift; sourceTree = ""; }; A77DF1CA9610844CF63C4BBC /* Pods-fearlessAll-fearless.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-fearlessAll-fearless.debug.xcconfig"; path = "Target Support Files/Pods-fearlessAll-fearless/Pods-fearlessAll-fearless.debug.xcconfig"; sourceTree = ""; }; A7AD1285797131E836CD994B /* AssetSelectionWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetSelectionWireframe.swift; sourceTree = ""; }; @@ -4675,14 +4623,12 @@ AEF50585261EE6230098574D /* PurchaseProviderPickerTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseProviderPickerTableViewCell.swift; sourceTree = ""; }; AEF5058A261EF27B0098574D /* PurchaseProviderPickerTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PurchaseProviderPickerTableViewCell.xib; sourceTree = ""; }; AEF505A82620249F0098574D /* UIColor+HEX.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+HEX.swift"; sourceTree = ""; }; - AEF50715262359DC0098574D /* WalletSelectPurchaseProviderCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletSelectPurchaseProviderCommand.swift; sourceTree = ""; }; AEF5071D262369C00098574D /* PurchaseProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseProviderProtocol.swift; sourceTree = ""; }; AEF5072926236F9E0098574D /* WalletPurchaseProvidersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletPurchaseProvidersTests.swift; sourceTree = ""; }; AEF507AE262423FD0098574D /* HmacSigner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HmacSigner.swift; sourceTree = ""; }; AEF507B9262486F80098574D /* MoonpayProviderFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoonpayProviderFactory.swift; sourceTree = ""; }; AEF507F126259DF00098574D /* ValidationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidationViewModel.swift; sourceTree = ""; }; AEF507F62625A3280098574D /* ValidatorState+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ValidatorState+Status.swift"; sourceTree = ""; }; - AEF73FB425DBA24300407D41 /* PhishingCheckExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhishingCheckExecutor.swift; sourceTree = ""; }; AEF7404D25E6DC9400407D41 /* RewardCalculatorEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RewardCalculatorEngine.swift; sourceTree = ""; }; AEFC6D612600A772000BD310 /* StoriesPreviewCollectionItem.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StoriesPreviewCollectionItem.xib; sourceTree = ""; }; AEFC6D662600A808000BD310 /* StoriesPreviewCollectionItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoriesPreviewCollectionItem.swift; sourceTree = ""; }; @@ -4700,7 +4646,6 @@ B4774F00EDBB28F374797637 /* ClaimCrowdloanRewardsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ClaimCrowdloanRewardsInteractor.swift; sourceTree = ""; }; B5934BA68F375F5F8237967D /* NetworkInfoViewController.xib */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file.xib; path = NetworkInfoViewController.xib; sourceTree = ""; }; B5E69C4F66805399A3DB4ED8 /* MainNftContainerAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainNftContainerAssembly.swift; sourceTree = ""; }; - B6884DFC1AA1B995C21C274C /* WalletHistoryFilterViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletHistoryFilterViewController.swift; sourceTree = ""; }; B8D9DD27C76FE239728ED5F8 /* StakingPoolCreateInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPoolCreateInteractor.swift; sourceTree = ""; }; B90CEC70F101AA25A4C00021 /* YourValidatorListViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = YourValidatorListViewController.swift; sourceTree = ""; }; B93A1BA7DFEE1D7728B84949 /* AccountExportPasswordTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountExportPasswordTests.swift; sourceTree = ""; }; @@ -4718,6 +4663,7 @@ C316BE4F5A0342D379F783E8 /* StartSelectValidatorsTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StartSelectValidatorsTests.swift; sourceTree = ""; }; C323602A21644DCB1B2EEFF6 /* Pods_fearlessAll_fearlessIntegrationTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_fearlessAll_fearlessIntegrationTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C503100478AB56E903598A78 /* ReferralCrowdloanPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferralCrowdloanPresenter.swift; sourceTree = ""; }; + C53DFFFB3B5B48DB51692EFA /* LiquidityPoolDetailsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolDetailsInteractor.swift; sourceTree = ""; }; C554924081754A981EB4243E /* AssetNetworksInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetNetworksInteractor.swift; sourceTree = ""; }; C569B746953D3BA947ACEA8D /* ChainSelectionViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChainSelectionViewController.swift; sourceTree = ""; }; C597BAB74DF23914B68FDC39 /* SwapTransactionDetailAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapTransactionDetailAssembly.swift; sourceTree = ""; }; @@ -4843,7 +4789,6 @@ C96C3B5ABF4A8124848EFD17 /* ControllerAccountConfirmationWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ControllerAccountConfirmationWireframe.swift; sourceTree = ""; }; CA7427142A4B905B5DB15498 /* NftDetailsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftDetailsViewController.swift; sourceTree = ""; }; CA8ECADDA809DE7932B7A17C /* StakingPoolCreateConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPoolCreateConfirmProtocols.swift; sourceTree = ""; }; - CB9150FEC66FC503CF1BD1D0 /* WalletHistoryFilterPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletHistoryFilterPresenter.swift; sourceTree = ""; }; CC17D12DCD0CDAF0BC13D80D /* StakingUnbondSetupViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondSetupViewController.swift; sourceTree = ""; }; CC5083A5751A1A3CC95F4F6F /* StakingUnbondSetupWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondSetupWireframe.swift; sourceTree = ""; }; CC516FE0E7682210D0F07FB2 /* StakingPoolInfoAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPoolInfoAssembly.swift; sourceTree = ""; }; @@ -4860,7 +4805,6 @@ D39D54DC9992CF9CB6699AA3 /* StakingAmountViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingAmountViewFactory.swift; sourceTree = ""; }; D45B7031E0809CED062C83F8 /* StakingUnbondConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondConfirmPresenter.swift; sourceTree = ""; }; D482753E0D75C5F5E0617998 /* CreateContactInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateContactInteractor.swift; sourceTree = ""; }; - D491B2698446FA356D6D800B /* LiquidityPoolsListViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsListViewLayout.swift; sourceTree = ""; }; D4C391292F22D16427C77CD9 /* SelectValidatorsStartViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectValidatorsStartViewFactory.swift; sourceTree = ""; }; D55EEBFA4A54B3AAF9F52855 /* WarningAlertViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WarningAlertViewLayout.swift; sourceTree = ""; }; D5B7937620F4339EE948EC25 /* AddCustomNodePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddCustomNodePresenter.swift; sourceTree = ""; }; @@ -4887,7 +4831,6 @@ DFBFF377F35B254AB3141100 /* AssetNetworksViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetNetworksViewLayout.swift; sourceTree = ""; }; E0A0CC21B0CD2A47B9B28841 /* NftSendViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftSendViewController.swift; sourceTree = ""; }; E11575D8B4F64C2E805372A5 /* AccountExportPasswordViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountExportPasswordViewFactory.swift; sourceTree = ""; }; - E137263F032552C463560C91 /* LiquidityPoolsListProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsListProtocols.swift; sourceTree = ""; }; E18B94164B49123E62FA60B7 /* AccountManagementViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountManagementViewFactory.swift; sourceTree = ""; }; E1E60EF37AC0A7646ED8FE64 /* AccountImportViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountImportViewFactory.swift; sourceTree = ""; }; E32C2DC4CC106A3509BE651D /* ExportMnemonicTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExportMnemonicTests.swift; sourceTree = ""; }; @@ -4905,6 +4848,7 @@ EA86DE0B14A20416D3AF1E1E /* Pods-fearlessAll-fearlessIntegrationTests.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-fearlessAll-fearlessIntegrationTests.dev.xcconfig"; path = "Target Support Files/Pods-fearlessAll-fearlessIntegrationTests/Pods-fearlessAll-fearlessIntegrationTests.dev.xcconfig"; sourceTree = ""; }; EB8605FD90D8C3553A9897B4 /* AccountImportPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountImportPresenter.swift; sourceTree = ""; }; EC012CF1C792B34BD5FF45A2 /* NftDetailsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftDetailsProtocols.swift; sourceTree = ""; }; + EC863A9CE29C63B740C6E4D9 /* LiquidityPoolsOverviewAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsOverviewAssembly.swift; sourceTree = ""; }; ECBF10B7D4707E4D7D6387CF /* FiltersViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FiltersViewFactory.swift; sourceTree = ""; }; ED916AAFB6B5A7FA0C802615 /* Pods-fearlessTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-fearlessTests.debug.xcconfig"; path = "Target Support Files/Pods-fearlessTests/Pods-fearlessTests.debug.xcconfig"; sourceTree = ""; }; EDB9EDB05686DF11958145E1 /* ControllerAccountWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ControllerAccountWireframe.swift; sourceTree = ""; }; @@ -5083,6 +5027,8 @@ FA004898282CCFCD0032FF49 /* SelectValidatorsStartParachainViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectValidatorsStartParachainViewModelFactory.swift; sourceTree = ""; }; FA00489A282CCFDC0032FF49 /* SelectValidatorsStartRelaychainViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectValidatorsStartRelaychainViewModelFactory.swift; sourceTree = ""; }; FA0066E82935D07D0068FC61 /* RecommendedValidatorListPoolStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecommendedValidatorListPoolStrategy.swift; sourceTree = ""; }; + FA054A992BCD1FA3007B8F6D /* AvailableLiquidityPoolsListPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvailableLiquidityPoolsListPresenter.swift; sourceTree = ""; }; + FA054A9B2BCD1FAF007B8F6D /* AvailableLiquidityPoolsListViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvailableLiquidityPoolsListViewModelFactory.swift; sourceTree = ""; }; FA072C13277AE2FE00731718 /* QRCaptureService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCaptureService.swift; sourceTree = ""; }; FA072C15277B023D00731718 /* QRExtractionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRExtractionService.swift; sourceTree = ""; }; FA072C20277B19A900731718 /* ImageGalleryProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGalleryProtocols.swift; sourceTree = ""; }; @@ -5118,6 +5064,26 @@ FA17B4D327E9CF2C006E0735 /* UtilityConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UtilityConstants.swift; sourceTree = ""; }; FA1A023B274F51A900DA07CB /* ChainAccountBalanceTableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainAccountBalanceTableCell.swift; sourceTree = ""; }; FA1A023D274F55D900DA07CB /* HorizontalKeyValueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalKeyValueView.swift; sourceTree = ""; }; + FA1D01EE2BBE713D005B7071 /* LiquidityPoolListCellModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiquidityPoolListCellModel.swift; sourceTree = ""; }; + FA1D01EF2BBE713D005B7071 /* LiquidityPoolListViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiquidityPoolListViewModel.swift; sourceTree = ""; }; + FA1D01F02BBE713D005B7071 /* LiquidityPoolsListViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsListViewLayout.swift; sourceTree = ""; }; + FA1D01F12BBE713D005B7071 /* LiquidityPoolsListRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsListRouter.swift; sourceTree = ""; }; + FA1D01F22BBE713D005B7071 /* LiquidityPoolsListProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsListProtocols.swift; sourceTree = ""; }; + FA1D01F42BBE713D005B7071 /* LiquidityPoolsListAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsListAssembly.swift; sourceTree = ""; }; + FA1D01F62BBE713D005B7071 /* LiquidityPoolListCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiquidityPoolListCell.swift; sourceTree = ""; }; + FA1D01F72BBE713D005B7071 /* LiquidityPoolsListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsListViewController.swift; sourceTree = ""; }; + FA1D02032BBE71F2005B7071 /* TokenPairIconsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenPairIconsView.swift; sourceTree = ""; }; + FA1D02052BBE71F9005B7071 /* TokenPairsIconViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenPairsIconViewModel.swift; sourceTree = ""; }; + FA1D02082BBE74B7005B7071 /* AvailableLiquidityPoolsListInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvailableLiquidityPoolsListInteractor.swift; sourceTree = ""; }; + FA1D020C2BBE7690005B7071 /* UserLiquidityPoolsListInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLiquidityPoolsListInteractor.swift; sourceTree = ""; }; + FA1D51D62BCFD445001353E7 /* LiquidityPools+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LiquidityPools+ViewModel.swift"; sourceTree = ""; }; + FA1D51D82BCFE353001353E7 /* SoraFiatService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoraFiatService.swift; sourceTree = ""; }; + FA1D51DB2BCFE38D001353E7 /* SubqueryFiatInfoOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubqueryFiatInfoOperation.swift; sourceTree = ""; }; + FA22228C2BD237910031DE04 /* SubqueryPriceFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubqueryPriceFetcher.swift; sourceTree = ""; }; + FA22228E2BD237CF0031DE04 /* SoraSubqueryPriceFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoraSubqueryPriceFetcher.swift; sourceTree = ""; }; + FA2222902BD239500031DE04 /* SoraSubqueryPriceResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoraSubqueryPriceResponse.swift; sourceTree = ""; }; + FA2222932BD2726F0031DE04 /* SkeletonLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkeletonLabel.swift; sourceTree = ""; }; + FA2222952BD272A30031DE04 /* SkeletonLoadableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkeletonLoadableView.swift; sourceTree = ""; }; FA24FEFD2B95C32200CD9E04 /* Decimal+DoubleValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Decimal+DoubleValue.swift"; sourceTree = ""; }; FA256982274CE5A400875A53 /* BalanceLockType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalanceLockType.swift; sourceTree = ""; }; FA256983274CE5A500875A53 /* BalanceLocks+Sort.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BalanceLocks+Sort.swift"; sourceTree = ""; }; @@ -5168,11 +5134,6 @@ FA256A43274CE8BC00875A53 /* StoriesCollectionItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoriesCollectionItem.swift; sourceTree = ""; }; FA256A44274CE8BD00875A53 /* StoriesCollectionItem.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = StoriesCollectionItem.xib; sourceTree = ""; }; FA256A47274CE8C200875A53 /* StakingMainInteractor+Subscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "StakingMainInteractor+Subscription.swift"; sourceTree = ""; }; - FA256A4A274CE93800875A53 /* TransferConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransferConstants.swift; sourceTree = ""; }; - FA256A4C274CEB9000875A53 /* DummySelectedAssetView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DummySelectedAssetView.swift; sourceTree = ""; }; - FA256A4E274CEB9600875A53 /* FeePriceViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeePriceViewModel.swift; sourceTree = ""; }; - FA256A4F274CEB9700875A53 /* RichAmountDisplayViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RichAmountDisplayViewModel.swift; sourceTree = ""; }; - FA256A50274CEB9700875A53 /* RichAmountInputViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RichAmountInputViewModel.swift; sourceTree = ""; }; FA286AF42A3043C3008BD527 /* ConvenienceError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConvenienceError.swift; sourceTree = ""; }; FA286AF72A3043DB008BD527 /* CrossChainConfirmationViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrossChainConfirmationViewLayout.swift; sourceTree = ""; }; FA286AF82A3043DB008BD527 /* CrossChainConfirmationRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrossChainConfirmationRouter.swift; sourceTree = ""; }; @@ -5628,6 +5589,9 @@ FA8800672B31A335000AE5EB /* StakingAccountSubscriptionV13.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingAccountSubscriptionV13.swift; sourceTree = ""; }; FA8800692B31A33E000AE5EB /* StakingAccountSubscriptionV14.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingAccountSubscriptionV14.swift; sourceTree = ""; }; FA88006B2B31A46D000AE5EB /* StakingAccountSubscriptionAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingAccountSubscriptionAssembly.swift; sourceTree = ""; }; + FA8810D12BDCAF350084CC4B /* shared-features-spm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "shared-features-spm"; path = "../shared-features-spm"; sourceTree = ""; }; + FA8810D22BDCCF7E0084CC4B /* UserLiquidityPoolsListPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLiquidityPoolsListPresenter.swift; sourceTree = ""; }; + FA8810D42BDCD19D0084CC4B /* UserLiquidityPoolsListViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLiquidityPoolsListViewModelFactory.swift; sourceTree = ""; }; FA8C34B051607218638BA851 /* FiltersProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FiltersProtocols.swift; sourceTree = ""; }; FA8ED43228FD960F00EBB712 /* YourValidatorListFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YourValidatorListFlow.swift; sourceTree = ""; }; FA8ED43528FD983A00EBB712 /* YourValidatorListRelaychainStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YourValidatorListRelaychainStrategy.swift; sourceTree = ""; }; @@ -5742,11 +5706,6 @@ FA9A8F242A72579D008FA99F /* AlchemySortOrder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlchemySortOrder.swift; sourceTree = ""; }; FA9A8F252A72579D008FA99F /* AlchemyTokenCategory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlchemyTokenCategory.swift; sourceTree = ""; }; FA9A8F2A2A725B30008FA99F /* SubstrateAccountInfoFetching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubstrateAccountInfoFetching.swift; sourceTree = ""; }; - FA9A8F392A776DFD008FA99F /* SubstrateDataModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel.xcdatamodel; sourceTree = ""; }; - FA9A8F3A2A776DFD008FA99F /* SubstrateDataModel_v4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel_v4.xcdatamodel; sourceTree = ""; }; - FA9A8F3B2A776DFD008FA99F /* SubstrateDataModel_v2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel_v2.xcdatamodel; sourceTree = ""; }; - FA9A8F3C2A776DFD008FA99F /* SubstrateDataModel_v5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel_v5.xcdatamodel; sourceTree = ""; }; - FA9A8F3D2A776DFD008FA99F /* SubstrateDataModel_v3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel_v3.xcdatamodel; sourceTree = ""; }; FA9A8F422A78C03D008FA99F /* EthereumTransferService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthereumTransferService.swift; sourceTree = ""; }; FA9A8F442A78C045008FA99F /* SubstrateTransferService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubstrateTransferService.swift; sourceTree = ""; }; FA9A8F462A82005F008FA99F /* EthereumWalletRemoteSubscriptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthereumWalletRemoteSubscriptionService.swift; sourceTree = ""; }; @@ -5901,24 +5860,9 @@ FAB707612BB317D300A1131C /* CrossChainViewLoadingCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossChainViewLoadingCollector.swift; sourceTree = ""; }; FAB707642BB3C06900A1131C /* AssetsAccountRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsAccountRequest.swift; sourceTree = ""; }; FAB707662BB3C1A400A1131C /* AssetAccountInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetAccountInfo.swift; sourceTree = ""; }; - FAB707C42BB6792700A1131C /* TokenPairIconsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenPairIconsView.swift; sourceTree = ""; }; - FAB707C62BB67F7000A1131C /* TokenPairsIconViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenPairsIconViewModel.swift; sourceTree = ""; }; - FAB707C82BB68C4000A1131C /* LiquidityPoolListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidityPoolListCell.swift; sourceTree = ""; }; FAB8B96D29F23FCA002E5F04 /* ChainAccountBalanceViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChainAccountBalanceViewModel.swift; sourceTree = ""; }; FABA161A2B0C941B001AF2F0 /* MultiassetV11MigrationPolicy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiassetV11MigrationPolicy.swift; sourceTree = ""; }; FABA161C2B0C94C9001AF2F0 /* UserDataModelV10toV11.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = UserDataModelV10toV11.xcmappingmodel; sourceTree = ""; }; - FABA161F2B0C94DB001AF2F0 /* MultiassetUserDataModel_v8.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MultiassetUserDataModel_v8.xcdatamodel; sourceTree = ""; }; - FABA16202B0C94DB001AF2F0 /* MultiassetUserDataModel_v4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MultiassetUserDataModel_v4.xcdatamodel; sourceTree = ""; }; - FABA16212B0C94DB001AF2F0 /* MultiassetUserDataModel_v7.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MultiassetUserDataModel_v7.xcdatamodel; sourceTree = ""; }; - FABA16222B0C94DB001AF2F0 /* UserDataModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = UserDataModel.xcdatamodel; sourceTree = ""; }; - FABA16232B0C94DB001AF2F0 /* MultiassetUserDataModel_v11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MultiassetUserDataModel_v11.xcdatamodel; sourceTree = ""; }; - FABA16242B0C94DB001AF2F0 /* MultiassetUserDataModel_v2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MultiassetUserDataModel_v2.xcdatamodel; sourceTree = ""; }; - FABA16252B0C94DB001AF2F0 /* MultiassetUserDataModel_v5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MultiassetUserDataModel_v5.xcdatamodel; sourceTree = ""; }; - FABA16262B0C94DB001AF2F0 /* MultiassetUserDataModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MultiassetUserDataModel.xcdatamodel; sourceTree = ""; }; - FABA16272B0C94DB001AF2F0 /* MultiassetUserDataModel_v9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MultiassetUserDataModel_v9.xcdatamodel; sourceTree = ""; }; - FABA16282B0C94DB001AF2F0 /* MultiassetUserDataModel_v3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MultiassetUserDataModel_v3.xcdatamodel; sourceTree = ""; }; - FABA16292B0C94DB001AF2F0 /* MultiassetUserDataModel_v10.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MultiassetUserDataModel_v10.xcdatamodel; sourceTree = ""; }; - FABA162A2B0C94DB001AF2F0 /* MultiassetUserDataModel_v6.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MultiassetUserDataModel_v6.xcdatamodel; sourceTree = ""; }; FABA162C2B0C9504001AF2F0 /* TabBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabBar.swift; sourceTree = ""; }; FABA162F2B0C9510001AF2F0 /* NetworkManagmentPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkManagmentPresenter.swift; sourceTree = ""; }; FABA16302B0C9510001AF2F0 /* NetworkManagmentTableCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkManagmentTableCell.swift; sourceTree = ""; }; @@ -5958,6 +5902,27 @@ FAC0BBCE291D0EB000E6F106 /* SelectAssetProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectAssetProtocols.swift; sourceTree = ""; }; FAC0BBE5291D11D600E6F106 /* SelectableAmountInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectableAmountInputView.swift; sourceTree = ""; }; FAC2175D293F41B600A8BA83 /* AllDonePresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDonePresentable.swift; sourceTree = ""; }; + FAC6CD852BA7F9990013A17E /* Pagination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pagination.swift; sourceTree = ""; }; + FAC6CD872BA7F9F70013A17E /* FeeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeeViewModel.swift; sourceTree = ""; }; + FAC6CD892BA7FA4B0013A17E /* AssetTransactionData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetTransactionData.swift; sourceTree = ""; }; + FAC6CD8B2BA7FA6C0013A17E /* AmountDecimal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmountDecimal.swift; sourceTree = ""; }; + FAC6CD8D2BA7FBD30013A17E /* WalletHistoryRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletHistoryRequest.swift; sourceTree = ""; }; + FAC6CD8F2BA7FCA70013A17E /* AssetTransactionFee.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetTransactionFee.swift; sourceTree = ""; }; + FAC6CD912BA8022D0013A17E /* WalletAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletAsset.swift; sourceTree = ""; }; + FAC6CD932BA802840013A17E /* NumberFormatterFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberFormatterFactoryProtocol.swift; sourceTree = ""; }; + FAC6CD952BA807B80013A17E /* AccessoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessoryViewModel.swift; sourceTree = ""; }; + FAC6CD972BA807D30013A17E /* AccessoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessoryView.swift; sourceTree = ""; }; + FAC6CD992BA809160013A17E /* ReceiveInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveInfo.swift; sourceTree = ""; }; + FAC6CD9C2BA8097C0013A17E /* L10n.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = L10n.swift; sourceTree = ""; }; + FAC6CD9E2BA80AB70013A17E /* WalletLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletLanguage.swift; sourceTree = ""; }; + FAC6CDA02BA80CB10013A17E /* WalletTransactionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletTransactionType.swift; sourceTree = ""; }; + FAC6CDA32BA80D6A0013A17E /* WalletErrorContentProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletErrorContentProtocol.swift; sourceTree = ""; }; + FAC6CDA62BA814020013A17E /* BalanceContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalanceContext.swift; sourceTree = ""; }; + FAC6CDA82BA814F20013A17E /* UIControl+Disable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIControl+Disable.swift"; sourceTree = ""; }; + FAC6CDAA2BA819540013A17E /* SearchData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchData.swift; sourceTree = ""; }; + FAC6CDAC2BA81D680013A17E /* FeeViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeeViewProtocol.swift; sourceTree = ""; }; + FAC6CDAE2BA81FA00013A17E /* WalletLoggerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletLoggerProtocol.swift; sourceTree = ""; }; + FAC6CDB02BA821B00013A17E /* WalletImageViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletImageViewModelProtocol.swift; sourceTree = ""; }; FACACE1027BCF104005422EE /* MetaAccountCreationMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetaAccountCreationMetadata.swift; sourceTree = ""; }; FACACE1227BCF10E005422EE /* MetaAccountImportMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetaAccountImportMetadata.swift; sourceTree = ""; }; FACACE1327BCF10E005422EE /* MetaAccountImportPreferredInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetaAccountImportPreferredInfo.swift; sourceTree = ""; }; @@ -5992,8 +5957,11 @@ FACD42BB2A5BE91D009975AA /* PortionRewardCalculatorService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PortionRewardCalculatorService.swift; sourceTree = ""; }; FACD42BE2A5BE93D009975AA /* polkadot-9370metadata */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "polkadot-9370metadata"; sourceTree = ""; }; FACD42C02A5C10BB009975AA /* TransferService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransferService.swift; sourceTree = ""; }; - FACE6C222BB6C37900643CEF /* LiquidityPoolListCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidityPoolListCellModel.swift; sourceTree = ""; }; - FACE6C642BBBBC8C00643CEF /* LiquidityPoolListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidityPoolListViewModel.swift; sourceTree = ""; }; + FACE6C5E2BBAC3B000643CEF /* SubstrateDataModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel.xcdatamodel; sourceTree = ""; }; + FACE6C5F2BBAC3B000643CEF /* SubstrateDataModel_v4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel_v4.xcdatamodel; sourceTree = ""; }; + FACE6C602BBAC3B000643CEF /* SubstrateDataModel_v2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel_v2.xcdatamodel; sourceTree = ""; }; + FACE6C612BBAC3B000643CEF /* SubstrateDataModel_v5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel_v5.xcdatamodel; sourceTree = ""; }; + FACE6C622BBAC3B000643CEF /* SubstrateDataModel_v3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel_v3.xcdatamodel; sourceTree = ""; }; FAD0067E27EA252400C97E09 /* AboutViewModelFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutViewModelFactory.swift; sourceTree = ""; }; FAD0067F27EA252400C97E09 /* AboutViewState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutViewState.swift; sourceTree = ""; }; FAD0068227EA255900C97E09 /* AboutTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutTableViewCell.swift; sourceTree = ""; }; @@ -6214,8 +6182,11 @@ FB19F24CAA892FE8B5FE1520 /* NftCollectionViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftCollectionViewController.swift; sourceTree = ""; }; FBAA28C551344F1457D76DD5 /* ClaimCrowdloanRewardsAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ClaimCrowdloanRewardsAssembly.swift; sourceTree = ""; }; FBFDF844248CD43AAD13139F /* StakingPayoutConfirmationInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPayoutConfirmationInteractor.swift; sourceTree = ""; }; + FC0B2D0A77F4B0F7CC9E7C1D /* LiquidityPoolsOverviewViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsOverviewViewLayout.swift; sourceTree = ""; }; FC1236F31289F7F25A25E69C /* ClaimCrowdloanRewardsRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ClaimCrowdloanRewardsRouter.swift; sourceTree = ""; }; + FC76E7D99A98423180BC572F /* LiquidityPoolsOverviewTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsOverviewTests.swift; sourceTree = ""; }; FD845193EDFC3A1D0BC73719 /* NftCollectionProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftCollectionProtocols.swift; sourceTree = ""; }; + FD8B69E9E18C11EAEC9284B3 /* LiquidityPoolsOverviewViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsOverviewViewController.swift; sourceTree = ""; }; FE4AF0849E32E5B9C72E2ABB /* RecommendedValidatorListViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RecommendedValidatorListViewFactory.swift; sourceTree = ""; }; FE826F356F6D72EACFB0AE31 /* NftSendConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftSendConfirmPresenter.swift; sourceTree = ""; }; FF4688AF0658F8BB7A90C2BE /* ExportMnemonicConfirmViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExportMnemonicConfirmViewFactory.swift; sourceTree = ""; }; @@ -6237,15 +6208,44 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + FA8810B62BDCAF260084CC4B /* SSFNetwork in Frameworks */, FA72546F2AC2F12D00EC47A6 /* Web3Wallet in Frameworks */, + FA8810B02BDCAF260084CC4B /* SSFKeyPair in Frameworks */, + FA8810AE2BDCAF260084CC4B /* SSFHelpers in Frameworks */, + FA8810A22BDCAF260084CC4B /* SSFChainConnection in Frameworks */, + FA8810A02BDCAF260084CC4B /* SSFAssetManagment in Frameworks */, + FA88109A2BDCAF260084CC4B /* RobinHood in Frameworks */, + FA8810C82BDCAF260084CC4B /* SSFTransferService in Frameworks */, FA72546B2AC2F12D00EC47A6 /* WalletConnectNetworking in Frameworks */, + FA8810CA2BDCAF260084CC4B /* SSFUtils in Frameworks */, FA8FD1882AF4BEDD00354482 /* Swime in Frameworks */, + FA8810BA2BDCAF260084CC4B /* SSFPools in Frameworks */, + FA8810A42BDCAF260084CC4B /* SSFChainRegistry in Frameworks */, FA8FD1832AF4B55100354482 /* Web3ContractABI in Frameworks */, + FA8810B82BDCAF260084CC4B /* SSFPolkaswap in Frameworks */, FA8FD1812AF4B55100354482 /* Web3 in Frameworks */, + FA8810BE2BDCAF260084CC4B /* SSFQRService in Frameworks */, + FA8810CE2BDCAF260084CC4B /* SoraKeystore in Frameworks */, + FA88109E2BDCAF260084CC4B /* SSFAccountManagmentStorage in Frameworks */, + FA8810AC2BDCAF260084CC4B /* SSFExtrinsicKit in Frameworks */, + FA88109C2BDCAF260084CC4B /* SSFAccountManagment in Frameworks */, + FA8810A62BDCAF260084CC4B /* SSFCloudStorage in Frameworks */, + FA8810A82BDCAF260084CC4B /* SSFCrypto in Frameworks */, + FA8810C42BDCAF260084CC4B /* SSFSingleValueCache in Frameworks */, + FA8810AA2BDCAF260084CC4B /* SSFEraKit in Frameworks */, FA8FD1852AF4B55100354482 /* Web3PromiseKit in Frameworks */, + FA8810D02BDCAF260084CC4B /* keccak in Frameworks */, + FA8810C62BDCAF260084CC4B /* SSFStorageQueryKit in Frameworks */, + FA8810B42BDCAF260084CC4B /* SSFModels in Frameworks */, FA7254672AC2F12D00EC47A6 /* WalletConnect in Frameworks */, FA72546D2AC2F12D00EC47A6 /* WalletConnectPairing in Frameworks */, FA7254692AC2F12D00EC47A6 /* WalletConnectAuth in Frameworks */, + FA8810C02BDCAF260084CC4B /* SSFRuntimeCodingService in Frameworks */, + FA8810BC2BDCAF260084CC4B /* SSFPoolsStorage in Frameworks */, + FA8810B22BDCAF260084CC4B /* SSFLogger in Frameworks */, + FA8810C22BDCAF260084CC4B /* SSFSigner in Frameworks */, + FA8810982BDCAF260084CC4B /* IrohaCrypto in Frameworks */, + FA8810CC2BDCAF260084CC4B /* SSFXCM in Frameworks */, C5AFED6C37C2C29E9903D136 /* Pods_fearlessAll_fearless.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -6663,13 +6663,6 @@ path = Chainlink; sourceTree = ""; }; - 07C96DD92AC16EC000CB39F5 /* Packages */ = { - isa = PBXGroup; - children = ( - ); - name = Packages; - sourceTree = ""; - }; 07D05E4228EEFBFE00B66C70 /* Pool */ = { isa = PBXGroup; children = ( @@ -7003,22 +6996,6 @@ path = ClaimCrowdloanRewards; sourceTree = ""; }; - 0D31C32ADE0D58DD072D6AB2 /* LiquidityPoolsList */ = { - isa = PBXGroup; - children = ( - FACE6C212BB6C36B00643CEF /* ViewModel */, - FAB707C32BB678CB00A1131C /* View */, - E137263F032552C463560C91 /* LiquidityPoolsListProtocols.swift */, - 9BB81B27EBBDD4D246767C4A /* LiquidityPoolsListRouter.swift */, - 65D8653B07D92DFE321CEF1E /* LiquidityPoolsListPresenter.swift */, - 7045FDDE48C27A858389B302 /* LiquidityPoolsListInteractor.swift */, - 101F1829F8CB980B39A7F14A /* LiquidityPoolsListViewController.swift */, - D491B2698446FA356D6D800B /* LiquidityPoolsListViewLayout.swift */, - 6627C06C19A177744E32BA35 /* LiquidityPoolsListAssembly.swift */, - ); - path = LiquidityPoolsList; - sourceTree = ""; - }; 0D3E297DAEAAF14373239578 /* NetworkInfo */ = { isa = PBXGroup; children = ( @@ -7265,7 +7242,6 @@ children = ( 2AB7A7FE25CD0E7F00767D87 /* GitHubPhishingAPIService.swift */, 2AD0A18F25D3D1E100312428 /* GitHubPhishingServiceFactory.swift */, - AEF73FB425DBA24300407D41 /* PhishingCheckExecutor.swift */, ); path = GitHubPhishingService; sourceTree = ""; @@ -7317,20 +7293,6 @@ path = WalletOption; sourceTree = ""; }; - 35542AFCBEC3F0BD298F5030 /* HistoryFilter */ = { - isa = PBXGroup; - children = ( - 84ACEBF0261E663700AAE665 /* ViewModel */, - 41DFB2757D029FB5DF3CEBC2 /* WalletHistoryFilterProtocols.swift */, - 78670B0926E92B75088D2D7B /* WalletHistoryFilterWireframe.swift */, - CB9150FEC66FC503CF1BD1D0 /* WalletHistoryFilterPresenter.swift */, - B6884DFC1AA1B995C21C274C /* WalletHistoryFilterViewController.swift */, - 0AB10C8DA56E92C280D66BE8 /* WalletHistoryFilterViewFactory.swift */, - 84ACEBFE261E6C7C00AAE665 /* WalletHistoryFilterViewLayout.swift */, - ); - path = HistoryFilter; - sourceTree = ""; - }; 3722354DA3C59896C49B5794 /* StakingRewardDetails */ = { isa = PBXGroup; children = ( @@ -7438,6 +7400,20 @@ path = NftCollection; sourceTree = ""; }; + 478C62A42D572C8647512722 /* LiquidityPoolDetails */ = { + isa = PBXGroup; + children = ( + 9FED48DE9B681995E6E4A581 /* LiquidityPoolDetailsProtocols.swift */, + 28E3B4BA1160B87C435C2AAF /* LiquidityPoolDetailsRouter.swift */, + 81E9DD6EB14A352635BAC711 /* LiquidityPoolDetailsPresenter.swift */, + C53DFFFB3B5B48DB51692EFA /* LiquidityPoolDetailsInteractor.swift */, + 67BDE520860A67C800E7F4AB /* LiquidityPoolDetailsViewController.swift */, + A4900562AFFD45F29F4C5DEF /* LiquidityPoolDetailsViewLayout.swift */, + 5F6F7AE5AFF3F2E7BADA02BB /* LiquidityPoolDetailsAssembly.swift */, + ); + path = LiquidityPoolDetails; + sourceTree = ""; + }; 493D529207797CDC4F179A5C /* NftSend */ = { isa = PBXGroup; children = ( @@ -7633,6 +7609,20 @@ path = CustomValidators; sourceTree = ""; }; + 675669EAE3DAF27168F1B390 /* LiquidityPoolsOverview */ = { + isa = PBXGroup; + children = ( + A692D227372B24F922EFA058 /* LiquidityPoolsOverviewProtocols.swift */, + 3F25F3A88B4BEB4DE498220C /* LiquidityPoolsOverviewRouter.swift */, + 3BA8DC8007FC0A322C6DF00E /* LiquidityPoolsOverviewPresenter.swift */, + 6717FF1B7777400B62F028C3 /* LiquidityPoolsOverviewInteractor.swift */, + FD8B69E9E18C11EAEC9284B3 /* LiquidityPoolsOverviewViewController.swift */, + FC0B2D0A77F4B0F7CC9E7C1D /* LiquidityPoolsOverviewViewLayout.swift */, + EC863A9CE29C63B740C6E4D9 /* LiquidityPoolsOverviewAssembly.swift */, + ); + path = LiquidityPoolsOverview; + sourceTree = ""; + }; 6784511E27C7B7E095FFD4AB /* Modules */ = { isa = PBXGroup; children = ( @@ -7688,7 +7678,8 @@ E1A8025898B97966D25AF46D /* NftSendConfirm */, 85BB4B6268BB3E4F39F414D1 /* AssetNetworks */, D734A66128ED3C8653098101 /* ClaimCrowdloanRewards */, - ACA6E90849E6A0CC4C775867 /* LiquidityPoolsList */, + 9468F26496DF90FEE7016AA3 /* LiquidityPoolsOverview */, + 9439C16432098735E8F4C122 /* LiquidityPoolDetails */, ); path = Modules; sourceTree = ""; @@ -8576,12 +8567,6 @@ 84585A32251CE1BF00390F7A /* Contacts */ = { isa = PBXGroup; children = ( - 84FAB070254366D100319F74 /* View */, - 84585A33251CE2E300390F7A /* ContactsLocalSearchEngine.swift */, - 84969733251CE9CD00C39524 /* ContactsConfigurator.swift */, - 84FAB06A2542F2C700319F74 /* ContactsViewModelFactory.swift */, - 84FAB06C254303DA00319F74 /* ContactViewModel.swift */, - 84FAB075254378FC00319F74 /* ContactsListViewModelFactory.swift */, 84FAB0772543791A00319F74 /* ContactContext.swift */, ); path = Contacts; @@ -8657,15 +8642,6 @@ path = Longrun; sourceTree = ""; }; - 8460E1142541E01B00826F55 /* Style */ = { - isa = PBXGroup; - children = ( - 8490151724AB8C6D008F705E /* WalletCommonStyleConfigurator.swift */, - 8460E1152541E03400826F55 /* WalletFormCellStyle+Internal.swift */, - ); - path = Style; - sourceTree = ""; - }; 8461CC8826BD2EE8007460E4 /* RuntimeProviderPool */ = { isa = PBXGroup; children = ( @@ -8822,8 +8798,7 @@ 8467FD4E24EFD0EC005D486C /* Storage */ = { isa = PBXGroup; children = ( - FABA161E2B0C94DB001AF2F0 /* UserDataModel.xcdatamodeld */, - FA9A8F382A776DFD008FA99F /* SubstrateDataModel.xcdatamodeld */, + FACE6C5D2BBAC3B000643CEF /* SubstrateDataModel.xcdatamodeld */, FACD42852A5BE811009975AA /* Migration */, FA74359C29C0736F0085A47E /* StorageWrapper.swift */, 8457F91126EB9088006803E1 /* EntityToModel */, @@ -9082,7 +9057,7 @@ 8490139F24A80984008F705E = { isa = PBXGroup; children = ( - 07C96DD92AC16EC000CB39F5 /* Packages */, + FAF25D3F2BD7C45E00EA4E3B /* Packages */, 849013AA24A80984008F705E /* fearless */, 849013C124A80986008F705E /* fearlessTests */, 8438E1D024BFAAD2001BDB13 /* fearlessIntegrationTests */, @@ -9147,6 +9122,8 @@ 849013D124A92686008F705E /* Common */ = { isa = PBXGroup; children = ( + FAC6CDA22BA80D590013A17E /* Errors */, + FAC6CD9B2BA8096D0013A17E /* Localization */, FA6C178F2993601A00A55254 /* AddressChainDefiner */, FAADC1BD29265A7900DA9903 /* QRCoding */, 07FBC9E328BE269F00ED65B4 /* ChainIssuesCenter */, @@ -9183,6 +9160,7 @@ 849013D224A9268D008F705E /* Modules */ = { isa = PBXGroup; children = ( + FA1D51D42BCFD410001353E7 /* LiquidityPools */, FA34EEC82B98723B0042E73E /* BalanceLocksDetail */, FA34EEBD2B98723B0042E73E /* Onboarding */, FA34EEB52B98723B0042E73E /* OnboardingStart */, @@ -9250,7 +9228,6 @@ 45C94A390068322611CA7C02 /* SwapTransactionDetail */, DA46895F73B0FB1064146E47 /* AssetNetworks */, 0AEF32A92BEEDB9B5A60A433 /* ClaimCrowdloanRewards */, - 0D31C32ADE0D58DD072D6AB2 /* LiquidityPoolsList */, ); path = Modules; sourceTree = ""; @@ -9305,6 +9282,7 @@ FAD9AAC02B8DF62100AA603B /* AsyncMap.swift */, FAA9BC402B8F17BA00A875BF /* Collection+Average.swift */, FA24FEFD2B95C32200CD9E04 /* Decimal+DoubleValue.swift */, + FAC6CDA82BA814F20013A17E /* UIControl+Disable.swift */, ); path = Extension; sourceTree = ""; @@ -9332,7 +9310,6 @@ 842876B024AE059700D91AD8 /* AboutData.swift */, 84113B6B255B2835009BD21A /* AccountCreateError.swift */, 843C49DC24DF397800B71DDA /* AccountImportSource.swift */, - 84DA3B1524C81ECE00B5E27F /* AccountItem.swift */, 8423B0DF251A759000B8687C /* Chain.swift */, 8463A72C25E3A8E1003B8160 /* ChainStorageDecodedItem.swift */, 843910AF253ED36C00E3C217 /* ChainStorageItem.swift */, @@ -9398,6 +9375,15 @@ FA3067212B621540006A0BA5 /* LockProtocol.swift */, FAAA29562B8DED770089AFE6 /* MapKeyType.swift */, FA34EEF02B9875CC0042E73E /* AccountIdVariant.swift */, + FAC6CD852BA7F9990013A17E /* Pagination.swift */, + FAC6CD892BA7FA4B0013A17E /* AssetTransactionData.swift */, + FAC6CD8B2BA7FA6C0013A17E /* AmountDecimal.swift */, + FAC6CD8D2BA7FBD30013A17E /* WalletHistoryRequest.swift */, + FAC6CD8F2BA7FCA70013A17E /* AssetTransactionFee.swift */, + FAC6CD912BA8022D0013A17E /* WalletAsset.swift */, + FAC6CD992BA809160013A17E /* ReceiveInfo.swift */, + FAC6CDA02BA80CB10013A17E /* WalletTransactionType.swift */, + FAC6CDAA2BA819540013A17E /* SearchData.swift */, ); path = Model; sourceTree = ""; @@ -9481,6 +9467,7 @@ FA17B4BF27E97536006E0735 /* DeactivatableView.swift */, 07089AF828B78248001566CA /* SheetAlertPresentable.swift */, FAC2175D293F41B600A8BA83 /* AllDonePresentable.swift */, + FAC6CDAC2BA81D680013A17E /* FeeViewProtocol.swift */, ); path = Protocols; sourceTree = ""; @@ -9598,6 +9585,8 @@ FAD428932A834A8E001D6A16 /* TopViewControllerHelper.swift */, FAD428972A860C9B001D6A16 /* EthereumNodeFetching.swift */, FA584C792AB2BFE300F6F020 /* MediaType.swift */, + FAC6CD932BA802840013A17E /* NumberFormatterFactoryProtocol.swift */, + FAC6CDAE2BA81FA00013A17E /* WalletLoggerProtocol.swift */, ); path = Helpers; sourceTree = ""; @@ -9690,6 +9679,7 @@ 8490149D24AA7F9A008F705E /* View */ = { isa = PBXGroup; children = ( + FA1D02032BBE71F2005B7071 /* TokenPairIconsView.swift */, FA34EE9D2B9871BD0042E73E /* TriangularedTitleMultiValueView.swift */, FAD429312A865695001D6A16 /* CheckboxButton.swift */, FACD42A82A5BE82A009975AA /* ContentAlignment.swift */, @@ -9773,7 +9763,9 @@ 076D9D58294C461E002762E3 /* PolkaswapDoubleSymbolView.swift */, FA584C772AB2BCD500F6F020 /* UniversalMediaView.swift */, C6DC2D592B1458CC00BAA4DB /* CollectionViewSectionHeader.swift */, - FAB707C42BB6792700A1131C /* TokenPairIconsView.swift */, + FAC6CD972BA807D30013A17E /* AccessoryView.swift */, + FA2222932BD2726F0031DE04 /* SkeletonLabel.swift */, + FA2222952BD272A30031DE04 /* SkeletonLoadableView.swift */, ); path = View; sourceTree = ""; @@ -9857,21 +9849,14 @@ 849014FD24AB698B008F705E /* Wallet */ = { isa = PBXGroup; children = ( + FAC6CDA52BA814020013A17E /* AccountList */, FA8F63A429825C90004B8CD4 /* Receive */, - FA256A49274CE93800875A53 /* Model */, - 8490150924AB8A3A008F705E /* AccountList */, - 8494D8612524752500614D8F /* Commands */, 84585A32251CE1BF00390F7A /* Contacts */, 8490151924ABC343008F705E /* History */, - 35542AFCBEC3F0BD298F5030 /* HistoryFilter */, 849E2330254AEF9F00B1F6D4 /* InvoiceScan */, - 8460E1142541E01B00826F55 /* Style */, 84D97ECD2521CA2100F07405 /* View */, - 8490152824ABCFDA008F705E /* WalletCommandDecorator.swift */, - 849014FE24AB69A4008F705E /* WalletContextFactory.swift */, 8490150E24AB8A3A008F705E /* WalletEmptyStateDataSource.swift */, 8490150824AB8A3A008F705E /* WalletEmptyStateDataSource+Module.swift */, - 84FB1F642526879200E0242B /* WalletSingleProviderIdFactory.swift */, 8490152024ABC721008F705E /* WalletStaticImageViewModel.swift */, ); path = Wallet; @@ -9894,40 +9879,10 @@ path = Network; sourceTree = ""; }; - 8490150924AB8A3A008F705E /* AccountList */ = { - isa = PBXGroup; - children = ( - 84CD357125264F510081BC0B /* ViewModel */, - 8490150A24AB8A3A008F705E /* View */, - 84CD357425264FA30081BC0B /* WalletAccountListConstants.swift */, - 846AF8452525C93A00868F37 /* BalanceContext.swift */, - 8475AE1E258B7F4A00B058F3 /* TransferMetadataContext.swift */, - ); - path = AccountList; - sourceTree = ""; - }; - 8490150A24AB8A3A008F705E /* View */ = { - isa = PBXGroup; - children = ( - 8490151C24ABC57A008F705E /* AssetStyleFactory.swift */, - 84C07EE924AF303D008FC35B /* WalletActionsCell.xib */, - 84C07EEB24AF30F2008FC35B /* WalletActionsCell.swift */, - 846AF8472525E23C00868F37 /* WalletTotalPriceCell.swift */, - 846AF8492525E24C00868F37 /* WalletTotalPriceCell.xib */, - 84CD3576252654EA0081BC0B /* WalletAssetCell.swift */, - 84CD35782526554A0081BC0B /* WalletAssetCell.xib */, - ); - path = View; - sourceTree = ""; - }; 8490151924ABC343008F705E /* History */ = { isa = PBXGroup; children = ( - 84FB1F702526AB1200E0242B /* View */, - 84C07EE724AF2E4D008FC35B /* WalletHistoryStyle.swift */, 84FB1F782527065A00E0242B /* HistoryConstants.swift */, - 84A2CD5225685A4100A9FB0D /* WalletHistoryViewFactoryOverriding.swift */, - 84C91FAE261E7CDD002796B9 /* WalletHistoryFilterEditor.swift */, ); path = History; sourceTree = ""; @@ -9993,6 +9948,7 @@ 849244902514EDD900477C1B /* ViewModel */ = { isa = PBXGroup; children = ( + FA1D02052BBE71F9005B7071 /* TokenPairsIconViewModel.swift */, 0702B3162970182B003519F5 /* Amount */, FAA013A028DA1328000A5230 /* StakeAmountViewModel.swift */, FAA013A128DA1328000A5230 /* TitleMultiValueViewModel.swift */, @@ -10008,7 +9964,9 @@ FA38C9A0275FD6B9005C5577 /* BundleImageViewModel.swift */, FA38C9C02761E68B005C5577 /* AccountViewModel.swift */, FA86443F276843E100956D8E /* ViewModelObserver.swift */, - FAB707C62BB67F7000A1131C /* TokenPairsIconViewModel.swift */, + FAC6CD872BA7F9F70013A17E /* FeeViewModel.swift */, + FAC6CD952BA807B80013A17E /* AccessoryViewModel.swift */, + FAC6CDB02BA821B00013A17E /* WalletImageViewModelProtocol.swift */, ); path = ViewModel; sourceTree = ""; @@ -10051,30 +10009,10 @@ 8494D85C25246E7D00614D8F /* ViewModel */ = { isa = PBXGroup; children = ( - FA256A4E274CEB9600875A53 /* FeePriceViewModel.swift */, - FA256A4F274CEB9700875A53 /* RichAmountDisplayViewModel.swift */, - FA256A50274CEB9700875A53 /* RichAmountInputViewModel.swift */, - 8494D85D25246E9A00614D8F /* WalletTokenViewModel.swift */, - 8460E12825422C5A00826F55 /* WalletCompoundDetailsViewModel.swift */, ); path = ViewModel; sourceTree = ""; }; - 8494D8612524752500614D8F /* Commands */ = { - isa = PBXGroup; - children = ( - 84DF21A82535AA8F005454AE /* ExistentialDepositInfoCommand.swift */, - 84DF21B02536DDC1005454AE /* TransferConfirmCommand.swift */, - 2AD0A16925D3854700312428 /* TransferConfirmCommandProxy.swift */, - 8436EDDB25893FA0004D9E97 /* WalletBuyCommand.swift */, - 8494D8622524753400614D8F /* WalletCopyCommand.swift */, - 846A2C482527490700731018 /* WalletSelectAccountCommand.swift */, - 846A2C4F252A063800731018 /* WalletSelectAccountCommandFactory.swift */, - AEF50715262359DC0098574D /* WalletSelectPurchaseProviderCommand.swift */, - ); - path = Commands; - sourceTree = ""; - }; 8494D86E2525316500614D8F /* Subscan */ = { isa = PBXGroup; children = ( @@ -10194,8 +10132,6 @@ 849E2330254AEF9F00B1F6D4 /* InvoiceScan */ = { isa = PBXGroup; children = ( - 849E2331254AEFB400B1F6D4 /* InvoiceScanConfigurator.swift */, - 849E2350254AF44200B1F6D4 /* InvoiceScanLocalSearchEngine.swift */, ); path = InvoiceScan; sourceTree = ""; @@ -10228,14 +10164,6 @@ path = Model; sourceTree = ""; }; - 84ACEBF0261E663700AAE665 /* ViewModel */ = { - isa = PBXGroup; - children = ( - 84ACEBF1261E664900AAE665 /* WalletHistoryFilterViewModel.swift */, - ); - path = ViewModel; - sourceTree = ""; - }; 84C1B98224F5244000FE5470 /* ViewModel */ = { isa = PBXGroup; children = ( @@ -10316,16 +10244,6 @@ path = JSONRPC; sourceTree = ""; }; - 84CD357125264F510081BC0B /* ViewModel */ = { - isa = PBXGroup; - children = ( - 84CD357225264F640081BC0B /* WalletTotalPriceViewModel.swift */, - 8490152224ABC77E008F705E /* WalletAssetViewModel.swift */, - 84BEF600258938C1002A22E2 /* WalletActionsViewModel.swift */, - ); - path = ViewModel; - sourceTree = ""; - }; 84CD82BC263C3A64001A6F01 /* Subscription */ = { isa = PBXGroup; children = ( @@ -10510,23 +10428,7 @@ 84D97ECD2521CA2100F07405 /* View */ = { isa = PBXGroup; children = ( - FA256A4C274CEB9000875A53 /* DummySelectedAssetView.swift */, 8494D85C25246E7D00614D8F /* ViewModel */, - 8494D856252465AC00614D8F /* WalletDisplayTokenView.swift */, - 8494D8542524633300614D8F /* WalletTransferTokenView.swift */, - 84D97ECE2521CA2F00F07405 /* WalletBaseTokenView.swift */, - 84D97ED02521CA5200F07405 /* WalletTokenView.xib */, - 8409C26F25227C1E0049B5C8 /* WalletSingleActionAccessoryView.swift */, - 8409C27125227CA00049B5C8 /* WalletSingleActionAccessoryView.xib */, - 8409C27325227DC60049B5C8 /* WalletSingleActionAccessoryFactory.swift */, - 84265E032523D20A005EEE2D /* WalletBaseAmountView.swift */, - 8409C263252210A70049B5C8 /* WalletDisplayAmountView.swift */, - 8409C2652522110F0049B5C8 /* WalletAmountView.xib */, - 8460E1242542293A00826F55 /* WalletCompoundDetailsView.swift */, - 8460E12625422A6D00826F55 /* WalletCompoundDetailsView.xib */, - 8494D85F252470F400614D8F /* WalletFearlessFormDefining.swift */, - 8409C267252218FF0049B5C8 /* WalletFearlessDefinitionFactory.swift */, - 8409C2692522192B0049B5C8 /* WalletFearlessDefinition.swift */, ); path = View; sourceTree = ""; @@ -10538,7 +10440,6 @@ 84D8F16E24D8451F00AF43E9 /* CryptoType+ViewModel.swift */, 84D8F17024D856D300AF43E9 /* SNAddressType+ViewModel.swift */, 843C49E024DFFC9500B71DDA /* AccountImportSource+ViewModel.swift */, - 84A2C90124E07E440020D3B7 /* CryptoType+Utils.swift */, 84754C992513871300854599 /* SNAddressType+Codable.swift */, 84D97EC0251FEE1E00F07405 /* WalletAssetId+Display.swift */, 84CD356F252620FB0081BC0B /* CryptoType+Extrinsic.swift */, @@ -10835,15 +10736,6 @@ path = Repositories; sourceTree = ""; }; - 84FAB070254366D100319F74 /* View */ = { - isa = PBXGroup; - children = ( - 84FAB071254366ED00319F74 /* ContactTableViewCell.swift */, - 84FAB07325436D6300319F74 /* ContactsConstants.swift */, - ); - path = View; - sourceTree = ""; - }; 84FACB2B25F562CB00F32ED4 /* Resources */ = { isa = PBXGroup; children = ( @@ -10856,14 +10748,6 @@ path = Resources; sourceTree = ""; }; - 84FB1F702526AB1200E0242B /* View */ = { - isa = PBXGroup; - children = ( - 84644B3225685915004EAA4B /* WalletHistoryBackgroundView.swift */, - ); - path = View; - sourceTree = ""; - }; 84FB298A2639AB9400BE0FCD /* ChangeValidators */ = { isa = PBXGroup; children = ( @@ -10951,6 +10835,22 @@ path = StakingRewardPayouts; sourceTree = ""; }; + 9439C16432098735E8F4C122 /* LiquidityPoolDetails */ = { + isa = PBXGroup; + children = ( + 2CF682B92176E0FED5D7B4DB /* LiquidityPoolDetailsTests.swift */, + ); + path = LiquidityPoolDetails; + sourceTree = ""; + }; + 9468F26496DF90FEE7016AA3 /* LiquidityPoolsOverview */ = { + isa = PBXGroup; + children = ( + FC76E7D99A98423180BC572F /* LiquidityPoolsOverviewTests.swift */, + ); + path = LiquidityPoolsOverview; + sourceTree = ""; + }; 98D53F0D0FA4CBD6693747C1 /* WalletTransactionDetails */ = { isa = PBXGroup; children = ( @@ -11057,14 +10957,6 @@ path = StakingUnbondConfirm; sourceTree = ""; }; - ACA6E90849E6A0CC4C775867 /* LiquidityPoolsList */ = { - isa = PBXGroup; - children = ( - 23BF570839C35B8FA1F7CAF6 /* LiquidityPoolsListTests.swift */, - ); - path = LiquidityPoolsList; - sourceTree = ""; - }; AE1000EF2667981A004753B7 /* ChangeTargets */ = { isa = PBXGroup; children = ( @@ -12576,6 +12468,113 @@ path = CustomTransition; sourceTree = ""; }; + FA1D01EC2BBE713D005B7071 /* LiquidityPoolsList */ = { + isa = PBXGroup; + children = ( + FA1D02072BBE74A6005B7071 /* Flows */, + FA1D01ED2BBE713D005B7071 /* ViewModel */, + FA1D01F02BBE713D005B7071 /* LiquidityPoolsListViewLayout.swift */, + FA1D01F12BBE713D005B7071 /* LiquidityPoolsListRouter.swift */, + FA1D01F22BBE713D005B7071 /* LiquidityPoolsListProtocols.swift */, + FA1D01F42BBE713D005B7071 /* LiquidityPoolsListAssembly.swift */, + FA1D01F52BBE713D005B7071 /* View */, + FA1D01F72BBE713D005B7071 /* LiquidityPoolsListViewController.swift */, + ); + path = LiquidityPoolsList; + sourceTree = ""; + }; + FA1D01ED2BBE713D005B7071 /* ViewModel */ = { + isa = PBXGroup; + children = ( + FA1D01EE2BBE713D005B7071 /* LiquidityPoolListCellModel.swift */, + FA1D01EF2BBE713D005B7071 /* LiquidityPoolListViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + FA1D01F52BBE713D005B7071 /* View */ = { + isa = PBXGroup; + children = ( + FA1D01F62BBE713D005B7071 /* LiquidityPoolListCell.swift */, + ); + path = View; + sourceTree = ""; + }; + FA1D02072BBE74A6005B7071 /* Flows */ = { + isa = PBXGroup; + children = ( + FA1D020B2BBE7678005B7071 /* UserPools */, + FA1D020A2BBE766E005B7071 /* AvailablePools */, + ); + path = Flows; + sourceTree = ""; + }; + FA1D020A2BBE766E005B7071 /* AvailablePools */ = { + isa = PBXGroup; + children = ( + FA1D02082BBE74B7005B7071 /* AvailableLiquidityPoolsListInteractor.swift */, + FA054A992BCD1FA3007B8F6D /* AvailableLiquidityPoolsListPresenter.swift */, + FA054A9B2BCD1FAF007B8F6D /* AvailableLiquidityPoolsListViewModelFactory.swift */, + ); + path = AvailablePools; + sourceTree = ""; + }; + FA1D020B2BBE7678005B7071 /* UserPools */ = { + isa = PBXGroup; + children = ( + FA1D020C2BBE7690005B7071 /* UserLiquidityPoolsListInteractor.swift */, + FA8810D22BDCCF7E0084CC4B /* UserLiquidityPoolsListPresenter.swift */, + FA8810D42BDCD19D0084CC4B /* UserLiquidityPoolsListViewModelFactory.swift */, + ); + path = UserPools; + sourceTree = ""; + }; + FA1D51D42BCFD410001353E7 /* LiquidityPools */ = { + isa = PBXGroup; + children = ( + 478C62A42D572C8647512722 /* LiquidityPoolDetails */, + 675669EAE3DAF27168F1B390 /* LiquidityPoolsOverview */, + FA1D51D52BCFD41F001353E7 /* Common */, + FA1D01EC2BBE713D005B7071 /* LiquidityPoolsList */, + ); + path = LiquidityPools; + sourceTree = ""; + }; + FA1D51D52BCFD41F001353E7 /* Common */ = { + isa = PBXGroup; + children = ( + FA1D51D62BCFD445001353E7 /* LiquidityPools+ViewModel.swift */, + ); + path = Common; + sourceTree = ""; + }; + FA1D51DA2BCFE377001353E7 /* SoraFiatService */ = { + isa = PBXGroup; + children = ( + FA1D51D82BCFE353001353E7 /* SoraFiatService.swift */, + FA1D51DB2BCFE38D001353E7 /* SubqueryFiatInfoOperation.swift */, + ); + path = SoraFiatService; + sourceTree = ""; + }; + FA22228B2BD237850031DE04 /* Pricing */ = { + isa = PBXGroup; + children = ( + FA22228C2BD237910031DE04 /* SubqueryPriceFetcher.swift */, + FA22228E2BD237CF0031DE04 /* SoraSubqueryPriceFetcher.swift */, + FA2222922BD239620031DE04 /* Model */, + ); + path = Pricing; + sourceTree = ""; + }; + FA2222922BD239620031DE04 /* Model */ = { + isa = PBXGroup; + children = ( + FA2222902BD239500031DE04 /* SoraSubqueryPriceResponse.swift */, + ); + path = Model; + sourceTree = ""; + }; FA25698C274CE65100875A53 /* Requests */ = { isa = PBXGroup; children = ( @@ -12663,14 +12662,6 @@ path = Requests; sourceTree = ""; }; - FA256A49274CE93800875A53 /* Model */ = { - isa = PBXGroup; - children = ( - FA256A4A274CE93800875A53 /* TransferConstants.swift */, - ); - path = Model; - sourceTree = ""; - }; FA286AF62A3043DB008BD527 /* CrossChainConfirmation */ = { isa = PBXGroup; children = ( @@ -13296,6 +13287,7 @@ FA38C9A227606FDD005C5577 /* ApplicationLayer */ = { isa = PBXGroup; children = ( + FA22228B2BD237850031DE04 /* Pricing */, FA34EE8D2B98710C0042E73E /* Models */, FA34EE8A2B9870FE0042E73E /* ComponentFactories */, FA7741D32B6A350200358315 /* StakingRewards */, @@ -13330,6 +13322,7 @@ FA38C9A52760701F005C5577 /* SearchService.swift */, FA8644322767AB2300956D8E /* HistoryService.swift */, FADBA5F52B61222C00CFCF30 /* NetworkInfoFetching.swift */, + FA1D51DA2BCFE377001353E7 /* SoraFiatService */, ); path = Services; sourceTree = ""; @@ -14789,14 +14782,6 @@ path = AssetsPallet; sourceTree = ""; }; - FAB707C32BB678CB00A1131C /* View */ = { - isa = PBXGroup; - children = ( - FAB707C82BB68C4000A1131C /* LiquidityPoolListCell.swift */, - ); - path = View; - sourceTree = ""; - }; FABA162E2B0C9510001AF2F0 /* NetworkManagment */ = { isa = PBXGroup; children = ( @@ -14892,6 +14877,31 @@ path = View; sourceTree = ""; }; + FAC6CD9B2BA8096D0013A17E /* Localization */ = { + isa = PBXGroup; + children = ( + FAC6CD9C2BA8097C0013A17E /* L10n.swift */, + FAC6CD9E2BA80AB70013A17E /* WalletLanguage.swift */, + ); + path = Localization; + sourceTree = ""; + }; + FAC6CDA22BA80D590013A17E /* Errors */ = { + isa = PBXGroup; + children = ( + FAC6CDA32BA80D6A0013A17E /* WalletErrorContentProtocol.swift */, + ); + path = Errors; + sourceTree = ""; + }; + FAC6CDA52BA814020013A17E /* AccountList */ = { + isa = PBXGroup; + children = ( + FAC6CDA62BA814020013A17E /* BalanceContext.swift */, + ); + path = AccountList; + sourceTree = ""; + }; FAC842347BD44065AAC00D29 /* SelectMarket */ = { isa = PBXGroup; children = ( @@ -14961,15 +14971,6 @@ path = Model; sourceTree = ""; }; - FACE6C212BB6C36B00643CEF /* ViewModel */ = { - isa = PBXGroup; - children = ( - FACE6C222BB6C37900643CEF /* LiquidityPoolListCellModel.swift */, - FACE6C642BBBBC8C00643CEF /* LiquidityPoolListViewModel.swift */, - ); - path = ViewModel; - sourceTree = ""; - }; FAD4289B2A865635001D6A16 /* BackupRiskWarnings */ = { isa = PBXGroup; children = ( @@ -15332,6 +15333,14 @@ path = Adapter; sourceTree = ""; }; + FAF25D3F2BD7C45E00EA4E3B /* Packages */ = { + isa = PBXGroup; + children = ( + FA8810D12BDCAF350084CC4B /* shared-features-spm */, + ); + name = Packages; + sourceTree = ""; + }; FAF5E9C827E46D3E005A3448 /* Codable */ = { isa = PBXGroup; children = ( @@ -15620,6 +15629,35 @@ FA8FD1822AF4B55100354482 /* Web3ContractABI */, FA8FD1842AF4B55100354482 /* Web3PromiseKit */, FA8FD1872AF4BEDD00354482 /* Swime */, + FA8810972BDCAF260084CC4B /* IrohaCrypto */, + FA8810992BDCAF260084CC4B /* RobinHood */, + FA88109B2BDCAF260084CC4B /* SSFAccountManagment */, + FA88109D2BDCAF260084CC4B /* SSFAccountManagmentStorage */, + FA88109F2BDCAF260084CC4B /* SSFAssetManagment */, + FA8810A12BDCAF260084CC4B /* SSFChainConnection */, + FA8810A32BDCAF260084CC4B /* SSFChainRegistry */, + FA8810A52BDCAF260084CC4B /* SSFCloudStorage */, + FA8810A72BDCAF260084CC4B /* SSFCrypto */, + FA8810A92BDCAF260084CC4B /* SSFEraKit */, + FA8810AB2BDCAF260084CC4B /* SSFExtrinsicKit */, + FA8810AD2BDCAF260084CC4B /* SSFHelpers */, + FA8810AF2BDCAF260084CC4B /* SSFKeyPair */, + FA8810B12BDCAF260084CC4B /* SSFLogger */, + FA8810B32BDCAF260084CC4B /* SSFModels */, + FA8810B52BDCAF260084CC4B /* SSFNetwork */, + FA8810B72BDCAF260084CC4B /* SSFPolkaswap */, + FA8810B92BDCAF260084CC4B /* SSFPools */, + FA8810BB2BDCAF260084CC4B /* SSFPoolsStorage */, + FA8810BD2BDCAF260084CC4B /* SSFQRService */, + FA8810BF2BDCAF260084CC4B /* SSFRuntimeCodingService */, + FA8810C12BDCAF260084CC4B /* SSFSigner */, + FA8810C32BDCAF260084CC4B /* SSFSingleValueCache */, + FA8810C52BDCAF260084CC4B /* SSFStorageQueryKit */, + FA8810C72BDCAF260084CC4B /* SSFTransferService */, + FA8810C92BDCAF260084CC4B /* SSFUtils */, + FA8810CB2BDCAF260084CC4B /* SSFXCM */, + FA8810CD2BDCAF260084CC4B /* SoraKeystore */, + FA8810CF2BDCAF260084CC4B /* keccak */, ); productName = fearless; productReference = 849013A824A80984008F705E /* fearless.app */; @@ -15690,6 +15728,7 @@ FA7254652AC2F12D00EC47A6 /* XCRemoteSwiftPackageReference "WalletConnectSwiftV2" */, FA8FD17F2AF4B55100354482 /* XCRemoteSwiftPackageReference "Web3.swift" */, FA8FD1862AF4BEDD00354482 /* XCRemoteSwiftPackageReference "Swime" */, + FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */, ); productRefGroup = 849013A924A80984008F705E /* Products */; projectDirPath = ""; @@ -15730,11 +15769,8 @@ 84D2F45A25EF0556008B914D /* RecommendedValidatorCell.xib in Resources */, 84452F7625D5E2B300F47EC5 /* runtime-default.json in Resources */, FA66FE902779C9AE0037C989 /* runtime-empty.json in Resources */, - 8409C2662522110F0049B5C8 /* WalletAmountView.xib in Resources */, FA2569A5274CE6AD00875A53 /* polkadot-v14-runtime in Resources */, - 84CD35792526554A0081BC0B /* WalletAssetCell.xib in Resources */, 8467FD4524ED5F60005D486C /* ProfileSectionTableViewCell.xib in Resources */, - 84C07EEA24AF303D008FC35B /* WalletActionsCell.xib in Resources */, 8490142224A93027008F705E /* sora-Regular.otf in Resources */, 8490141424A92F6D008F705E /* OnbordingMain.xib in Resources */, AEF5058B261EF27B0098574D /* PurchaseProviderPickerTableViewCell.xib in Resources */, @@ -15748,8 +15784,6 @@ 8490142524A93027008F705E /* sora-SemiBold.otf in Resources */, 84452F7425D5E2B300F47EC5 /* runtime-westend.json in Resources */, FACD42BF2A5BE93D009975AA /* polkadot-9370metadata in Resources */, - 846AF84A2525E24C00868F37 /* WalletTotalPriceCell.xib in Resources */, - 84D97ED12521CA5200F07405 /* WalletTokenView.xib in Resources */, FA256A46274CE8BD00875A53 /* StoriesCollectionItem.xib in Resources */, 076D9D6229506CFA002762E3 /* polkaswapSettings.json in Resources */, 849014B824AA87E3008F705E /* PinSetupViewController.xib in Resources */, @@ -15759,12 +15793,10 @@ 84452F7725D5E2B300F47EC5 /* runtime-kusama.json in Resources */, 842876A924AE049B00D91AD8 /* SelectionSubtitleTableViewCell.xib in Resources */, 8468B86724F59DA700B76BC6 /* IconTitleHeaderView.xib in Resources */, - 8409C27225227CA00049B5C8 /* WalletSingleActionAccessoryView.xib in Resources */, 84D2F46425EF6A5A008B914D /* RecommendedValidatorListHeaderView.xib in Resources */, DAD0C626F7AA97E4974486AD /* AccountConfirmViewController.xib in Resources */, 8438C45B2655AC2600047E3F /* runtime-rococo.json in Resources */, 4F932A5E6F990DC5EDF56215 /* AccountManagementViewController.xib in Resources */, - 8460E12725422A6D00826F55 /* WalletCompoundDetailsView.xib in Resources */, AE2060B02637068700357578 /* CIKeys.stencil in Resources */, 9D5F6A48E7A9166B9341F417 /* NetworkInfoViewController.xib in Resources */, 07F2B75D28A6565900280C38 /* chains.json in Resources */, @@ -16084,6 +16116,7 @@ FAFFAE9A29AC84B10074AF1F /* ParachainSubsquidHistoryResponse.swift in Sources */, F462B364260C88050005AB01 /* UITableView+Reuse.swift in Sources */, FA286AF52A3043C3008BD527 /* ConvenienceError.swift in Sources */, + FA1D02062BBE71F9005B7071 /* TokenPairsIconViewModel.swift in Sources */, FA72543F2AC2E48500EC47A6 /* ABI+Collection.swift in Sources */, 848919D726FB238E004DBAD5 /* JsonDataProviderFactory.swift in Sources */, 846802A3265DA5530034F9B5 /* CrowdloanContributionSetupViewModel.swift in Sources */, @@ -16110,7 +16143,6 @@ FA62624E2AC2E35A005D3D95 /* WalletConnectActiveSessionsViewLayout.swift in Sources */, 842876B624AE05C700D91AD8 /* EmailPresentable.swift in Sources */, FAFFAE9E29AC84B10074AF1F /* RewardOrSlashResponse.swift in Sources */, - 8494D857252465AC00614D8F /* WalletDisplayTokenView.swift in Sources */, 07DE95BA28A1119400E9C2CB /* BalanceInfoPresenter.swift in Sources */, 84D1110E26B931C20016D962 /* ChainModel.swift in Sources */, FAE39AFA2AA063450011A9D6 /* NFTTransferService.swift in Sources */, @@ -16137,10 +16169,8 @@ 8473D40D2657E9DC00B394B2 /* MultiSigner.swift in Sources */, FA17B4B527E858CB006E0735 /* JsonLoadingFailedAlertConfig.swift in Sources */, FAD9AAC52B8DFF6700AA603B /* PrefixStorageRequestWorker.swift in Sources */, - 8409C27425227DC60049B5C8 /* WalletSingleActionAccessoryFactory.swift in Sources */, F477CD26262EAD30004DF739 /* StakingPayoutViewModelFactory.swift in Sources */, FA7336E12A0E3B880096A291 /* JSONResponseDecoder.swift in Sources */, - 84C07EE824AF2E4D008FC35B /* WalletHistoryStyle.swift in Sources */, 844E1E19266142080076AC59 /* BondedState+Status.swift in Sources */, 8493D0E126FF4F5000A28008 /* ScaleCoder+Extension.swift in Sources */, 8428766724ADF22000D91AD8 /* TransformAnimator+Common.swift in Sources */, @@ -16151,7 +16181,6 @@ 8490147424A94A37008F705E /* RootProtocol.swift in Sources */, 842876A424AE049B00D91AD8 /* SelectableSubtitleListViewModel.swift in Sources */, FA004895282CCEB70032FF49 /* SelectValidatorsStartParachainViewModelState.swift in Sources */, - 84CD3577252654EA0081BC0B /* WalletAssetCell.swift in Sources */, AEE5FB1426457A98002B8FDC /* StakingRewardDestSetupInteractor.swift in Sources */, F47BBD7A26314A0C0087DA11 /* StakingBalanceWidgetView.swift in Sources */, FA1A023C274F51A900DA07CB /* ChainAccountBalanceTableCell.swift in Sources */, @@ -16221,6 +16250,7 @@ 845BB8B825E4465F00E5FCDC /* ExtrinsicService.swift in Sources */, F4F65C2E26D8B7F7002EE838 /* FWXAxisChartLegendView.swift in Sources */, FA4B92AD2844D0E60003BCEF /* SelectCurrencyViewModelFactory.swift in Sources */, + FAC6CDAF2BA81FA00013A17E /* WalletLoggerProtocol.swift in Sources */, FA86442327671C8C00956D8E /* WalletTransactionHistorySection.swift in Sources */, FAA0133228DA12B6000A5230 /* StakingPoolMainRouter.swift in Sources */, 07D05E5F28EF0A0A00B66C70 /* SelectValidatorsConfirmPoolInitiatedStrategy.swift in Sources */, @@ -16230,18 +16260,16 @@ FACD42B02A5BE8A4009975AA /* NumbersAndSlashesProcessor.swift in Sources */, F4F2297C260DC05600ACFDB8 /* StakingRewardTokenUsdViewModel.swift in Sources */, FA256989274CE5DD00875A53 /* ViewController+Wrapper.swift in Sources */, + FAC6CD8A2BA7FA4B0013A17E /* AssetTransactionData.swift in Sources */, FAA0137628DA12E3000A5230 /* StakingRedeemConfirmationRelaychainStrategy.swift in Sources */, 842876A524AE049B00D91AD8 /* SelectableTitleListViewModel.swift in Sources */, FA9A8F492A82034B008FA99F /* EthereumConnectionPool.swift in Sources */, 847C96302553426D002D288F /* ExportGenericViewModel.swift in Sources */, - FA256A4B274CE93800875A53 /* TransferConstants.swift in Sources */, - AEF73FB525DBA24300407D41 /* PhishingCheckExecutor.swift in Sources */, FAFFAE9229AC84B10074AF1F /* RewardOrSlash.swift in Sources */, FAD429022A86567F001D6A16 /* BackupCreatePasswordViewController.swift in Sources */, AEE5FB1826457AC1002B8FDC /* StakingRewardDestSetupViewController.swift in Sources */, 07DFA45A289B8D520035A8AB /* WalletBalanceInfo.swift in Sources */, FAA0137328DA12E3000A5230 /* StakingRedeemConfirmationFlow.swift in Sources */, - 846AF8462525C93A00868F37 /* BalanceContext.swift in Sources */, 842D1E8624D1A90400C30A7A /* NSPredicate+Validation.swift in Sources */, F4FDA0F826A57626003D753B /* EraCountdownOperationFactory.swift in Sources */, 07D05E6028EF0A0A00B66C70 /* SelectValidatorsConfirmPoolInitiatedViewModelFactory.swift in Sources */, @@ -16253,7 +16281,6 @@ 84C6801424D7013500006BF5 /* SubtitleContentView.swift in Sources */, FA15BC132823B35C0037C023 /* ParachainStakingLocalStorageSubscriber.swift in Sources */, 84452FA525D679F200F47EC5 /* CDRuntimeMetadataItem+CoreDataCodable.swift in Sources */, - 84D97ECF2521CA2F00F07405 /* WalletBaseTokenView.swift in Sources */, 844EFB5A265FCD9F0090ACB1 /* CrowdloanContributionProtocols.swift in Sources */, FAE5F63127B2384F00F13206 /* AddCustomNodeViewModelFactory.swift in Sources */, 073B34BC2AE8CC4500DC5106 /* WalletConnectDisconnectService.swift in Sources */, @@ -16337,6 +16364,7 @@ 07089AF728B64928001566CA /* NetworkIssuesProviderProtocol.swift in Sources */, 8428768A24AE046300D91AD8 /* AboutProtocols.swift in Sources */, FA004893282CCA520032FF49 /* SelectValidatorsStartRelaychainStrategy.swift in Sources */, + FAC6CDA42BA80D6A0013A17E /* WalletErrorContentProtocol.swift in Sources */, FA7254262AC2E48500EC47A6 /* WalletConnectEthereumTransaction.swift in Sources */, 07B6BC8428BC723500621864 /* NetworkIssuesNotificationTableCell.swift in Sources */, F47BBD612630BA800087DA11 /* StakingBalanceViewLayout.swift in Sources */, @@ -16361,7 +16389,6 @@ 076D9D5C29500F1D002762E3 /* PolkaswapSwapConfirmationViewModelFactory.swift in Sources */, F43A597026D535E2005E973D /* AnalyticsPresenterBaseProtocols.swift in Sources */, FAD4292F2A865680001D6A16 /* BackupWalletImportedPresenter.swift in Sources */, - 8490151824AB8C6D008F705E /* WalletCommonStyleConfigurator.swift in Sources */, FAA086D228470B3700CC2F33 /* SelectValidatorsConfirmParachainViewModelFactory.swift in Sources */, 849E0CD025CFDDB700B33506 /* StorageUpdateData+Decoding.swift in Sources */, FA2E9BAE27A116DA0023FAD2 /* SwitchFilterTableCellViewModel.swift in Sources */, @@ -16377,12 +16404,12 @@ 842349C52624E98C0066ACFE /* MultiAddress+Query.swift in Sources */, 84155DED2539817200A27058 /* ApplicationService.swift in Sources */, FA169FAB28BCBAF000E8D2DC /* PoolStakingAccountUpdatingService.swift in Sources */, - 8409C264252210A70049B5C8 /* WalletDisplayAmountView.swift in Sources */, 841493DC2604C144000D8D1A /* SubscanRewardData.swift in Sources */, 84403D7F25E91BC100494FD4 /* SuperIdentity.swift in Sources */, 842876A824AE049B00D91AD8 /* SelectionListViewController.swift in Sources */, 84786E1A25FA6A470089DFF7 /* StashItem.swift in Sources */, FAD428FB2A86567F001D6A16 /* BackupRiskWarningsPresenter.swift in Sources */, + FAC6CDA92BA814F20013A17E /* UIControl+Disable.swift in Sources */, FA2FC80728B3807C00CC0A42 /* StakingPoolJoinConfigViewLayout.swift in Sources */, 84D2F46925EF6B5F008B914D /* RecommendedValidatorListHeaderView.swift in Sources */, 0765296D2AD9517E00BA1D99 /* PolkaswapService.swift in Sources */, @@ -16426,7 +16453,6 @@ 07DE95B828A1119400E9C2CB /* BalanceInfoInteractor.swift in Sources */, FA86442027671C7700956D8E /* WalletTransactionHistoryCellViewModel.swift in Sources */, FA17B4C027E97536006E0735 /* DeactivatableView.swift in Sources */, - 84FAB076254378FC00319F74 /* ContactsListViewModelFactory.swift in Sources */, F40966CF26B297D7008CD244 /* AnalyticsStakeViewFactory.swift in Sources */, FA93A3032834E4620021330F /* ValidatorInfoParachainStrategy.swift in Sources */, AEAC690426EB891900346599 /* Logger+FearlessUtils.swift in Sources */, @@ -16463,7 +16489,6 @@ 849014CB24AA8B75008F705E /* RootControllerAnimationCoordinator.swift in Sources */, 84DED3FC266651EB00A153BB /* ReferralCrowdloanViewModel.swift in Sources */, FA2FC82328B380C500CC0A42 /* StakingPoolState.swift in Sources */, - 8409C27025227C1E0049B5C8 /* WalletSingleActionAccessoryView.swift in Sources */, 8428766924ADF27D00D91AD8 /* AuthorizationPresentable.swift in Sources */, F4871DEE26D63E3E00D27F23 /* AnalyticsRewardDetailsViewModel.swift in Sources */, 84563D0524F466470055591D /* ManagedAccountItem.swift in Sources */, @@ -16485,6 +16510,7 @@ FAAA290E2B8C8FD10089AFE6 /* StakingErasRewardPointsRequest.swift in Sources */, FAD429242A865680001D6A16 /* BackupWalletProtocols.swift in Sources */, FA936BDC286EE0430059B97A /* CandidateBondMoreCall.swift in Sources */, + FAC6CD962BA807B80013A17E /* AccessoryViewModel.swift in Sources */, FA584C7E2AB308A600F6F020 /* AlchemyNftCollection.swift in Sources */, 84BC330026315D7B003C3FF9 /* ExtrinsicConfirmViewModel.swift in Sources */, AEA0C8A6267B6B2600F9666F /* SelectedValidatorListWireframe.swift in Sources */, @@ -16512,6 +16538,7 @@ FA34EEE72B98723C0042E73E /* BalanceLocksDetailInteractor.swift in Sources */, FA34EE952B9871200042E73E /* CrowdloanService.swift in Sources */, 849013DE24A927E2008F705E /* LocalizationManager+Shared.swift in Sources */, + FAC6CD862BA7F9990013A17E /* Pagination.swift in Sources */, FA904D902B04958500DAEC2D /* SortTableCell.swift in Sources */, FA7336DC2A0E3B880096A291 /* RequestConfig.swift in Sources */, 849013DC24A927E2008F705E /* ApplicationConfigs.swift in Sources */, @@ -16529,7 +16556,6 @@ FA256A23274CE7D600875A53 /* MoonbeamAgreeRemarkInfo.swift in Sources */, 849014BE24AA87E4008F705E /* PinSetupPresenter.swift in Sources */, FAFFAE7F29AC84B10074AF1F /* GraphQLResponse.swift in Sources */, - 84C07EEC24AF30F2008FC35B /* WalletActionsCell.swift in Sources */, 849ABE63262785F200011A2A /* ControllerMapper.swift in Sources */, FAC0BBD9291D0EB000E6F106 /* SendViewController.swift in Sources */, FAA0139D28DA131B000A5230 /* StakingBondMorePoolViewModelState.swift in Sources */, @@ -16554,7 +16580,6 @@ FAD429162A86567F001D6A16 /* BackupSelectWalletRouter.swift in Sources */, 840689FC26321F2700A017B1 /* StorageQuery.swift in Sources */, 8423ADD026B2C38600057EDD /* ImportantFlowViewFactory.swift in Sources */, - 8490152924ABCFDA008F705E /* WalletCommandDecorator.swift in Sources */, FA38C9CD276305B6005C5577 /* WalletSendConfirmViewModel.swift in Sources */, FA256A39274CE80200875A53 /* CrowdloanAddMemoParam.swift in Sources */, FA99C938283B76F5007B1F83 /* SelectValidatorsConfirmRelaychainExistingStrategy.swift in Sources */, @@ -16583,9 +16608,9 @@ 846CA77C27099DD90011124C /* WeaklyAnalyticsRewardSource.swift in Sources */, 84452F9D25D6768000F47EC5 /* RuntimeMetadataItem.swift in Sources */, 849842FE26592C2B006BBB9F /* StatusSectionView.swift in Sources */, - 84DA3B1624C81ECE00B5E27F /* AccountItem.swift in Sources */, F42D125F26C1B14D00E59214 /* AnalyticsValidatorsViewModelFactory.swift in Sources */, FAFFAE9329AC84B10074AF1F /* GiantsquidBond.swift in Sources */, + FA2222912BD239500031DE04 /* SoraSubqueryPriceResponse.swift in Sources */, FAD4292A2A865680001D6A16 /* BackupWalletImportedViewController.swift in Sources */, 8461CC7F26BBFEEA007460E4 /* ExtrinsicEraOperationFactory.swift in Sources */, FA6262622AC2E35A005D3D95 /* RawDataViewLayout.swift in Sources */, @@ -16628,6 +16653,7 @@ FAFFAE8529AC84B10074AF1F /* SubqueryRewardOrSlash.swift in Sources */, 84CA68D926BE9E7F003B9453 /* SpecVersionSubscription.swift in Sources */, 84C74363251E4C2F009576C6 /* DummySigner.swift in Sources */, + FA1D02042BBE71F2005B7071 /* TokenPairIconsView.swift in Sources */, 07AC51132AD8040C000970B8 /* XorlessTransfer.swift in Sources */, 84BEE22D2564765F00D05EB3 /* UIAlertViewController+Account.swift in Sources */, FA8FD1902AFBA34700354482 /* AssetNetworksViewModelFactory.swift in Sources */, @@ -16673,7 +16699,6 @@ FA2FC81128B3807D00CC0A42 /* StakingPoolJoinConfirmViewController.swift in Sources */, 07DE95B628A1119400E9C2CB /* BalanceInfoAssembly.swift in Sources */, 84DB9E982640A49E00F23DD3 /* StakingRedeemViewModelFactory.swift in Sources */, - 84ACEBF2261E664900AAE665 /* WalletHistoryFilterViewModel.swift in Sources */, FA6C176329935DC700A55254 /* RewardOperationFactory.swift in Sources */, FAC0BBAF291CE2B700E6F106 /* StakingPoolMainError.swift in Sources */, AEE5FAFD26415DE2002B8FDC /* StakingRebondSetupViewController.swift in Sources */, @@ -16697,7 +16722,6 @@ 84243095265B1888003E07EC /* CrowdloanMetadata.swift in Sources */, 84CFF1ED26526FBC00DB7CF7 /* StakingBondMoreConfirmationWireframe.swift in Sources */, 847119D5262EF95A00716580 /* PayoutInfoFactoryProtocol.swift in Sources */, - 846A2C50252A063800731018 /* WalletSelectAccountCommandFactory.swift in Sources */, FA9A8F2B2A725B30008FA99F /* SubstrateAccountInfoFetching.swift in Sources */, AEF507BA262486F80098574D /* MoonpayProviderFactory.swift in Sources */, FA9942722805524200D771E5 /* GetBalanceProvider.swift in Sources */, @@ -16712,7 +16736,6 @@ 849014C524AA890D008F705E /* UIFont+Style.swift in Sources */, 0726FFB12AC439DE00336D76 /* WalletConnectPolkadotSignature.swift in Sources */, FA3430EE285065A1002B5975 /* StakingUnbondConfirmRelaychainStrategy.swift in Sources */, - 84FAB06D254303DA00319F74 /* ContactViewModel.swift in Sources */, AE4623722719A85900E4FD30 /* MetaAccountOperationFactory.swift in Sources */, 8490149924AA7892008F705E /* SharingPresentable.swift in Sources */, FA5137AA29AC6F2F00560EBA /* PolkaswapDisclaimerViewModel.swift in Sources */, @@ -16721,7 +16744,6 @@ 8472C5B1265CF9C500E2481B /* StakingRewardDestConfirmViewFactory.swift in Sources */, F4488CF226143E0000AEE6DB /* EraRewardPoints.swift in Sources */, 849DF02F26C53DB900B702F4 /* RuntimeSyncEvents.swift in Sources */, - 84265E042523D20A005EEE2D /* WalletBaseAmountView.swift in Sources */, FAD429352A8656B7001D6A16 /* UITableView.swift in Sources */, FA7336FD2A132F740096A291 /* AlchemyHistoryOperationFactory.swift in Sources */, 844DD65525EAF32D00B1DA97 /* SelectValidatorsStartViewModel.swift in Sources */, @@ -16763,13 +16785,11 @@ 840882B02514024800177E20 /* SelectedConnectionChanged.swift in Sources */, 84FD3DB72540EF0700A234E3 /* TransactionSubscription.swift in Sources */, FA7741E02B6A350200358315 /* StakingRewardsFetcherAssembly.swift in Sources */, - 2AD0A16A25D3854700312428 /* TransferConfirmCommandProxy.swift in Sources */, 84C3F7832602086100D47501 /* StakingViewState.swift in Sources */, FA2FC7FD28B3807C00CC0A42 /* StakingPoolListTableCell.swift in Sources */, 849014DC24AA8F60008F705E /* MainTabBarPresenter.swift in Sources */, FA93A2D32832325C0021330F /* CustomValidatorListFlow.swift in Sources */, 8467F4C526B1E4E200C5B6F4 /* StakingDurationFetching.swift in Sources */, - 84FAB072254366ED00319F74 /* ContactTableViewCell.swift in Sources */, FABA163E2B0C9510001AF2F0 /* NetworkManagmentViewModelFactory.swift in Sources */, 071BC677292B21CC007685D1 /* UIImage.swift in Sources */, FA34EEB32B9872240042E73E /* StakingControllerRequest.swift in Sources */, @@ -16806,7 +16826,6 @@ 84452F2B25D5B84200F47EC5 /* RuntimeRegistryServiceProtocol.swift in Sources */, FA7336DF2A0E3B880096A291 /* NetworkRequestType.swift in Sources */, 070CDD752ACAE04100F3F20A /* SoraQRInfo.swift in Sources */, - 8460E1162541E03400826F55 /* WalletFormCellStyle+Internal.swift in Sources */, FACD42992A5BE811009975AA /* SubstrateStorageMigrator+Sync.swift in Sources */, FAA0132E28DA12B6000A5230 /* StakingPoolNetworkInfoViewModel.swift in Sources */, FAADC1AA29261F7000DA9903 /* PoolRolesConfirmInteractor.swift in Sources */, @@ -16822,6 +16841,7 @@ 84DD5F30263D84F300425ACF /* RuntimeConstantFetching.swift in Sources */, C6D7CE0B27E9932D00FFAA6B /* AccountImportViewController.swift in Sources */, 07DFA4442897AEB40035A8AB /* WalletsManagmentTableCell.swift in Sources */, + FAC6CDA72BA814020013A17E /* BalanceContext.swift in Sources */, FAFDB2C329112A00003971FB /* SubstrateCallPath.swift in Sources */, F47BBD5B2630BA500087DA11 /* StakingBalanceViewController.swift in Sources */, 84B64E412704569D00914E88 /* RelaychainStakingLocalSubscriptionHandler.swift in Sources */, @@ -16830,14 +16850,12 @@ 8428766B24ADF51D00D91AD8 /* UIViewController+Modal.swift in Sources */, FAAC292C2ADCF3BB00063962 /* WalletConnectConfirmationCoordinator.swift in Sources */, 84F30E9725FD3C5300039D09 /* EventEmittingStorageSubscription.swift in Sources */, - 8494D85E25246E9A00614D8F /* WalletTokenViewModel.swift in Sources */, AEAC68FE26EA3C2A00346599 /* CoingeckoPriceSource.swift in Sources */, 8472C5B4265CF9C500E2481B /* StakingRewardDestConfirmPresenter.swift in Sources */, FAA0133928DA12B6000A5230 /* StakingPoolManagementViewModel.swift in Sources */, 2AB7A7FF25CD0E8000767D87 /* GitHubPhishingAPIService.swift in Sources */, AEF507F72625A3280098574D /* ValidatorState+Status.swift in Sources */, 843910D9253F8DFB00E3C217 /* StatusPresentable.swift in Sources */, - 8490151D24ABC57A008F705E /* AssetStyleFactory.swift in Sources */, 847C963D255351BB002D288F /* ScrollableContainerView.swift in Sources */, FAFFAE9D29AC84B10074AF1F /* DelegatorHistoryItem.swift in Sources */, FA4889672B7F5E360092ABF8 /* GiantsquidExtrinsic.swift in Sources */, @@ -16870,6 +16888,7 @@ 844CB57826FA702700396E13 /* CrowdloansViewInfo.swift in Sources */, AEACD5F9265E94AB00A09892 /* StatusViewModel.swift in Sources */, 84CCBFBC2509709500180F4F /* UIBarButtonItem+Style.swift in Sources */, + FA1D02092BBE74B7005B7071 /* AvailableLiquidityPoolsListInteractor.swift in Sources */, 8449660A25E15ECA00F2E9F5 /* RewardDestinationViewModel.swift in Sources */, FA2E9BBB27A14BB00023FAD2 /* WalletTransactionHistoryFilters.swift in Sources */, 847DD8DC26034B99003DE053 /* LocalizableViewProtocol.swift in Sources */, @@ -16882,7 +16901,6 @@ FA37AE3C2859CCD8001DCA96 /* StakingUnbondConfirmParachainViewModelFactory.swift in Sources */, FA38C9C32761E77A005C5577 /* AccountViewModelFactory.swift in Sources */, FA8800562B2C610E000AE5EB /* ReefSubsquidHistory.swift in Sources */, - 8494D8632524753400614D8F /* WalletCopyCommand.swift in Sources */, 84F51060263AE530005D15AE /* TitleValueView.swift in Sources */, FACD427B2A5BE7C6009975AA /* RuntimeSnapshotReady.swift in Sources */, 84DB4E5C25EA71C100A6DF41 /* StringScaleMapper.swift in Sources */, @@ -16902,11 +16920,11 @@ F4871DE626D63DC000D27F23 /* AnalyticsRewardDetailsModel.swift in Sources */, AE6F7FE22685E812002BBC3E /* ValidatorListFilterWireframe.swift in Sources */, FAA013A928DA133E000A5230 /* StakeAmountView.swift in Sources */, - FA256A4D274CEB9100875A53 /* DummySelectedAssetView.swift in Sources */, FA00488F282CC7710032FF49 /* SelectValidatorsStartFlow.swift in Sources */, FAA0137828DA12E3000A5230 /* StakingRedeemConfirmation]PoolStrategy.swift in Sources */, FACD42B92A5BE90C009975AA /* InflationRewardCalculatorService.swift in Sources */, 849014DD24AA8F60008F705E /* MainTabBarViewFactory.swift in Sources */, + FA1D51D72BCFD445001353E7 /* LiquidityPools+ViewModel.swift in Sources */, FA9942712805524200D771E5 /* BalanceBuilder.swift in Sources */, AE7129C12608CAE7000AA3F5 /* NetworkStakingInfo.swift in Sources */, C6267B8D28BDF6A5001E31BF /* ChainAssetListAssembly.swift in Sources */, @@ -16923,20 +16941,17 @@ FA256985274CE5A500875A53 /* BalanceLocks+Sort.swift in Sources */, FAAA29312B8DCE590089AFE6 /* SimpleStorageRequestWorker.swift in Sources */, FA34EEE22B98723C0042E73E /* OnboardingViewLayout.swift in Sources */, - 8475AE1F258B7F4A00B058F3 /* TransferMetadataContext.swift in Sources */, C6264C302799DA4500FCA0DB /* WalletDetailsViewModel.swift in Sources */, 8463A71F25E39E07003B8160 /* StorageProviderSource.swift in Sources */, FA93A2F52834AB750021330F /* ValidatorInfoFlow.swift in Sources */, 8430AADC26022C58005B1066 /* NoStashState.swift in Sources */, 84F4A9182550331D000CF0A3 /* ExportOption.swift in Sources */, - FAB707C92BB68C4000A1131C /* LiquidityPoolListCell.swift in Sources */, FA2FC80C28B3807C00CC0A42 /* StakingPoolJoinConfirmInteractor.swift in Sources */, 84FD3DB12540C09800A234E3 /* TransactionHistoryMergeManager.swift in Sources */, 84038FEC26FFBA4D00C73F3F /* PriceLocalStorageSubscriber.swift in Sources */, 8473D4082657E9AD00B394B2 /* CrowdloanLastContribution.swift in Sources */, FA15BC0D2823ADCD0037C023 /* ParachainStakingLocalSubscriptionFactory.swift in Sources */, 07D05E6528EF0BE500B66C70 /* SelectValidatorsConfirmPoolViewModelState.swift in Sources */, - 84A2C90224E07E440020D3B7 /* CryptoType+Utils.swift in Sources */, 845FC93426B09EDB0021EC48 /* RoundedButton+Style.swift in Sources */, FABA163B2B0C9510001AF2F0 /* NetworkManagmentPresenter.swift in Sources */, FA4B929F2844D0C80003BCEF /* SnapKit.swift in Sources */, @@ -16947,7 +16962,6 @@ C63468DA28E5E3A3005CB1F1 /* TableView+Scrolling.swift in Sources */, AEA0C8BA268113F900F9666F /* YourValidatorList+RecommendedList.swift in Sources */, 84D33996262250B800130A89 /* String+Substrate.swift in Sources */, - 849014FF24AB69A4008F705E /* WalletContextFactory.swift in Sources */, FACD429A2A5BE811009975AA /* ChainSubstrateV2MigrationPolicy.swift in Sources */, 07DE95CB28A169A600E9C2CB /* AssetListSearchRouter.swift in Sources */, FA93A2EC2833B1000021330F /* RecommendedValidatorListRelaychainStrategy.swift in Sources */, @@ -17051,21 +17065,17 @@ FA3067202B621144006A0BA5 /* TokenLock.swift in Sources */, C6264C3E279E9D5000FCA0DB /* WalletTableViewCell.swift in Sources */, F40966F626B299FC008CD244 /* SubqueryStakeSource.swift in Sources */, - 84FB1F652526879200E0242B /* WalletSingleProviderIdFactory.swift in Sources */, - 8436EDDC25893FA0004D9E97 /* WalletBuyCommand.swift in Sources */, C69E89462B0C5F7D003663BA /* MainNftContainerStateHolder.swift in Sources */, FAA0133628DA12B6000A5230 /* StakingPoolMainProtocols.swift in Sources */, 849014FC24AA9939008F705E /* Charset+Mnemonic.swift in Sources */, AEE5FAFB26414C4F002B8FDC /* StakingRebondSetupProtocols.swift in Sources */, FAA013AF28DA134B000A5230 /* PendingRewardsCall.swift in Sources */, - 8460E12925422C5A00826F55 /* WalletCompoundDetailsViewModel.swift in Sources */, FA99423528053C5000D771E5 /* ChainAccountInfo.swift in Sources */, AEA0C8BE2681141700F9666F /* YourValidatorList+SelectedList.swift in Sources */, FA37AE4D28603C37001DCA96 /* StakingUpdatedEvent.swift in Sources */, FA7336DD2A0E3B880096A291 /* ResponseDecoderType.swift in Sources */, FA37AE3F2859DF94001DCA96 /* StakingRedeemParachainStrategy.swift in Sources */, 8430AAE926022F69005B1066 /* StashState.swift in Sources */, - 84644B3325685915004EAA4B /* WalletHistoryBackgroundView.swift in Sources */, 8428768424AE046300D91AD8 /* LanguageSelectionInteractor.swift in Sources */, FA34EEE42B98723C0042E73E /* BalanceLocksDetailStakingViewModel.swift in Sources */, F4D6FF4126B3E4C2002313AF /* AnalyticsRewardsWireframe.swift in Sources */, @@ -17073,20 +17083,21 @@ F4A12BF7260B61E900392C33 /* StakingManageOption.swift in Sources */, FAD428F92A86567F001D6A16 /* BackupPasswordInteractor.swift in Sources */, 843A2C7726A86FD000266F53 /* TitleStatusView.swift in Sources */, - 84FAB07425436D6300319F74 /* ContactsConstants.swift in Sources */, FA7254332AC2E48500EC47A6 /* RLPBigInt.swift in Sources */, FA8122E329CC374B00F43560 /* AssetVisibility.swift in Sources */, FA99426C28053CFA00D771E5 /* WalletDetailsFlow.swift in Sources */, + FAC6CD902BA7FCA70013A17E /* AssetTransactionFee.swift in Sources */, FA9A8F262A72579D008FA99F /* AlchemySortOrder.swift in Sources */, FAFFAEAC29AC90E50074AF1F /* SubqueryPayoutValidatorsForNominatorFactory.swift in Sources */, FA8800632B31A04C000AE5EB /* StakingAccountResolverAssembly.swift in Sources */, - 8409C268252218FF0049B5C8 /* WalletFearlessDefinitionFactory.swift in Sources */, FAA013B328DA1355000A5230 /* StakingPoolRewards.swift in Sources */, FAAA292A2B8DCE3E0089AFE6 /* SingleStorageResponseValueExtractor.swift in Sources */, FAA0139828DA1312000A5230 /* StakingBondMoreConfirmationPoolStrategy.swift in Sources */, 84CFF1E526526FBC00DB7CF7 /* StakingBondMoreViewController.swift in Sources */, + FA1D02012BBE713D005B7071 /* LiquidityPoolsListViewController.swift in Sources */, 84E1CD11260DCD44001E81B5 /* SwitchAccount+AccountImportWireframe.swift in Sources */, 8401AEC72642A71D000B03E3 /* StakingRebondConfirmationPresenter.swift in Sources */, + FAC6CDA12BA80CB10013A17E /* WalletTransactionType.swift in Sources */, FA74359F29C073790085A47E /* ChainSettingsMapper.swift in Sources */, 844CB56226F943AD00396E13 /* WalletLocalSubscriptionFactory.swift in Sources */, 84893C0324DA8641008F6A3F /* AccountCreationRequest.swift in Sources */, @@ -17122,12 +17133,14 @@ FA256A26274CE7D600875A53 /* MoonbeamMakeSignatureData.swift in Sources */, C6CA20352B05E900001503C2 /* NftFiltersViewController.swift in Sources */, F406B0E626299E34004FDCCC /* ErrorStateView.swift in Sources */, + FAC6CD882BA7F9F70013A17E /* FeeViewModel.swift in Sources */, 07B6BC7A28B78E8000621864 /* SheetAlertPresentableStyle.swift in Sources */, FADBA5F42B5FE5C900CFCF30 /* ChainAccountPresenter.swift in Sources */, 8467FCFC24E5C3BD005D486C /* URLHandlingService.swift in Sources */, F40966BA26B297D6008CD244 /* AnalyticsSummaryRewardViewModel.swift in Sources */, FAA0133D28DA12B6000A5230 /* StakingPoolManagementPresenter.swift in Sources */, FAAA29422B8DCED90089AFE6 /* JSONRPCListWorker.swift in Sources */, + FAC6CD922BA8022D0013A17E /* WalletAsset.swift in Sources */, 849013DB24A927E2008F705E /* Logger.swift in Sources */, 84443BA226C123F100C33B5D /* Data+Random.swift in Sources */, 07DE95CA28A169A600E9C2CB /* AssetListSearchAssembly.swift in Sources */, @@ -17180,7 +17193,6 @@ 8428769324AE046300D91AD8 /* AboutWireframe.swift in Sources */, FA6261F22AC2A535005D3D95 /* MIME+PathExtensions.swift in Sources */, 07BF3D9D2B3D8C9B0046ABF4 /* AssetTransactionData+OklinkHistory.swift in Sources */, - 84ACEBFF261E6C7C00AAE665 /* WalletHistoryFilterViewLayout.swift in Sources */, FA86442527671E0E00956D8E /* WalletTransactionHistoryViewModelFactory.swift in Sources */, 84754CA22513DB8800854599 /* EmptyAccountIcon.swift in Sources */, FAFFAEA629AC851A0074AF1F /* Bool + Inverted.swift in Sources */, @@ -17207,7 +17219,6 @@ 8425EA9025EA7E5800C307C9 /* ElectedValidatorInfo.swift in Sources */, FA62623D2AC2E35A005D3D95 /* WalletConnectConfirmationViewModel.swift in Sources */, 846AF8442525BE0100868F37 /* Price.swift in Sources */, - 84CD357325264F640081BC0B /* WalletTotalPriceViewModel.swift in Sources */, FA72543B2AC2E48500EC47A6 /* ABITypeParser.swift in Sources */, 84FACCD925F8C22A00F32ED4 /* BigInt+Hex.swift in Sources */, F409672626B29B04008CD244 /* UIScrollView+ScrollToPage.swift in Sources */, @@ -17274,7 +17285,6 @@ FA072C25277C0FA900731718 /* ApplicationSettingsPresentable.swift in Sources */, F462B35C260C86880005AB01 /* ViewHolder.swift in Sources */, 84C515FB26D84F8C000DBA45 /* AccountImportWrapper.swift in Sources */, - FACE6C232BB6C37900643CEF /* LiquidityPoolListCellModel.swift in Sources */, 849014DE24AA8F60008F705E /* MainTabBarInteractor.swift in Sources */, 849ABE7226280F3800011A2A /* ControllersReducer.swift in Sources */, FAD4290E2A86567F001D6A16 /* BannersViewLayout.swift in Sources */, @@ -17381,7 +17391,6 @@ FAA013B028DA134B000A5230 /* PoolBondMoreCall.swift in Sources */, FA2E9B93279FE35E0023FAD2 /* ExtrinsicOptionsPresentable.swift in Sources */, 072EB84A28E2A2A1007E70FF /* StakingPoolCreateConfirmViewModel.swift in Sources */, - 8494D860252470F400614D8F /* WalletFearlessFormDefining.swift in Sources */, FA00489B282CCFDC0032FF49 /* SelectValidatorsStartRelaychainViewModelFactory.swift in Sources */, FA25698B274CE61000875A53 /* MultiSignature+CryptoType.swift in Sources */, C640415D28F507E900845780 /* TriangularedTextField.swift in Sources */, @@ -17399,13 +17408,16 @@ FAD429282A865680001D6A16 /* BackupWalletInteractor.swift in Sources */, 0702B31829701864003519F5 /* WalletViewModelObserverContainer.swift in Sources */, C600C4D52802B87100111316 /* UsernameSetupViewLayout.swift in Sources */, + FAC6CD982BA807D30013A17E /* AccessoryView.swift in Sources */, 07DFA46D289B8D8E0035A8AB /* BaseEducationStoriesView.swift in Sources */, 84FFE505261290830054EA63 /* NetworkInfoView.swift in Sources */, + FAC6CD8C2BA7FA6C0013A17E /* AmountDecimal.swift in Sources */, 8490144F24A93E2E008F705E /* UIImage+Drawing.swift in Sources */, FAB16A342A9C9BD000E71F43 /* NftCellViewModel.swift in Sources */, 8444D13F267133CF00AF6D8C /* CrowdloanAddMemo.swift in Sources */, 84EBC54324F656D100459D15 /* SortDescriptor+Storage.swift in Sources */, 845532D02684690D00C2645D /* ParachainSlotLease.swift in Sources */, + FA22228F2BD237CF0031DE04 /* SoraSubqueryPriceFetcher.swift in Sources */, 84CD82B3263C30BC001A6F01 /* SubstrateProviderSubscriptionHandler.swift in Sources */, 84DB4E1E25E93B1700A6DF41 /* Identity.swift in Sources */, F47F5A822626FDB9009BCFF4 /* StakingPayoutViewModel.swift in Sources */, @@ -17471,7 +17483,6 @@ FAFFAE3829AC84180074AF1F /* ParachainSubsquidHistoryOperationFactory.swift in Sources */, FA6262372AC2E35A005D3D95 /* WalletConnectConfirmationViewController.swift in Sources */, C67E781B27B3AD510053346B /* CheckPincodeViewLayout.swift in Sources */, - 84C91FAF261E7CDD002796B9 /* WalletHistoryFilterEditor.swift in Sources */, FAD646DC284F6C90007CCB92 /* StakingUnbondSetupRelaychainViewModelFactory.swift in Sources */, FA286B1A2A3043DB008BD527 /* CrossChainViewLayout.swift in Sources */, FAFFAE3629AC84180074AF1F /* ParachainHistoryOperationFactory.swift in Sources */, @@ -17479,7 +17490,6 @@ 84113B91255B2CA0009BD21A /* MainTransitionHelper.swift in Sources */, 8428768524AE046300D91AD8 /* LanguageSelectionProtocols.swift in Sources */, AEA0C8A4267B6B1900F9666F /* SelectedValidatorListProtocols.swift in Sources */, - 8494D8552524633300614D8F /* WalletTransferTokenView.swift in Sources */, FA2FC81528B3807D00CC0A42 /* StakingPoolStartInteractor.swift in Sources */, 8490142E24A935FE008F705E /* LoadableViewProtocol.swift in Sources */, FADBA5F32B5FE36200CFCF30 /* ChainAccountViewMode.swift in Sources */, @@ -17490,8 +17500,8 @@ FA3F5B67281BAF5200BEF03B /* StakingAmountRelaychainStrategy.swift in Sources */, FA904D8E2B0488E000DAEC2D /* AssetNetworksFilter.swift in Sources */, 843939902636F88F0087658D /* YourValidatorsModel.swift in Sources */, - 8490152324ABC77E008F705E /* WalletAssetViewModel.swift in Sources */, 845C407D2702812E00BFA50B /* StakingAccountUpdatingService.swift in Sources */, + FAC6CD9F2BA80AB70013A17E /* WalletLanguage.swift in Sources */, 84DA3B1924C8200E00B5E27F /* ConnectionItem+Default.swift in Sources */, FAD429072A86567F001D6A16 /* BackupCreatePasswordProtocols.swift in Sources */, 84D331AF2519E8080078D044 /* TriangularedView+Style.swift in Sources */, @@ -17585,11 +17595,9 @@ FA93A2F02833B11A0021330F /* RecommendedValidatorListParachainViewModelFactory.swift in Sources */, 84DBEA42265E80DD00FDF73C /* LearnMoreViewModel.swift in Sources */, FAF5E9D127E46D6A005A3448 /* AppVersionObserver.swift in Sources */, - 849E2351254AF44200B1F6D4 /* InvoiceScanLocalSearchEngine.swift in Sources */, 07DFA46E289B8D8E0035A8AB /* EducationStoriesProgressView.swift in Sources */, FA5137B929AC76EB00560EBA /* GiantsquidHistoryOperationFactory.swift in Sources */, 84DD5F21263CB6BE00425ACF /* UnbondCall.swift in Sources */, - AEF50716262359DC0098574D /* WalletSelectPurchaseProviderCommand.swift in Sources */, FA4B92B32844D0E60003BCEF /* SelectCurrencyTableCell.swift in Sources */, 848DE76226D62AFE0045CD29 /* PersistentStoreCoordinator+Extensions.swift in Sources */, 9942034DCB680824831B0AC1 /* AccountCreatePresenter.swift in Sources */, @@ -17600,7 +17608,6 @@ FAD4291E2A86567F001D6A16 /* WalletNameInteractor.swift in Sources */, FAA0133E28DA12B6000A5230 /* StakingPoolManagementViewLayout.swift in Sources */, FA7254322AC2E48500EC47A6 /* Address.swift in Sources */, - 846AF8482525E23C00868F37 /* WalletTotalPriceCell.swift in Sources */, 84585A31251C0B9300390F7A /* NetworkInfoMode.swift in Sources */, 847C962825534134002D288F /* ExportGenericProtocols.swift in Sources */, FA86443C2767BED800956D8E /* BaseDataProviderFactory.swift in Sources */, @@ -17615,11 +17622,12 @@ 4448B591D4A193DBC9E2E3BF /* AccountCreateInteractor.swift in Sources */, FA9A8F092A6FB8F9008FA99F /* AssetTransactionData+EtherscanHistory.swift in Sources */, FA93A3062834FBF60021330F /* ValidatorSearchFlow.swift in Sources */, - 84DF21A92535AA8F005454AE /* ExistentialDepositInfoCommand.swift in Sources */, 84E6D57C262E2CE8000EA3F5 /* OperationCombiningService.swift in Sources */, + FA1D01FA2BBE713D005B7071 /* LiquidityPoolListViewModel.swift in Sources */, 0726FFAD2AC4399C00336D76 /* WalletConnectPolkadotParser.swift in Sources */, + FA2222942BD2726F0031DE04 /* SkeletonLabel.swift in Sources */, + FA22228D2BD237910031DE04 /* SubqueryPriceFetcher.swift in Sources */, FAED6F4427DA19C70051B337 /* AccountInfoSubscriptionAdapter.swift in Sources */, - 8460E1252542293A00826F55 /* WalletCompoundDetailsView.swift in Sources */, FAD646CC284DFE5A007CCB92 /* StakingBondMoreRelaychainViewModelState.swift in Sources */, 846C372E26B199D10098F303 /* StakingDurationOperationFactory.swift in Sources */, 8475AE39258B958300B058F3 /* ErrorContent+Wallet.swift in Sources */, @@ -17631,6 +17639,7 @@ AE805FC526B3DF8B00007CE9 /* ValidatorInfoInteractorBase.swift in Sources */, FAF9C29E2AAEED9600A61D21 /* TitleCopyableValueView.swift in Sources */, FA5137BA29AC76EB00560EBA /* SubsquidHistoryOperationFactory.swift in Sources */, + FACE6C632BBAC3B100643CEF /* SubstrateDataModel.xcdatamodeld in Sources */, F4D96B712637FE8600B23D3D /* StakingBalanceWidgetViewModel.swift in Sources */, FA37AE332859C171001DCA96 /* ScheduleDelegatorBondLessCall.swift in Sources */, AEE5FB0526415E5D002B8FDC /* StakingRebondSetupViewFactory.swift in Sources */, @@ -17747,11 +17756,9 @@ 42B79A8D0D9540C1D97D991C /* AccountConfirmWireframe.swift in Sources */, 0716C83C28853ACA004C8CB1 /* WalletBalanceSubscriptionAdapter.swift in Sources */, FAD429112A86567F001D6A16 /* CollectionViewDataSource.swift in Sources */, - 84DF21B12536DDC1005454AE /* TransferConfirmCommand.swift in Sources */, FA93A2FF2834AF9E0021330F /* ValidatorInfoParachainViewModelState.swift in Sources */, FA72543D2AC2E48500EC47A6 /* ABI.swift in Sources */, AEA0C8BC2681140700F9666F /* YourValidatorList+CustomList.swift in Sources */, - 846A2C492527490700731018 /* WalletSelectAccountCommand.swift in Sources */, 94B0F0C84AF74B3CD7223C3A /* AccountConfirmPresenter.swift in Sources */, 8468B87224F63D3A00B76BC6 /* AddAccount+AccountCreateWireframe.swift in Sources */, C65A6594288E5E0500679D65 /* NSLockExtension.swift in Sources */, @@ -17759,7 +17766,6 @@ 84BAFCD626AF64CB00871E86 /* SelectValidatorsViewLayout.swift in Sources */, 84B018AC26E01A4100C75E28 /* StakingStateView.swift in Sources */, FAFFAE8829AC84B10074AF1F /* SubqueryExtrinsic.swift in Sources */, - 849E2332254AEFB400B1F6D4 /* InvoiceScanConfigurator.swift in Sources */, 8472C5B2265CF9C500E2481B /* StakingRewardDestConfirmInteractor.swift in Sources */, FACD42B42A5BE8E1009975AA /* CheckPincodeWireframe.swift in Sources */, 846CA77A27099B1E0011124C /* StakingAnalyticsLocalSubscriptionFactory.swift in Sources */, @@ -17774,6 +17780,7 @@ FA34EED52B98723C0042E73E /* OnboardingStartProtocols.swift in Sources */, AE9EF264260A82B80026910A /* StoriesWireframe.swift in Sources */, 848EAEB02659310A00676CEA /* CrowdloanStatus.swift in Sources */, + FA1D020D2BBE7690005B7071 /* UserLiquidityPoolsListInteractor.swift in Sources */, 841185E7263F3B6D00E8A8B6 /* HintView.swift in Sources */, 8494D87C252537E500614D8F /* SubscanError.swift in Sources */, DAEE468553039B3600F64A0E /* AccountManagementWireframe.swift in Sources */, @@ -17828,16 +17835,18 @@ C67E781927B3AC350053346B /* CheckPincodeViewController.swift in Sources */, FA5137B029AC6F2F00560EBA /* PolkaswapDisclaimerProtocols.swift in Sources */, FACD42972A5BE811009975AA /* SettingsMigrator.swift in Sources */, + FA1D01FF2BBE713D005B7071 /* LiquidityPoolsListAssembly.swift in Sources */, 06590486EED4050BADDD32C5 /* AccountManagementPresenter.swift in Sources */, 84DED3EF26661CC600A153BB /* CrowdloanFlow.swift in Sources */, + FA1D01FB2BBE713D005B7071 /* LiquidityPoolsListViewLayout.swift in Sources */, 21322B4A297840739C389F17 /* AccountManagementInteractor.swift in Sources */, 84452F4E25D5BB1C00F47EC5 /* RuntimeCoderFactory.swift in Sources */, + FA1D51DC2BCFE38D001353E7 /* SubqueryFiatInfoOperation.swift in Sources */, 84452A6025D037AE00F47EC5 /* ChainStorage+Decodable.swift in Sources */, F4F65C3D26D8B9DD002EE838 /* FWYAxisChartFormatter.swift in Sources */, 847C963525534E41002D288F /* UIFactory.swift in Sources */, 07BFF8AA2AD666CE005A5C58 /* AutoNamespacesError+Extension.swift in Sources */, F40966D026B297D7008CD244 /* AnalyticsStakeInteractor.swift in Sources */, - 84585A34251CE2E300390F7A /* ContactsLocalSearchEngine.swift in Sources */, 8416A2D8265A99DE0052EE89 /* CrowdloanViewConstants.swift in Sources */, F3D2AC37709EAF088A594B73 /* AccountManagementViewController.swift in Sources */, 84CA68D526BE9E62003B9453 /* ChainRegistry.swift in Sources */, @@ -17860,14 +17869,11 @@ FAD429192A86567F001D6A16 /* BackupSelectWalletPresenter.swift in Sources */, 8488ECD7258CDCBC004591CC /* PurchaseCompletionHandler.swift in Sources */, F47BBD562630B9EF0087DA11 /* StakingBalanceInteractor.swift in Sources */, - 84FAB06B2542F2C700319F74 /* ContactsViewModelFactory.swift in Sources */, FA0066E92935D07D0068FC61 /* RecommendedValidatorListPoolStrategy.swift in Sources */, - FA256A51274CEB9800875A53 /* FeePriceViewModel.swift in Sources */, FA34EE972B9871200042E73E /* OnboardingPageInfo.swift in Sources */, 84FAB0782543791A00319F74 /* ContactContext.swift in Sources */, FA2FC81B28B3807D00CC0A42 /* StakingPoolStartViewLayout.swift in Sources */, 8460516D25536C4800A1F0B4 /* ExportOption+ViewModel.swift in Sources */, - 84969734251CE9CD00C39524 /* ContactsConfigurator.swift in Sources */, 843461CF26E25AD400DCE0CD /* SubscanHistoryItem+Wallet.swift in Sources */, FA286B192A3043DB008BD527 /* CrossChainPresenter.swift in Sources */, F458D3982642911B0055CB75 /* ControllerAccountViewModel.swift in Sources */, @@ -17880,7 +17886,6 @@ FAA0139028DA1303000A5230 /* StakingPayoutConfirmationPoolViewModelState.swift in Sources */, FA8800662B31A316000AE5EB /* StakingAccountSubscription.swift in Sources */, 844CB56826F9BF9800396E13 /* CrowdloanLocalSubscriptionFactory.swift in Sources */, - 8409C26A2522192B0049B5C8 /* WalletFearlessDefinition.swift in Sources */, FA256A33274CE7D600875A53 /* MoonbeamJSONDecoder.swift in Sources */, 84F9D4CF2664CCA500F7AAD2 /* CommonInputView.swift in Sources */, FA256A25274CE7D600875A53 /* MoonbeamAgreeRemarkData.swift in Sources */, @@ -17889,11 +17894,9 @@ 070CDD6D2ACAACB900F3F20A /* EthereumRemoteBalanceFetching.swift in Sources */, FAAA293F2B8DCED90089AFE6 /* MapKeyEncodingWorker.swift in Sources */, FAA086D028470B3200CC2F33 /* SelectValidatorsConfirmParachainViewModelState.swift in Sources */, - 84A2CD5325685A4100A9FB0D /* WalletHistoryViewFactoryOverriding.swift in Sources */, FA62625B2AC2E35A005D3D95 /* MultiSelectNetworksViewModel.swift in Sources */, FAB707672BB3C1A400A1131C /* AssetAccountInfo.swift in Sources */, FAD429182A86567F001D6A16 /* BackupSelectWalletTableCell.swift in Sources */, - FAB707C72BB67F7000A1131C /* TokenPairsIconViewModel.swift in Sources */, FA34EEDD2B98723C0042E73E /* OnboardingPageCell.swift in Sources */, FAE9EBA7288ABBFC009390B6 /* AnalyticsRewardsRelaychainViewModelState.swift in Sources */, 07F2B76328ACDA7800280C38 /* RuntimeHotBootSnapshotFactory.swift in Sources */, @@ -17907,6 +17910,7 @@ 845B822926F0BB6700D25C72 /* CrowdloanChainSettings.swift in Sources */, AEE5FB072641669B002B8FDC /* StakingRebondSetupLayout.swift in Sources */, FA2FC85D28B38A1400CC0A42 /* StakingAssetSelectionPresenter.swift in Sources */, + FA1D01FD2BBE713D005B7071 /* LiquidityPoolsListProtocols.swift in Sources */, FA2FC81428B3807D00CC0A42 /* StakingPoolStartViewModel.swift in Sources */, 847119C0262EF3BD00716580 /* PayoutValidatorsFactoryProtocol.swift in Sources */, 075FC63528D9AB1600E25263 /* CommonInputViewV2.swift in Sources */, @@ -17921,7 +17925,6 @@ FA37AE2F2859BED8001DCA96 /* StakingUnbondSetupParachainStrategy.swift in Sources */, FAA013AE28DA134B000A5230 /* PoolUnbondCall.swift in Sources */, 84DED3F726662CF200A153BB /* TitleValueSelectionControl.swift in Sources */, - 84BEF601258938C1002A22E2 /* WalletActionsViewModel.swift in Sources */, FAADC1BC2926597400DA9903 /* ScanQRRouter.swift in Sources */, 076D9D6629507B39002762E3 /* PolkaswapSettingsFactory.swift in Sources */, 07B018D328C714B300E05510 /* ScamServiceOperationFactory.swift in Sources */, @@ -17951,7 +17954,6 @@ 2E57C70B27E9EC0F00AF075A /* ProfileViewState.swift in Sources */, 843A2C6F26A8548B00266F53 /* RowView.swift in Sources */, 84EBAB06265DC24C0015E446 /* CrowdloanContributionViewModelFactory.swift in Sources */, - FABA162B2B0C94DB001AF2F0 /* UserDataModel.xcdatamodeld in Sources */, FA5AE96427B1326400B2564E /* ChainsUpdatedEvent.swift in Sources */, AE7E5BFE2615D23A006D5637 /* ProgressView + Manage.swift in Sources */, 84CA68DF26BEAA0F003B9453 /* ChainModelMapper.swift in Sources */, @@ -17966,7 +17968,6 @@ C6992F10280836FA003BC769 /* AccountConfirmFlow.swift in Sources */, 845CB6FA26276326005F798B /* LongrunOperation.swift in Sources */, 84FB1F6D2526987D00E0242B /* TransactionHistoryContext.swift in Sources */, - 84CD357525264FA30081BC0B /* WalletAccountListConstants.swift in Sources */, AE9EF269260A82C30026910A /* StoriesProtocols.swift in Sources */, F47BBD852631888C0087DA11 /* StakingBalanceActionsWidgetView.swift in Sources */, FA2DF9A82A8F49B10047F440 /* NftListCellModel.swift in Sources */, @@ -18002,6 +18003,7 @@ FAD428F42A86567F001D6A16 /* BackupPasswordPresenter.swift in Sources */, FA72543A2AC2E48500EC47A6 /* ABIElements.swift in Sources */, 848919DB26FB663D004DBAD5 /* CrowdloansChainViewModel.swift in Sources */, + FAC6CDB12BA821B00013A17E /* WalletImageViewModelProtocol.swift in Sources */, 8430AAF12602306A005B1066 /* BondedState.swift in Sources */, FAA086D628475AF300CC2F33 /* ParachainStakingDelegatorState.swift in Sources */, 84C3F77B2601F08B00D47501 /* NominationViewModel.swift in Sources */, @@ -18063,8 +18065,10 @@ 84CFF1EC26526FBC00DB7CF7 /* StakingBondMoreConfirmRelaychainViewModelFactory.swift in Sources */, FAC0BBB2291D074500E6F106 /* RuntimeCallPath.swift in Sources */, FA6C176229935DC700A55254 /* SubsquidRewardOperationFactory.swift in Sources */, + FA1D01F92BBE713D005B7071 /* LiquidityPoolListCellModel.swift in Sources */, 84038FF626FFDF4E00C73F3F /* CrowdloanSharedState.swift in Sources */, FAAA29402B8DCED90089AFE6 /* NMapKeyEncodingWorker.swift in Sources */, + FA1D01FC2BBE713D005B7071 /* LiquidityPoolsListRouter.swift in Sources */, 054C4BCDEC29ED5F74A36E8B /* ExportMnemonicPresenter.swift in Sources */, 39218CF5AA701518BD3B0103 /* ExportMnemonicInteractor.swift in Sources */, F4F22967260DBC7200ACFDB8 /* StakingPayoutStatusTableCell.swift in Sources */, @@ -18099,7 +18103,6 @@ ABA3D873BBECB7F4BD670872 /* ExportSeedPresenter.swift in Sources */, FA5DD0E3278EFE2500967047 /* WalletTransactionHistoryTableSectionHeader.swift in Sources */, FAA0134728DA12CD000A5230 /* StakingUnbondSetupPoolStrategy.swift in Sources */, - FA256A52274CEB9800875A53 /* RichAmountDisplayViewModel.swift in Sources */, FA74359B29C073650085A47E /* ChainSettings.swift in Sources */, FA6262402AC2E35A005D3D95 /* WalletConnectSessionAssembly.swift in Sources */, 9A6A55297F41DAE45071BF57 /* ExportSeedInteractor.swift in Sources */, @@ -18178,6 +18181,7 @@ FA9A8F222A72573C008FA99F /* AlchemyHistoryRequest.swift in Sources */, EC978E6C4FBF39BE9ED10C86 /* SelectValidatorsStartWireframe.swift in Sources */, FA2FC81828B3807D00CC0A42 /* StakingPoolStartRouter.swift in Sources */, + FAC6CD942BA802840013A17E /* NumberFormatterFactoryProtocol.swift in Sources */, 0713098128C6F7BB002B17D0 /* CDScamInfo+CoreDataCodable.swift in Sources */, FA93A3162836542D0021330F /* ValidatorListFilterRelaychainViewModelState.swift in Sources */, FA8644512768A13400956D8E /* TwoLabelView.swift in Sources */, @@ -18225,6 +18229,7 @@ FA93A2EA2833B0F90021330F /* RecommendedValidatorListRelaychainViewModelFactory.swift in Sources */, 0678271BE1BA5BBC084F478A /* RecommendedValidatorListWireframe.swift in Sources */, F441BE0E263984DD0096B67B /* BondExtraCall.swift in Sources */, + FA8810D52BDCD19D0084CC4B /* UserLiquidityPoolsListViewModelFactory.swift in Sources */, BA7AEE82627CFC0AFD69B299 /* RecommendedValidatorListPresenter.swift in Sources */, FAAA29492B8DCF350089AFE6 /* AsyncStorageRequestFactoryDefault.swift in Sources */, 8472C5B3265CF9C500E2481B /* StakingRewardDestConfirmViewController.swift in Sources */, @@ -18237,11 +18242,11 @@ FA4B928F284493C60003BCEF /* DelegateCall.swift in Sources */, 070CDD702ACACEDA00F3F20A /* QRMatcherProtocol.swift in Sources */, 2918DCAEB91F8276446873C8 /* StakingRewardPayoutsWireframe.swift in Sources */, - FA256A53274CEB9800875A53 /* RichAmountInputViewModel.swift in Sources */, AB5E2A2B4CC823E6F6515ADD /* StakingRewardPayoutsPresenter.swift in Sources */, 84100F3C26A60E4C00A5054E /* YourValidatorListWarningSectionView.swift in Sources */, FAADC1A729261F7000DA9903 /* PoolRolesConfirmViewModel.swift in Sources */, FAD429292A865680001D6A16 /* BackupWalletViewController.swift in Sources */, + FA054A9C2BCD1FAF007B8F6D /* AvailableLiquidityPoolsListViewModelFactory.swift in Sources */, FA2E9BB527A133A20023FAD2 /* SwitchFilterItem.swift in Sources */, FA2FC80928B3807C00CC0A42 /* StakingPoolJoinConfigInteractor.swift in Sources */, C648FFC828DC43A70072951B /* EmptyView.swift in Sources */, @@ -18292,20 +18297,14 @@ 849ABE772628103200011A2A /* ControllersListReducer.swift in Sources */, FA99425628053C8800D771E5 /* SelectableExportAccountTableCell.swift in Sources */, 9565BEB636E6D386B0C0FBE5 /* StakingPayoutConfirmationViewFactory.swift in Sources */, - FAB707C52BB6792700A1131C /* TokenPairIconsView.swift in Sources */, FA37AE202858838A001DCA96 /* ParachainStakingScheduledRequest.swift in Sources */, FA6262512AC2E35A005D3D95 /* WalletConnectActiveSessionsTableCell.swift in Sources */, FAA086D82848AB8600CC2F33 /* YourRewardDestinationViewModel.swift in Sources */, FA2569C2274CE74100875A53 /* BottomSheetInfoBalanceCell.swift in Sources */, - 6F0CFDAB9D0C35075BD74A77 /* WalletHistoryFilterProtocols.swift in Sources */, FA6262542AC2E35A005D3D95 /* MultiSelectNetworksViewLayout.swift in Sources */, - 60461B20A5DA9E9E3AF0BB84 /* WalletHistoryFilterWireframe.swift in Sources */, 84CFF1E326526FBC00DB7CF7 /* StakingBondMoreProtocols.swift in Sources */, FA93A2FB2834AF800021330F /* ValidatorInfoRelaychainViewModelState.swift in Sources */, - 5FE687B860FC10AB08518A6E /* WalletHistoryFilterPresenter.swift in Sources */, FA99425428053C8800D771E5 /* SelectExportAccountViewModelFactory.swift in Sources */, - 640A79BD1335394818E70366 /* WalletHistoryFilterViewController.swift in Sources */, - DD090C2ED91726FF7779F6C7 /* WalletHistoryFilterViewFactory.swift in Sources */, FAA0133B28DA12B6000A5230 /* StakingPoolManagementRouter.swift in Sources */, AE6F7FE82685F2F0002BBC3E /* ValidatorListFilterViewModelFactory.swift in Sources */, FAADC1A529261F7000DA9903 /* PoolRolesConfirmProtocols.swift in Sources */, @@ -18346,17 +18345,18 @@ 072EB84828E2A267007E70FF /* StakingPoolCreateConfirmViewModelFactory.swift in Sources */, FA88006C2B31A46D000AE5EB /* StakingAccountSubscriptionAssembly.swift in Sources */, FA2FC7CB28B3805400CC0A42 /* CreatePoolCall.swift in Sources */, + FAC6CD8E2BA7FBD30013A17E /* WalletHistoryRequest.swift in Sources */, FA7A4C7E2A1F937A0051FB4D /* SoraRewardCalculatorEngine.swift in Sources */, C63468E628F37912005CB1F1 /* CDContact + CoreDataCodable.swift in Sources */, F40966C926B297D6008CD244 /* AnalyticsStakeViewModelFactory.swift in Sources */, 84CFF1F026526FBC00DB7CF7 /* StakingBondMoreConfirmationViewLayout.swift in Sources */, F4433D6F26C1696A0002A91E /* AnalyticsValidatorsCell.swift in Sources */, - FA9A8F3E2A776DFD008FA99F /* SubstrateDataModel.xcdatamodeld in Sources */, C600C4D728053DF500111316 /* UsernameSetupViewController.swift in Sources */, F83EA1F4ECE14C390C0B287F /* StakingUnbondSetupInteractor.swift in Sources */, F418E891264D318C00699085 /* AlertsView.swift in Sources */, FAADC19929261F6400DA9903 /* NominationPoolsUpdateRolesCall.swift in Sources */, CCF5A7CED175D5E43B2C9971 /* StakingUnbondSetupViewController.swift in Sources */, + FA1D02002BBE713D005B7071 /* LiquidityPoolListCell.swift in Sources */, FAE39AF12A9DBDDA0011A9D6 /* NftCollectionViewModel.swift in Sources */, 57E20F0723C4748D576C4882 /* StakingUnbondSetupViewFactory.swift in Sources */, F40966CD26B297D6008CD244 /* AnalyticsStakePresenter.swift in Sources */, @@ -18484,6 +18484,7 @@ FA7254422AC2E48500EC47A6 /* SignTypedData.swift in Sources */, 921E4891E85C0DC6FDD8A0D0 /* CrowdloanContributionConfirmInteractor.swift in Sources */, 5C796EF8ED29F564B5D1126B /* CrowdloanContributionConfirmViewController.swift in Sources */, + FAC6CDAB2BA819540013A17E /* SearchData.swift in Sources */, FAA0132F28DA12B6000A5230 /* StakingPoolMainViewModelFactory.swift in Sources */, 2793D406FD618A892D54EA84 /* CrowdloanContributionConfirmViewLayout.swift in Sources */, 90A3F46EF181DC2B821CC80C /* CrowdloanContributionConfirmViewFactory.swift in Sources */, @@ -18526,6 +18527,7 @@ 237AD34CD1C2778834D7B330 /* AnalyticsValidatorsViewFactory.swift in Sources */, 070CDD772ACAE05D00F3F20A /* CexQRInfo.swift in Sources */, FA072C16277B023D00731718 /* QRExtractionService.swift in Sources */, + FA2222962BD272A30031DE04 /* SkeletonLoadableView.swift in Sources */, FA37AE2A2859BA1D001DCA96 /* StakingBondMoreConfirmationParachainStrategy.swift in Sources */, 31E260D462BF33CFCDFEBA6C /* AnalyticsRewardDetailsProtocols.swift in Sources */, FA6262482AC2E35A005D3D95 /* WalletConnectActiveSessionsViewController.swift in Sources */, @@ -18542,6 +18544,7 @@ 69EF1DC4093AC9AF06D71CF4 /* AnalyticsRewardDetailsViewFactory.swift in Sources */, 93434E8E407A6C63D8862A21 /* AssetSelectionProtocols.swift in Sources */, CDB78A5A733E4A4F1A2C48C8 /* AssetSelectionWireframe.swift in Sources */, + FA054A9A2BCD1FA3007B8F6D /* AvailableLiquidityPoolsListPresenter.swift in Sources */, 2FCB062A2D873BD72B795DB3 /* AssetSelectionPresenter.swift in Sources */, BEA539EE97A287868FD8BE46 /* AssetSelectionViewFactory.swift in Sources */, 07DFA44B289918E10035A8AB /* ModalSheetBlurPresentationController.swift in Sources */, @@ -18561,12 +18564,14 @@ FA6262442AC2E35A005D3D95 /* WalletConnectSessionViewLayout.swift in Sources */, 06197BBE4299DC971C42DB92 /* WalletSendConfirmPresenter.swift in Sources */, BDE80F08EBEE3B0C95598EA8 /* WalletSendConfirmInteractor.swift in Sources */, + FAC6CDAD2BA81D680013A17E /* FeeViewProtocol.swift in Sources */, FA72542E2AC2E48500EC47A6 /* WalletConnectSignDecision.swift in Sources */, EA16E259F0B0C1D3A6A1902A /* WalletSendConfirmViewController.swift in Sources */, 775C4C720600DAE242C67192 /* WalletSendConfirmViewLayout.swift in Sources */, FAD429012A86567F001D6A16 /* BackupCreatePasswordViewLayout.swift in Sources */, F4CBA064CDCF0F6EEFE1DDA1 /* WalletSendConfirmViewFactory.swift in Sources */, 3A7BF8FD79B7130241222C35 /* WalletTransactionHistoryProtocols.swift in Sources */, + FAC6CD9D2BA8097C0013A17E /* L10n.swift in Sources */, FAAA291D2B8DBFEE0089AFE6 /* StorageRequestWorkerBuilder.swift in Sources */, FA93A2E82833B0F10021330F /* RecommendedValidatorListRelaychainViewModelState.swift in Sources */, FAD429322A865696001D6A16 /* CheckboxButton.swift in Sources */, @@ -18715,6 +18720,7 @@ FAFFAE3E29AC84480074AF1F /* TappedLabel.swift in Sources */, 1CBFE5F285223EA5D5300C49 /* WalletMainContainerProtocols.swift in Sources */, 5967C80AC5F477E589DA4793 /* WalletMainContainerRouter.swift in Sources */, + FAC6CD9A2BA809160013A17E /* ReceiveInfo.swift in Sources */, 4D822D169784790EBF173EEE /* WalletMainContainerPresenter.swift in Sources */, CFAC59D34B15A2FAC4CD8256 /* WalletMainContainerInteractor.swift in Sources */, 6FA6FA6944AD6519FB8A2AC0 /* WalletMainContainerViewController.swift in Sources */, @@ -18736,7 +18742,6 @@ 982BB3FA25BA6AD5443B24C6 /* NetworkIssuesNotificationPresenter.swift in Sources */, BF3753B93B3E94BE1ECB9E81 /* NetworkIssuesNotificationInteractor.swift in Sources */, 445F1F1FB3ECC47D8DD2FBEA /* NetworkIssuesNotificationViewController.swift in Sources */, - FACE6C652BBBBC8C00643CEF /* LiquidityPoolListViewModel.swift in Sources */, 737F71CCDF39E7A400EBB7C0 /* NetworkIssuesNotificationViewLayout.swift in Sources */, 38BFEDD4B9F31EF2532962BD /* NetworkIssuesNotificationAssembly.swift in Sources */, A4FE32D50E4B7CB5B53E0067 /* StakingPoolInfoProtocols.swift in Sources */, @@ -18773,6 +18778,7 @@ DA62812C23210601F4ECF84D /* ContactsProtocols.swift in Sources */, F6CD0ED1CF9A605222745BEC /* ContactsRouter.swift in Sources */, DAAD3A3FCBA0C0E613167EBF /* ContactsPresenter.swift in Sources */, + FA1D51D92BCFE353001353E7 /* SoraFiatService.swift in Sources */, 705F5EEDD70D6941D138D3F9 /* ContactsInteractor.swift in Sources */, 3336F04749ADC27C81BA9464 /* ContactsViewController.swift in Sources */, 2515863D26C9F862AB800C4C /* ContactsViewLayout.swift in Sources */, @@ -18817,6 +18823,7 @@ A29F1452BF0AEB885E6460E2 /* SelectMarketPresenter.swift in Sources */, FADBA5F12B5FD0C200CFCF30 /* ClaimCrowdloanRewardViewModelFactory.swift in Sources */, 53DA09F488806FFE86C841AA /* SelectMarketInteractor.swift in Sources */, + FA8810D32BDCCF7E0084CC4B /* UserLiquidityPoolsListPresenter.swift in Sources */, 503DFF0EFCAD0A8B526FEC3A /* SelectMarketViewController.swift in Sources */, E40F3F3C0FE753970B8745DD /* SelectMarketAssembly.swift in Sources */, DBA6A0A26D77E7A587C51792 /* PolkaswapSwapConfirmationProtocols.swift in Sources */, @@ -18882,13 +18889,20 @@ 7E3FB57A93AFAE39CF3030C8 /* ClaimCrowdloanRewardsViewController.swift in Sources */, C80CF43B1F8307A48D03741D /* ClaimCrowdloanRewardsViewLayout.swift in Sources */, 04F9CF04588676D79EFB8570 /* ClaimCrowdloanRewardsAssembly.swift in Sources */, - 8485446546C43C075408F3B7 /* LiquidityPoolsListProtocols.swift in Sources */, - 5A403C06012CE3DD5AA0415C /* LiquidityPoolsListRouter.swift in Sources */, - A06CF2CB8E8AEC2E67B6A496 /* LiquidityPoolsListPresenter.swift in Sources */, - 04724AE7FCB7364D3AE0248F /* LiquidityPoolsListInteractor.swift in Sources */, - C45A034C51D3FE788E304291 /* LiquidityPoolsListViewController.swift in Sources */, - 95F3EB368D1DF5503DF40D4F /* LiquidityPoolsListViewLayout.swift in Sources */, - 085DBF0F0FE7856B5C52815E /* LiquidityPoolsListAssembly.swift in Sources */, + 225493AF467A54A56F74FFF5 /* LiquidityPoolsOverviewProtocols.swift in Sources */, + F52C9FF9ABB4ED034D177CF8 /* LiquidityPoolsOverviewRouter.swift in Sources */, + 1E59CE2953F8835954A4E5A7 /* LiquidityPoolsOverviewPresenter.swift in Sources */, + DFF0320CF3AA41142DEAC5F2 /* LiquidityPoolsOverviewInteractor.swift in Sources */, + 4A957B3BAC231B70CBC00EC3 /* LiquidityPoolsOverviewViewController.swift in Sources */, + 02EC6059AEE8C92B4EDD09C0 /* LiquidityPoolsOverviewViewLayout.swift in Sources */, + 61B5A91FBEF633FCC8D965B6 /* LiquidityPoolsOverviewAssembly.swift in Sources */, + CF96BE0B636DA8227E689DDA /* LiquidityPoolDetailsProtocols.swift in Sources */, + 23E30A21620831C600CBC1D6 /* LiquidityPoolDetailsRouter.swift in Sources */, + D2A85A5EE89EAAA856EA5C0F /* LiquidityPoolDetailsPresenter.swift in Sources */, + 7D8D644C5E1695288A0E86C0 /* LiquidityPoolDetailsInteractor.swift in Sources */, + A1C05D0028CD04C16AB6082F /* LiquidityPoolDetailsViewController.swift in Sources */, + 3A4026E96615A1D53DAF12D8 /* LiquidityPoolDetailsViewLayout.swift in Sources */, + 7DCC939E882247E141B1DE34 /* LiquidityPoolDetailsAssembly.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -19059,7 +19073,8 @@ 9F0457A013858A7ADEB41234 /* NftSendConfirmTests.swift in Sources */, 3E72CF4FCDFE9ED965124D18 /* AssetNetworksTests.swift in Sources */, 99DCBCC3298620721B213012 /* ClaimCrowdloanRewardsTests.swift in Sources */, - 423DD1059C62CFEF51A0CFCF /* LiquidityPoolsListTests.swift in Sources */, + 60C22E112CA857A2EA5A129E /* LiquidityPoolsOverviewTests.swift in Sources */, + 6D0E50CEBCB73C23A75A7F46 /* LiquidityPoolDetailsTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -19606,6 +19621,14 @@ version = 1.9.9; }; }; + FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/soramitsu/shared-features-spm.git"; + requirement = { + branch = "storage-requests-update"; + kind = branch; + }; + }; FA8FD17F2AF4B55100354482 /* XCRemoteSwiftPackageReference "Web3.swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/bnsports/Web3.swift.git"; @@ -19650,6 +19673,151 @@ package = FA7254652AC2F12D00EC47A6 /* XCRemoteSwiftPackageReference "WalletConnectSwiftV2" */; productName = Web3Wallet; }; + FA8810972BDCAF260084CC4B /* IrohaCrypto */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = IrohaCrypto; + }; + FA8810992BDCAF260084CC4B /* RobinHood */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = RobinHood; + }; + FA88109B2BDCAF260084CC4B /* SSFAccountManagment */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SSFAccountManagment; + }; + FA88109D2BDCAF260084CC4B /* SSFAccountManagmentStorage */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SSFAccountManagmentStorage; + }; + FA88109F2BDCAF260084CC4B /* SSFAssetManagment */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SSFAssetManagment; + }; + FA8810A12BDCAF260084CC4B /* SSFChainConnection */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SSFChainConnection; + }; + FA8810A32BDCAF260084CC4B /* SSFChainRegistry */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SSFChainRegistry; + }; + FA8810A52BDCAF260084CC4B /* SSFCloudStorage */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SSFCloudStorage; + }; + FA8810A72BDCAF260084CC4B /* SSFCrypto */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SSFCrypto; + }; + FA8810A92BDCAF260084CC4B /* SSFEraKit */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SSFEraKit; + }; + FA8810AB2BDCAF260084CC4B /* SSFExtrinsicKit */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SSFExtrinsicKit; + }; + FA8810AD2BDCAF260084CC4B /* SSFHelpers */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SSFHelpers; + }; + FA8810AF2BDCAF260084CC4B /* SSFKeyPair */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SSFKeyPair; + }; + FA8810B12BDCAF260084CC4B /* SSFLogger */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SSFLogger; + }; + FA8810B32BDCAF260084CC4B /* SSFModels */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SSFModels; + }; + FA8810B52BDCAF260084CC4B /* SSFNetwork */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SSFNetwork; + }; + FA8810B72BDCAF260084CC4B /* SSFPolkaswap */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SSFPolkaswap; + }; + FA8810B92BDCAF260084CC4B /* SSFPools */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SSFPools; + }; + FA8810BB2BDCAF260084CC4B /* SSFPoolsStorage */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SSFPoolsStorage; + }; + FA8810BD2BDCAF260084CC4B /* SSFQRService */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SSFQRService; + }; + FA8810BF2BDCAF260084CC4B /* SSFRuntimeCodingService */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SSFRuntimeCodingService; + }; + FA8810C12BDCAF260084CC4B /* SSFSigner */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SSFSigner; + }; + FA8810C32BDCAF260084CC4B /* SSFSingleValueCache */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SSFSingleValueCache; + }; + FA8810C52BDCAF260084CC4B /* SSFStorageQueryKit */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SSFStorageQueryKit; + }; + FA8810C72BDCAF260084CC4B /* SSFTransferService */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SSFTransferService; + }; + FA8810C92BDCAF260084CC4B /* SSFUtils */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SSFUtils; + }; + FA8810CB2BDCAF260084CC4B /* SSFXCM */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SSFXCM; + }; + FA8810CD2BDCAF260084CC4B /* SoraKeystore */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = SoraKeystore; + }; + FA8810CF2BDCAF260084CC4B /* keccak */ = { + isa = XCSwiftPackageProductDependency; + package = FA8810962BDCAF260084CC4B /* XCRemoteSwiftPackageReference "shared-features-spm" */; + productName = keccak; + }; FA8FD1802AF4B55100354482 /* Web3 */ = { isa = XCSwiftPackageProductDependency; package = FA8FD17F2AF4B55100354482 /* XCRemoteSwiftPackageReference "Web3.swift" */; @@ -19673,41 +19841,20 @@ /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ - FA9A8F382A776DFD008FA99F /* SubstrateDataModel.xcdatamodeld */ = { + FACE6C5D2BBAC3B000643CEF /* SubstrateDataModel.xcdatamodeld */ = { isa = XCVersionGroup; children = ( - FA9A8F392A776DFD008FA99F /* SubstrateDataModel.xcdatamodel */, - FA9A8F3A2A776DFD008FA99F /* SubstrateDataModel_v4.xcdatamodel */, - FA9A8F3B2A776DFD008FA99F /* SubstrateDataModel_v2.xcdatamodel */, - FA9A8F3C2A776DFD008FA99F /* SubstrateDataModel_v5.xcdatamodel */, - FA9A8F3D2A776DFD008FA99F /* SubstrateDataModel_v3.xcdatamodel */, + FACE6C5E2BBAC3B000643CEF /* SubstrateDataModel.xcdatamodel */, + FACE6C5F2BBAC3B000643CEF /* SubstrateDataModel_v4.xcdatamodel */, + FACE6C602BBAC3B000643CEF /* SubstrateDataModel_v2.xcdatamodel */, + FACE6C612BBAC3B000643CEF /* SubstrateDataModel_v5.xcdatamodel */, + FACE6C622BBAC3B000643CEF /* SubstrateDataModel_v3.xcdatamodel */, ); - currentVersion = FA9A8F3C2A776DFD008FA99F /* SubstrateDataModel_v5.xcdatamodel */; + currentVersion = FACE6C612BBAC3B000643CEF /* SubstrateDataModel_v5.xcdatamodel */; path = SubstrateDataModel.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; }; - FABA161E2B0C94DB001AF2F0 /* UserDataModel.xcdatamodeld */ = { - isa = XCVersionGroup; - children = ( - FABA161F2B0C94DB001AF2F0 /* MultiassetUserDataModel_v8.xcdatamodel */, - FABA16202B0C94DB001AF2F0 /* MultiassetUserDataModel_v4.xcdatamodel */, - FABA16212B0C94DB001AF2F0 /* MultiassetUserDataModel_v7.xcdatamodel */, - FABA16222B0C94DB001AF2F0 /* UserDataModel.xcdatamodel */, - FABA16232B0C94DB001AF2F0 /* MultiassetUserDataModel_v11.xcdatamodel */, - FABA16242B0C94DB001AF2F0 /* MultiassetUserDataModel_v2.xcdatamodel */, - FABA16252B0C94DB001AF2F0 /* MultiassetUserDataModel_v5.xcdatamodel */, - FABA16262B0C94DB001AF2F0 /* MultiassetUserDataModel.xcdatamodel */, - FABA16272B0C94DB001AF2F0 /* MultiassetUserDataModel_v9.xcdatamodel */, - FABA16282B0C94DB001AF2F0 /* MultiassetUserDataModel_v3.xcdatamodel */, - FABA16292B0C94DB001AF2F0 /* MultiassetUserDataModel_v10.xcdatamodel */, - FABA162A2B0C94DB001AF2F0 /* MultiassetUserDataModel_v6.xcdatamodel */, - ); - currentVersion = FABA16232B0C94DB001AF2F0 /* MultiassetUserDataModel_v11.xcdatamodel */; - path = UserDataModel.xcdatamodeld; - sourceTree = ""; - versionGroupType = wrapper.xcdatamodel; - }; /* End XCVersionGroup section */ }; rootObject = 849013A024A80984008F705E /* Project object */; diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index fd7aeea493..de6bdd306a 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,14 @@ { "pins" : [ + { + "identity" : "appauth-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/openid/AppAuth-iOS.git", + "state" : { + "revision" : "c89ed571ae140f8eb1142735e6e23d7bb8c34cb2", + "version" : "1.7.5" + } + }, { "identity" : "bigint", "kind" : "remoteSourceControl", @@ -18,6 +27,51 @@ "version" : "1.8.1" } }, + { + "identity" : "fearless-starscream", + "kind" : "remoteSourceControl", + "location" : "https://github.com/soramitsu/fearless-starscream", + "state" : { + "revision" : "3e1de9baeab87de379e0cb01c64d5db18fbf130f", + "version" : "4.0.12" + } + }, + { + "identity" : "google-api-objectivec-client-for-rest", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/google-api-objectivec-client-for-rest.git", + "state" : { + "revision" : "11941123a0d9c2641197fa83d57e6ff9f088b907", + "version" : "3.5.2" + } + }, + { + "identity" : "googlesignin-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleSignIn-iOS", + "state" : { + "revision" : "a7965d134c5d3567026c523e0a8a583f73b62b0d", + "version" : "7.1.0" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "0382ca27f22fb3494cf657d8dc356dc282cd1193", + "version" : "3.4.1" + } + }, + { + "identity" : "gtmappauth", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GTMAppAuth.git", + "state" : { + "revision" : "5d7d66f647400952b1758b230e019b07c0b4b22a", + "version" : "4.1.1" + } + }, { "identity" : "nimble", "kind" : "remoteSourceControl", @@ -54,6 +108,24 @@ "version" : "1.3.4" } }, + { + "identity" : "reachability.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ashleymills/Reachability.swift", + "state" : { + "revision" : "57da4b1270cab7c2228919eabc0e4e1bf93e48ea", + "version" : "5.2.2" + } + }, + { + "identity" : "scrypt.c", + "kind" : "remoteSourceControl", + "location" : "https://github.com/v57/scrypt.c.git", + "state" : { + "revision" : "0a1ac944808cd667a0c2e2457a4729dcdd447085", + "version" : "0.1.1" + } + }, { "identity" : "secp256k1.swift", "kind" : "remoteSourceControl", @@ -144,6 +216,15 @@ "version" : "1.0.3" } }, + { + "identity" : "swiftformat", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nicklockwood/SwiftFormat", + "state" : { + "revision" : "ab238886b8b50f8b678b251f3c28c0c887305407", + "version" : "0.53.8" + } + }, { "identity" : "swiftimagereadwrite", "kind" : "remoteSourceControl", @@ -153,6 +234,15 @@ "version" : "1.1.6" } }, + { + "identity" : "swiftybeaver", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SwiftyBeaver/SwiftyBeaver.git", + "state" : { + "revision" : "1ae1f8fe06ce9e3bd84d1e821dcca5054dc6fb3c", + "version" : "2.1.0" + } + }, { "identity" : "swime", "kind" : "remoteSourceControl", @@ -162,6 +252,15 @@ "version" : "3.1.0" } }, + { + "identity" : "tweetnacl-swiftwrap", + "kind" : "remoteSourceControl", + "location" : "https://github.com/bitmark-inc/tweetnacl-swiftwrap", + "state" : { + "revision" : "f8fd111642bf2336b11ef9ea828510693106e954", + "version" : "1.1.0" + } + }, { "identity" : "walletconnectswiftv2", "kind" : "remoteSourceControl", @@ -177,7 +276,7 @@ "location" : "https://github.com/bnsports/Web3.swift.git", "state" : { "branch" : "master", - "revision" : "f721578737afd87b704e00a913ed4566cfeb2895" + "revision" : "a526779488e5fe2fa993d9614f11f57b00cc1858" } }, { @@ -188,6 +287,15 @@ "revision" : "53fe0639a98903858d0196b699720decb42aee7b", "version" : "2.14.0" } + }, + { + "identity" : "xxhash-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/daisuke-t-jp/xxHash-Swift", + "state" : { + "revision" : "e86a07ab4867f81481d430e1370a5ec97b6e3359", + "version" : "1.1.1" + } } ], "version" : 2 diff --git a/fearless/ApplicationLayer/Alchemy/Model/AlchemySortOrder.swift b/fearless/ApplicationLayer/Alchemy/Model/AlchemySortOrder.swift index 0b5ba82947..f838dba811 100644 --- a/fearless/ApplicationLayer/Alchemy/Model/AlchemySortOrder.swift +++ b/fearless/ApplicationLayer/Alchemy/Model/AlchemySortOrder.swift @@ -1,6 +1,6 @@ import Foundation -enum AlchemySortOrder: String, Encodable { +enum AlchemySortOrder: String, Codable { case desc case asc } diff --git a/fearless/ApplicationLayer/Alchemy/Model/AlchemyTokenCategory.swift b/fearless/ApplicationLayer/Alchemy/Model/AlchemyTokenCategory.swift index 84150c9043..62aaa4cdd1 100644 --- a/fearless/ApplicationLayer/Alchemy/Model/AlchemyTokenCategory.swift +++ b/fearless/ApplicationLayer/Alchemy/Model/AlchemyTokenCategory.swift @@ -1,6 +1,6 @@ import Foundation -enum AlchemyTokenCategory: String, Encodable { +enum AlchemyTokenCategory: String, Codable { case external case `internal` case erc20 diff --git a/fearless/ApplicationLayer/Alchemy/Model/Request/AlchemyHistoryRequest.swift b/fearless/ApplicationLayer/Alchemy/Model/Request/AlchemyHistoryRequest.swift index 54c261fc06..8286683518 100644 --- a/fearless/ApplicationLayer/Alchemy/Model/Request/AlchemyHistoryRequest.swift +++ b/fearless/ApplicationLayer/Alchemy/Model/Request/AlchemyHistoryRequest.swift @@ -4,7 +4,7 @@ enum AlchemyConstants { static let firstBlockHex = "0x0" } -enum AlchemyHistoryBlockFilter: Encodable { +enum AlchemyHistoryBlockFilter: Codable { case hex(value: String) case int(value: UInt64) case latest @@ -39,7 +39,7 @@ enum AlchemyHistoryBlockFilter: Encodable { } } -struct AlchemyHistoryRequest: Encodable { +struct AlchemyHistoryRequest: Codable { let fromBlock: AlchemyHistoryBlockFilter? let toBlock: AlchemyHistoryBlockFilter? let category: [AlchemyTokenCategory] diff --git a/fearless/ApplicationLayer/Pricing/Model/SoraSubqueryPriceResponse.swift b/fearless/ApplicationLayer/Pricing/Model/SoraSubqueryPriceResponse.swift new file mode 100644 index 0000000000..18478a95ae --- /dev/null +++ b/fearless/ApplicationLayer/Pricing/Model/SoraSubqueryPriceResponse.swift @@ -0,0 +1,17 @@ +import Foundation +import SSFUtils + +struct SoraSubqueryPriceResponse: Decodable { + let entities: SoraSubqueryPricePage +} + +struct SoraSubqueryPricePage: Decodable { + let nodes: [SoraSubqueryPrice] + let pageInfo: SubqueryPageInfo +} + +struct SoraSubqueryPrice: Decodable { + let id: String + let priceUsd: String? + let priceChangeDay: Decimal? +} diff --git a/fearless/ApplicationLayer/Pricing/SoraSubqueryPriceFetcher.swift b/fearless/ApplicationLayer/Pricing/SoraSubqueryPriceFetcher.swift new file mode 100644 index 0000000000..6fc9267fb3 --- /dev/null +++ b/fearless/ApplicationLayer/Pricing/SoraSubqueryPriceFetcher.swift @@ -0,0 +1,70 @@ +import Foundation +import SSFModels + +final class SoraSubqueryPriceFetcherDefault: SoraSubqueryPriceFetcher { + private let chain: ChainModel + + init(chain: ChainModel) { + self.chain = chain + } + + func fetch(priceIds: [String]) async throws -> [SoraSubqueryPrice] { + var prices: [SoraSubqueryPrice] = [] + var cursor: String = "" + var allPricesFetched: Bool = false + + while !allPricesFetched { + let response = try await loadNewPrices(prices: prices, priceIds: priceIds, cursor: cursor) + prices = prices + response.nodes + allPricesFetched = response.pageInfo.hasNextPage.or(false) == false + cursor = response.pageInfo.endCursor.or("") + } + + return prices + } + + private func loadNewPrices( + prices _: [SoraSubqueryPrice], + priceIds: [String], + cursor: String + ) async throws -> SoraSubqueryPricePage { + guard let blockExplorer = chain.externalApi?.pricing else { + throw SubqueryPriceFetcherError.missingBlockExplorer(chain: chain.name) + } + + let request = try StakingRewardsRequest( + baseURL: blockExplorer.url, + query: queryString(priceIds: priceIds, cursor: cursor) + ) + let worker = NetworkWorker() + let response: GraphQLResponse = try await worker.performRequest(with: request) + + switch response { + case let .data(data): + return data.entities + case let .errors(error): + throw error + } + } + + private func queryString(priceIds: [String], cursor: String) -> String { + """ + query FiatPriceQuery { + entities: assets( + first: 100 + after: "\(cursor)", + filter: {id: {in: \(priceIds)}}) { + nodes { + id + priceUSD + priceChangeDay + } + pageInfo { + hasNextPage + endCursor + } + } + } + """ + } +} diff --git a/fearless/ApplicationLayer/Pricing/SubqueryPriceFetcher.swift b/fearless/ApplicationLayer/Pricing/SubqueryPriceFetcher.swift new file mode 100644 index 0000000000..a819e4d8e1 --- /dev/null +++ b/fearless/ApplicationLayer/Pricing/SubqueryPriceFetcher.swift @@ -0,0 +1,10 @@ +import Foundation +import SSFModels + +enum SubqueryPriceFetcherError: Error { + case missingBlockExplorer(chain: String) +} + +protocol SoraSubqueryPriceFetcher { + func fetch(priceIds: [String]) async throws -> [SoraSubqueryPrice] +} diff --git a/fearless/ApplicationLayer/Services/HistoryService.swift b/fearless/ApplicationLayer/Services/HistoryService.swift index 766357fedd..c9a8190d70 100644 --- a/fearless/ApplicationLayer/Services/HistoryService.swift +++ b/fearless/ApplicationLayer/Services/HistoryService.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import RobinHood import SSFModels diff --git a/fearless/ApplicationLayer/Services/SearchService.swift b/fearless/ApplicationLayer/Services/SearchService.swift index 4d6903c2a7..ce9fb556ba 100644 --- a/fearless/ApplicationLayer/Services/SearchService.swift +++ b/fearless/ApplicationLayer/Services/SearchService.swift @@ -1,4 +1,4 @@ -import CommonWallet + import RobinHood import IrohaCrypto import SSFModels diff --git a/fearless/ApplicationLayer/Services/SoraFiatService/SoraFiatService.swift b/fearless/ApplicationLayer/Services/SoraFiatService/SoraFiatService.swift new file mode 100644 index 0000000000..fa3bae1630 --- /dev/null +++ b/fearless/ApplicationLayer/Services/SoraFiatService/SoraFiatService.swift @@ -0,0 +1,73 @@ +import Foundation +import RobinHood +import sorawallet + +protocol FiatServiceObserverProtocol: AnyObject { + func processFiat(data: [FiatData]) +} + +protocol FiatServiceProtocol: AnyObject { + func getFiat() async -> [FiatData] +} + +struct FiatServiceObserver { + weak var observer: FiatServiceObserverProtocol? +} + +final class FiatService { + static let shared = FiatService() + private let operationManager = OperationManager() + private var expiredDate = Date() + private var fiatData: [FiatData] = [] + private var observers: [FiatServiceObserver] = [] + private let syncQueue = DispatchQueue(label: "co.jp.soramitsu.sora.fiat.service") + + private func updateFiatData() { + let queryOperation = SubqueryFiatInfoOperation<[FiatData]>(baseUrl: ApplicationConfig.shared.soraSubqueryUrl) + queryOperation.completionBlock = { [weak self] in + guard let self, let response = try? queryOperation.extractNoCancellableResultData() else { + return + } + self.fiatData = response + self.expiredDate = Date().addingTimeInterval(20) + } + operationManager.enqueue(operations: [queryOperation], in: .transient) + } + + private func updateFiatDataAwait() async -> [FiatData] { + let queryOperation = SubqueryFiatInfoOperation<[FiatData]>(baseUrl: ApplicationConfig.shared.soraSubqueryUrl) + operationManager.enqueue(operations: [queryOperation], in: .transient) + + return await withCheckedContinuation { continuation in + queryOperation.completionBlock = { + guard let response = try? queryOperation.extractNoCancellableResultData() else { + continuation.resume(returning: []) + return + } + continuation.resume(returning: response) + } + } + } + + private func updateFiatData(with data: [FiatData]) async { + fiatData = data + expiredDate = Date().addingTimeInterval(600) + } +} + +extension FiatService: FiatServiceProtocol { + func getFiat() async -> [FiatData] { + if expiredDate < Date() { + updateFiatData() + } + + if !fiatData.isEmpty { + return fiatData + } + + let response = await updateFiatDataAwait() + await updateFiatData(with: response) + + return response + } +} diff --git a/fearless/ApplicationLayer/Services/SoraFiatService/SubqueryFiatInfoOperation.swift b/fearless/ApplicationLayer/Services/SoraFiatService/SubqueryFiatInfoOperation.swift new file mode 100644 index 0000000000..5621b5653d --- /dev/null +++ b/fearless/ApplicationLayer/Services/SoraFiatService/SubqueryFiatInfoOperation.swift @@ -0,0 +1,57 @@ +import RobinHood +import sorawallet +import Foundation + +public final class SubqueryFiatInfoOperation: BaseOperation { + private let httpProvider: SoramitsuHttpClientProviderImpl + private let soraNetworkClient: SoramitsuNetworkClient + private let subQueryClient: SoraWalletBlockExplorerInfo + private let baseUrl: URL + + public init(baseUrl: URL) { + self.baseUrl = baseUrl + httpProvider = SoramitsuHttpClientProviderImpl() + soraNetworkClient = SoramitsuNetworkClient(timeout: 60000, logging: true, provider: httpProvider) + let provider = SoraRemoteConfigProvider( + client: soraNetworkClient, + commonUrl: "https://config.polkaswap2.io/prod/common.json", + mobileUrl: "https://config.polkaswap2.io/prod/mobile.json" + ) + let configBuilder = provider.provide() + + subQueryClient = SoraWalletBlockExplorerInfo(networkClient: soraNetworkClient, soraRemoteConfigBuilder: configBuilder) + + super.init() + } + + override public func main() { + super.main() + + if isCancelled { + return + } + + if result != nil { + return + } + + let semaphore = DispatchSemaphore(value: 0) + + var optionalCallResult: Result? + + DispatchQueue.main.async { + self.subQueryClient.getFiat(completionHandler: { [self] requestResult, _ in + + if let data = requestResult as? ResultType { + optionalCallResult = .success(data) + } + + semaphore.signal() + + result = optionalCallResult + }) + } + + semaphore.wait() + } +} diff --git a/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectSigner.swift b/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectSigner.swift index 5ca91d33dc..058e3bacbd 100644 --- a/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectSigner.swift +++ b/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectSigner.swift @@ -35,17 +35,17 @@ final class WalletConnectSignerImpl: WalletConnectSigner { let signer: WalletConnectPayloadSigner switch method { case .polkadotSignTransaction: - let cryptoType = wallet.fetch(for: chain.accountRequest())?.cryptoType.utilsType - let transactionSigner = try createSigner(for: chain, cryptoType: SFCryptoType(cryptoType ?? .sr25519)) + let cryptoType = wallet.fetch(for: chain.accountRequest())?.cryptoType + let transactionSigner = try createSigner(for: chain, cryptoType: cryptoType ?? .sr25519) let signType: WalletConnectPolkadorSigner.SignType = .signTransaction(transactionSigner: transactionSigner) signer = WalletConnectPolkadorSigner(signType: signType, chain: chain, wallet: wallet) case .polkadotSignMessage: - let cryptoType = wallet.fetch(for: chain.accountRequest())?.cryptoType.utilsType - let transactionSigner = try createSigner(for: chain, cryptoType: SFCryptoType(cryptoType ?? .sr25519)) + let cryptoType = wallet.fetch(for: chain.accountRequest())?.cryptoType + let transactionSigner = try createSigner(for: chain, cryptoType: cryptoType ?? .sr25519) let signType: WalletConnectPolkadorSigner.SignType = .signMessage(transactionSigner: transactionSigner) signer = WalletConnectPolkadorSigner(signType: signType, chain: chain, wallet: wallet) case .ethereumPersonalSign: - let transactionSigner = try createSigner(for: chain, cryptoType: .ethereumEcdsa) + let transactionSigner = try createSigner(for: chain, cryptoType: .ecdsa) let signType: WalletConnectEthereumSignerImpl.SignType = .bytes(transactionSigner: transactionSigner) signer = WalletConnectEthereumSignerImpl(signType: signType) case .ethereumSignTransaction: @@ -63,7 +63,7 @@ final class WalletConnectSignerImpl: WalletConnectSigner { ) signer = WalletConnectEthereumSignerImpl(signType: signType) case .ethereumSignTypeData, .ethereumSignTypeDataV4: - let transactionSigner = try createSigner(for: chain, cryptoType: .ethereumEcdsa) + let transactionSigner = try createSigner(for: chain, cryptoType: .ecdsa) let signType: WalletConnectEthereumSignerImpl.SignType = .bytes(transactionSigner: transactionSigner) signer = WalletConnectEthereumSignerImpl(signType: signType) } @@ -75,12 +75,13 @@ final class WalletConnectSignerImpl: WalletConnectSigner { private func createSigner( for chain: ChainModel, - cryptoType: SFCryptoType + cryptoType: CryptoType ) throws -> TransactionSignerProtocol { let publicKeyData = try extractPublicKey(for: chain) let secretKeyData = try extractPrivateKey(for: chain) - return TransactionSigner( + return TransactionSignerAssembly.signer( + for: chain.chainBaseType, publicKeyData: publicKeyData, secretKeyData: secretKeyData, cryptoType: cryptoType diff --git a/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectSocketEngine.swift b/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectSocketEngine.swift index b1ba9667ca..b9825d00ab 100644 --- a/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectSocketEngine.swift +++ b/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectSocketEngine.swift @@ -69,7 +69,7 @@ final class WalletConnectSocketEngine: WebSocketConnecting { self?.didDisconnectedWith(error: error) case let .text(text): self?.onText?(text) - case .ping, .pong, .binary: + case .ping, .pong, .binary, .timeout, .waiting: break } } diff --git a/fearless/Assets.xcassets/iconChevronRight.imageset/Chevron.pdf b/fearless/Assets.xcassets/iconChevronRight.imageset/Chevron.pdf new file mode 100644 index 0000000000000000000000000000000000000000..dcd514aaabf3bddcdf8d815b5f1d1e16eb9a25c8 GIT binary patch literal 1018 zcmZXTO>fjN5QgvbE9SCNdmypn_)DrP(OrrVAeNL{#UW(fZP0EENs0=;o^dvD(ykBD z`0;o?{7zQe&FveWXaJ53{g0o3cy)!>*Wj96*h04C=_l9h`$rT6i{RDutt*d^Zrq=` za>e@_T;DBsRR_O;C2Uy8CxcRsXg-sP$(7Ouv+1llLi9T~x~c)0WHvKaV4g^s8O|_V z(M)ru(Ii|LX*C`~7_E7p5!1>@#i2coW}HHy`xewbWq@wXYHTp zE(for url: URL) -> AnySingleValueProvider + func getJson(for url: URL) throws -> AnySingleValueProvider } class JsonDataProviderFactory: JsonDataProviderFactoryProtocol { @@ -18,7 +19,7 @@ class JsonDataProviderFactory: JsonDataProviderFactoryProtocol { self.useCache = useCache } - func getJson(for url: URL) -> AnySingleValueProvider { + func getJson(for url: URL) throws -> AnySingleValueProvider { let localKey = url.absoluteString if let provider = providers[localKey]?.target as? SingleValueProvider, useCache { @@ -27,7 +28,7 @@ class JsonDataProviderFactory: JsonDataProviderFactoryProtocol { let source = JsonSingleProviderSource(url: url) - let repository: CoreDataRepository = storageFacade.createRepository() + let repository: CoreDataRepository = try SingleValueCacheRepositoryFactoryDefault().createSingleValueCacheRepository() let singleValueProvider = SingleValueProvider( targetIdentifier: localKey, diff --git a/fearless/Common/DataProvider/PriceProviderFactory.swift b/fearless/Common/DataProvider/PriceProviderFactory.swift index 5e07d0234b..530a5efa72 100644 --- a/fearless/Common/DataProvider/PriceProviderFactory.swift +++ b/fearless/Common/DataProvider/PriceProviderFactory.swift @@ -1,6 +1,7 @@ import Foundation import RobinHood import SSFModels +import SSFSingleValueCache protocol PriceProviderFactoryProtocol { func getPricesProvider(currencies: [Currency]?) -> AnySingleValueProvider<[PriceData]> @@ -45,7 +46,7 @@ extension PriceProviderFactory: PriceProviderFactoryProtocol { return AnySingleValueProvider(provider) } - let repository: CoreDataRepository = storageFacade.createRepository() + let repository: CoreDataRepository = SingleValueCacheRepositoryFactoryDefault().createSingleValueCacheRepository() let source = PriceDataSource(currencies: currencies) let trigger: DataProviderEventTrigger = [.onInitialization, .onFetchPage] let provider = SingleValueProvider( diff --git a/fearless/Common/DataProvider/RelaychainStakingLocalSubscriptionFactory.swift b/fearless/Common/DataProvider/RelaychainStakingLocalSubscriptionFactory.swift index 2195f78767..fc7d382917 100644 --- a/fearless/Common/DataProvider/RelaychainStakingLocalSubscriptionFactory.swift +++ b/fearless/Common/DataProvider/RelaychainStakingLocalSubscriptionFactory.swift @@ -215,7 +215,7 @@ final class RelaychainStakingLocalSubscriptionFactory: SubstrateLocalSubscriptio return AnySingleValueProvider(provider) } - let repository = SubstrateRepositoryFactory( + let repository = try SubstrateRepositoryFactory( storageFacade: storageFacade ).createSingleValueRepository() diff --git a/fearless/Common/DataProvider/Sources/PriceDataSource.swift b/fearless/Common/DataProvider/Sources/PriceDataSource.swift index ed872221dd..94db4737cb 100644 --- a/fearless/Common/DataProvider/Sources/PriceDataSource.swift +++ b/fearless/Common/DataProvider/Sources/PriceDataSource.swift @@ -1,10 +1,13 @@ import Foundation - -import Foundation +import sorawallet import RobinHood import SoraKeystore import SSFModels +enum PriceDataSourceError: Swift.Error { + case memoryError +} + final class PriceDataSource: SingleValueProviderSourceProtocol { static let defaultIdentifier: String = "all-chainAsset-prices-usd" typealias Model = [PriceData] @@ -39,42 +42,54 @@ final class PriceDataSource: SingleValueProviderSourceProtocol { func fetchOperation() -> CompoundOperationWrapper<[PriceData]?> { let coingeckoOperation = createCoingeckoOperation() let chainlinkOperations = createChainlinkOperations() + let soraSubqueryOperation = createSoraSubqueryOperation() let targetOperation: BaseOperation<[PriceData]?> = ClosureOperation { [weak self] in + guard let self else { + throw PriceDataSourceError.memoryError + } + + var prices: [PriceData] = [] var coingeckoPrices = try coingeckoOperation.extractNoCancellableResultData() let chainlinkPrices = chainlinkOperations.compactMap { try? $0.extractNoCancellableResultData() } - - let replacedFiatDayChange: [PriceData] = chainlinkPrices.compactMap { chainlinkPrice in - let coingeckoPrice = coingeckoPrices.first(where: { $0.coingeckoPriceId == chainlinkPrice.coingeckoPriceId }) - return chainlinkPrice.replaceFiatDayChange(fiatDayChange: coingeckoPrice?.fiatDayChange) - } - - if chainlinkPrices.count != chainlinkOperations.count || chainlinkOperations.isEmpty { - let chainlinkPriceChainAsset = self?.chainAssets.filter { $0.asset.priceProvider?.type == .chainlink } - let failedPriceId = chainlinkPriceChainAsset?.compactMap { $0.asset.coingeckoPriceId }.diff(from: chainlinkPrices.map { $0.coingeckoPriceId }) - let replacedPrices = coingeckoPrices.compactMap { price in - if failedPriceId?.contains(price.coingeckoPriceId) == true { - guard let failedChainlinkCHainAsset = chainlinkPriceChainAsset?.first(where: { $0.asset.coingeckoPriceId == price.coingeckoPriceId }) else { - return price - } - return PriceData( - currencyId: price.currencyId, - priceId: failedChainlinkCHainAsset.asset.priceProvider?.id ?? price.priceId, - price: price.price, - fiatDayChange: price.fiatDayChange, - coingeckoPriceId: price.coingeckoPriceId - ) - } - return nil - } - coingeckoPrices = coingeckoPrices + replacedPrices - } - - return coingeckoPrices + replacedFiatDayChange + let soraSubqueryPrices = try? soraSubqueryOperation.extractNoCancellableResultData() + + prices = self.merge(coingeckoPrices: coingeckoPrices, chainlinkPrices: chainlinkPrices) + prices = self.merge(coingeckoPrices: coingeckoPrices, soraSubqueryPrices: soraSubqueryPrices.or([])) + + return prices +// let replacedFiatDayChange: [PriceData] = chainlinkPrices.compactMap { chainlinkPrice in +// let coingeckoPrice = coingeckoPrices.first(where: { $0.coingeckoPriceId == chainlinkPrice.coingeckoPriceId }) +// return chainlinkPrice.replaceFiatDayChange(fiatDayChange: coingeckoPrice?.fiatDayChange) +// } +// +// if chainlinkPrices.count != chainlinkOperations.count || chainlinkOperations.isEmpty { +// let chainlinkPriceChainAsset = self.chainAssets.filter { $0.asset.priceProvider?.type == .chainlink } +// let failedPriceId = chainlinkPriceChainAsset.compactMap { $0.asset.coingeckoPriceId }.diff(from: chainlinkPrices.map { $0.coingeckoPriceId }) +// let replacedPrices = coingeckoPrices.compactMap { price in +// if failedPriceId.contains(price.coingeckoPriceId) == true { +// guard let failedChainlinkCHainAsset = chainlinkPriceChainAsset.first(where: { $0.asset.coingeckoPriceId == price.coingeckoPriceId }) else { +// return price +// } +// return PriceData( +// currencyId: price.currencyId, +// priceId: failedChainlinkCHainAsset.asset.priceProvider?.id ?? price.priceId, +// price: price.price, +// fiatDayChange: price.fiatDayChange, +// coingeckoPriceId: price.coingeckoPriceId +// ) +// } +// return nil +// } +// coingeckoPrices = coingeckoPrices + replacedPrices +// } +// +// return coingeckoPrices + replacedFiatDayChange } + targetOperation.addDependency(soraSubqueryOperation) targetOperation.addDependency(coingeckoOperation) chainlinkOperations.forEach { targetOperation.addDependency($0) @@ -82,12 +97,88 @@ final class PriceDataSource: SingleValueProviderSourceProtocol { return CompoundOperationWrapper( targetOperation: targetOperation, - dependencies: [coingeckoOperation] + chainlinkOperations + dependencies: [coingeckoOperation, soraSubqueryOperation] + chainlinkOperations ) } // MARK: - Private methods + private func merge(coingeckoPrices: [PriceData], chainlinkPrices: [PriceData]) -> [PriceData] { + let caPriceIds = Set(chainAssets.compactMap { $0.asset.coingeckoPriceId }) + let sqPriceIds = Set(chainlinkPrices.compactMap { $0.coingeckoPriceId }) + + let replacedFiatDayChange: [PriceData] = chainlinkPrices.compactMap { chainlinkPrice in + let coingeckoPrice = coingeckoPrices.first(where: { $0.coingeckoPriceId == chainlinkPrice.coingeckoPriceId }) + return chainlinkPrice.replaceFiatDayChange(fiatDayChange: coingeckoPrice?.fiatDayChange) + } + + let filtered = coingeckoPrices.filter { coingeckoPrice in + guard let coingeckoPriceId = coingeckoPrice.coingeckoPriceId else { + return true + } + return !caPriceIds.intersection(sqPriceIds).contains(coingeckoPriceId) + } + + return filtered + replacedFiatDayChange + } + + private func merge(coingeckoPrices: [PriceData], soraSubqueryPrices: [PriceData]) -> [PriceData] { + let caPriceIds = Set(chainAssets.compactMap { $0.asset.priceId }) + let sqPriceIds = Set(soraSubqueryPrices.compactMap { $0.priceId }) + + let replacedFiatDayChange: [PriceData] = soraSubqueryPrices.compactMap { soraSubqueryPrice in + let coingeckoPrice = coingeckoPrices.first(where: { $0.priceId == soraSubqueryPrice.priceId }) + return soraSubqueryPrice.replaceFiatDayChange(fiatDayChange: coingeckoPrice?.fiatDayChange) + } + + let filtered = coingeckoPrices.filter { coingeckoPrice in + let chainAsset = chainAssets.first { $0.asset.coingeckoPriceId == coingeckoPrice.priceId } + guard let priceId = chainAsset?.asset.priceId else { + return true + } + return !caPriceIds.intersection(sqPriceIds).contains(priceId) + } + + return filtered + replacedFiatDayChange + } + + private func createSoraSubqueryOperation() -> BaseOperation<[PriceData]> { + guard currencies?.count == 1, currencies?.first?.id == Currency.defaultCurrency().id else { + return BaseOperation.createWithResult([]) + } + + guard let chain = chainAssets.first(where: { $0.chain.knownChainEquivalent == .soraMain })?.chain else { + return BaseOperation.createWithResult([]) + } + let fetcher = SoraSubqueryPriceFetcherDefault(chain: chain) + return AwaitOperation { + let priceIds = chain.assets.filter { $0.priceProvider?.type == .sorasubquery }.compactMap { $0.priceProvider?.id } + let prices = try await fetcher.fetch(priceIds: priceIds) + + return prices.compactMap { price in + let chainAsset = self.chainAssets.filter { $0.chain.knownChainEquivalent == .soraMain }.first(where: { $0.asset.currencyId == price.id }) + + guard let chainAsset = chainAsset else { + return nil + } + guard chainAsset.asset.priceProvider?.type == .sorasubquery else { + return nil + } + guard let priceId = chainAsset.asset.priceId else { + return nil + } + + return PriceData( + currencyId: "usd", + priceId: priceId, + price: "\(price.priceUsd.or("0"))", + fiatDayChange: price.priceChangeDay, + coingeckoPriceId: chainAsset.asset.coingeckoPriceId + ) + } + } + } + private func createCoingeckoOperation() -> BaseOperation<[PriceData]> { let currencies = self.currencies ?? [SelectedWalletSettings.shared.value?.selectedCurrency].compactMap { $0 } let priceIds = chainAssets diff --git a/fearless/Common/DataProvider/Sources/Rewards/SubqueryRewardSource.swift b/fearless/Common/DataProvider/Sources/Rewards/SubqueryRewardSource.swift index 5c98a94b75..a422921c80 100644 --- a/fearless/Common/DataProvider/Sources/Rewards/SubqueryRewardSource.swift +++ b/fearless/Common/DataProvider/Sources/Rewards/SubqueryRewardSource.swift @@ -1,7 +1,6 @@ import Foundation import RobinHood import BigInt -import CommonWallet final class SubqueryRewardSource { typealias Model = TotalRewardItem diff --git a/fearless/Common/DataProvider/StakingAnalyticsLocalSubscriptionFactory.swift b/fearless/Common/DataProvider/StakingAnalyticsLocalSubscriptionFactory.swift index e3c01e00ac..4fb249970c 100644 --- a/fearless/Common/DataProvider/StakingAnalyticsLocalSubscriptionFactory.swift +++ b/fearless/Common/DataProvider/StakingAnalyticsLocalSubscriptionFactory.swift @@ -7,7 +7,7 @@ protocol StakingAnalyticsLocalSubscriptionFactoryProtocol { chainAsset: ChainAsset, for address: AccountAddress, url: URL - ) -> AnySingleValueProvider<[SubqueryRewardItemData]>? + ) throws -> AnySingleValueProvider<[SubqueryRewardItemData]>? } final class ParachainAnalyticsLocalSubscriptionFactory { @@ -35,7 +35,7 @@ extension ParachainAnalyticsLocalSubscriptionFactory: StakingAnalyticsLocalSubsc chainAsset: ChainAsset, for address: AccountAddress, url: URL - ) -> AnySingleValueProvider<[SubqueryRewardItemData]>? { + ) throws -> AnySingleValueProvider<[SubqueryRewardItemData]>? { clearIfNeeded() let identifier = "weaklyAnalytics" + address + url.absoluteString @@ -44,7 +44,7 @@ extension ParachainAnalyticsLocalSubscriptionFactory: StakingAnalyticsLocalSubsc return AnySingleValueProvider(provider) } - let repository = SubstrateRepositoryFactory(storageFacade: storageFacade) + let repository = try SubstrateRepositoryFactory(storageFacade: storageFacade) .createSingleValueRepository() let operationFactory = RewardOperationFactory.factory(chain: chainAsset.chain) @@ -91,7 +91,7 @@ extension RelaychainAnalyticsLocalSubscriptionFactory: StakingAnalyticsLocalSubs chainAsset: ChainAsset, for address: AccountAddress, url: URL - ) -> AnySingleValueProvider<[SubqueryRewardItemData]>? { + ) throws -> AnySingleValueProvider<[SubqueryRewardItemData]>? { clearIfNeeded() let identifier = "weaklyAnalytics" + address + url.absoluteString @@ -100,7 +100,7 @@ extension RelaychainAnalyticsLocalSubscriptionFactory: StakingAnalyticsLocalSubs return AnySingleValueProvider(provider) } - let repository = SubstrateRepositoryFactory(storageFacade: storageFacade) + let repository = try SubstrateRepositoryFactory(storageFacade: storageFacade) .createSingleValueRepository() let operationFactory = RewardOperationFactory.factory(chain: chainAsset.chain) diff --git a/fearless/Common/DataProvider/Subscription/JsonLocalStorageSubscriber.swift b/fearless/Common/DataProvider/Subscription/JsonLocalStorageSubscriber.swift index 9da91c0cd6..252eff6dd8 100644 --- a/fearless/Common/DataProvider/Subscription/JsonLocalStorageSubscriber.swift +++ b/fearless/Common/DataProvider/Subscription/JsonLocalStorageSubscriber.swift @@ -10,16 +10,16 @@ protocol JsonLocalStorageSubscriber where Self: AnyObject { func subscribeToCrowdloanDisplayInfo( for url: URL, chainId: ChainModel.Id - ) -> AnySingleValueProvider? + ) throws -> AnySingleValueProvider? } extension JsonLocalStorageSubscriber { func subscribeToCrowdloanDisplayInfo( for url: URL, chainId: ChainModel.Id - ) -> AnySingleValueProvider? { + ) throws -> AnySingleValueProvider? { let displayInfoProvider: AnySingleValueProvider = - jsonLocalSubscriptionFactory.getJson(for: url) + try jsonLocalSubscriptionFactory.getJson(for: url) let updateClosure: ([DataProviderChange]) -> Void = { [weak self] changes in let result = changes.reduceToLastChange() diff --git a/fearless/Common/DataProvider/Subscription/StakingAnalyticsLocalStorageSubscriber.swift b/fearless/Common/DataProvider/Subscription/StakingAnalyticsLocalStorageSubscriber.swift index f4e043c3cb..5cc6b5dd31 100644 --- a/fearless/Common/DataProvider/Subscription/StakingAnalyticsLocalStorageSubscriber.swift +++ b/fearless/Common/DataProvider/Subscription/StakingAnalyticsLocalStorageSubscriber.swift @@ -11,7 +11,7 @@ protocol StakingAnalyticsLocalStorageSubscriber where Self: AnyObject { chainAsset: ChainAsset, for address: AccountAddress, url: URL - ) -> AnySingleValueProvider<[SubqueryRewardItemData]>? + ) throws -> AnySingleValueProvider<[SubqueryRewardItemData]>? } extension StakingAnalyticsLocalStorageSubscriber { @@ -19,8 +19,8 @@ extension StakingAnalyticsLocalStorageSubscriber { chainAsset: ChainAsset, for address: AccountAddress, url: URL - ) -> AnySingleValueProvider<[SubqueryRewardItemData]>? { - let provider = stakingAnalyticsLocalSubscriptionFactory + ) throws -> AnySingleValueProvider<[SubqueryRewardItemData]>? { + let provider = try stakingAnalyticsLocalSubscriptionFactory .getWeaklyAnalyticsProvider(chainAsset: chainAsset, for: address, url: url) let updateClosure = { [weak self] (changes: [DataProviderChange<[SubqueryRewardItemData]>]) in diff --git a/fearless/Common/Errors/WalletErrorContentProtocol.swift b/fearless/Common/Errors/WalletErrorContentProtocol.swift new file mode 100644 index 0000000000..74873ce4ed --- /dev/null +++ b/fearless/Common/Errors/WalletErrorContentProtocol.swift @@ -0,0 +1,15 @@ +import Foundation + +public protocol WalletErrorContentProtocol { + var title: String { get } + var message: String { get } +} + +public protocol WalletErrorContentConvertible { + func toErrorContent(for locale: Locale?) -> WalletErrorContentProtocol +} + +struct WalletErrorContent: WalletErrorContentProtocol { + let title: String + let message: String +} diff --git a/fearless/Common/Extension/FearlessUtils/MultiSignature+CryptoType.swift b/fearless/Common/Extension/FearlessUtils/MultiSignature+CryptoType.swift index cb93bce833..cf116b9c9c 100644 --- a/fearless/Common/Extension/FearlessUtils/MultiSignature+CryptoType.swift +++ b/fearless/Common/Extension/FearlessUtils/MultiSignature+CryptoType.swift @@ -1,5 +1,6 @@ import Foundation import SSFUtils +import SSFModels extension MultiSignature { static func signature(from cryptoType: CryptoType, data: Data) -> MultiSignature { diff --git a/fearless/Common/Extension/Foundation/NSPredicate+Filter.swift b/fearless/Common/Extension/Foundation/NSPredicate+Filter.swift index 302f982c5d..94be9f7027 100644 --- a/fearless/Common/Extension/Foundation/NSPredicate+Filter.swift +++ b/fearless/Common/Extension/Foundation/NSPredicate+Filter.swift @@ -1,6 +1,7 @@ import Foundation import IrohaCrypto import SSFModels +import SSFAccountManagmentStorage extension NSPredicate { // TODO: Remove diff --git a/fearless/Common/Extension/Model/CryptoType+Extrinsic.swift b/fearless/Common/Extension/Model/CryptoType+Extrinsic.swift index 6c3cdadd70..a838720594 100644 --- a/fearless/Common/Extension/Model/CryptoType+Extrinsic.swift +++ b/fearless/Common/Extension/Model/CryptoType+Extrinsic.swift @@ -1,4 +1,5 @@ import Foundation +import SSFModels extension CryptoType { init?(version: UInt8) { diff --git a/fearless/Common/Extension/Model/CryptoType+Keystore.swift b/fearless/Common/Extension/Model/CryptoType+Keystore.swift index 7ce2fbea34..a6209c4cd3 100644 --- a/fearless/Common/Extension/Model/CryptoType+Keystore.swift +++ b/fearless/Common/Extension/Model/CryptoType+Keystore.swift @@ -1,4 +1,5 @@ import Foundation +import SSFModels extension CryptoType { var supportsSeedFromSecretKey: Bool { diff --git a/fearless/Common/Extension/Model/CryptoType+Utils.swift b/fearless/Common/Extension/Model/CryptoType+Utils.swift deleted file mode 100644 index ab70ee746b..0000000000 --- a/fearless/Common/Extension/Model/CryptoType+Utils.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Foundation -import SSFUtils - -extension CryptoType { - init(_ utilsType: SSFUtils.CryptoType) { - switch utilsType { - case .sr25519: - self = .sr25519 - case .ed25519: - self = .ed25519 - case .ecdsa: - self = .ecdsa - } - } - - var utilsType: SSFUtils.CryptoType { - switch self { - case .sr25519: - return .sr25519 - case .ed25519: - return .ed25519 - case .ecdsa: - return .ecdsa - } - } -} diff --git a/fearless/Common/Extension/Model/CryptoType+ViewModel.swift b/fearless/Common/Extension/Model/CryptoType+ViewModel.swift index cee4efad43..4bfe437444 100644 --- a/fearless/Common/Extension/Model/CryptoType+ViewModel.swift +++ b/fearless/Common/Extension/Model/CryptoType+ViewModel.swift @@ -1,4 +1,5 @@ import Foundation +import SSFModels extension CryptoType { func titleForLocale(_ locale: Locale) -> String { diff --git a/fearless/Common/Extension/Storage/CDAccountInfo+CoreDataCodable.swift b/fearless/Common/Extension/Storage/CDAccountInfo+CoreDataCodable.swift index 869e2d6369..1b35c35366 100644 --- a/fearless/Common/Extension/Storage/CDAccountInfo+CoreDataCodable.swift +++ b/fearless/Common/Extension/Storage/CDAccountInfo+CoreDataCodable.swift @@ -2,6 +2,7 @@ import Foundation import CoreData import RobinHood import IrohaCrypto +import SSFAccountManagmentStorage extension CDAccountInfo: CoreDataCodable { public func populate(from decoder: Decoder, using _: NSManagedObjectContext) throws { diff --git a/fearless/Common/Extension/Storage/CDAccountItem+CoreDataDecodable.swift b/fearless/Common/Extension/Storage/CDAccountItem+CoreDataDecodable.swift index dbb20c1642..c3d5aa6a81 100644 --- a/fearless/Common/Extension/Storage/CDAccountItem+CoreDataDecodable.swift +++ b/fearless/Common/Extension/Storage/CDAccountItem+CoreDataDecodable.swift @@ -2,6 +2,7 @@ import Foundation import CoreData import RobinHood import IrohaCrypto +import SSFAccountManagmentStorage // TODO: Fix logic extension CDMetaAccount: CoreDataCodable { diff --git a/fearless/Common/Extension/Storage/CDSingleValue+CoreDataCodable.swift b/fearless/Common/Extension/Storage/CDSingleValue+CoreDataCodable.swift index 616d000760..32fe78958b 100644 --- a/fearless/Common/Extension/Storage/CDSingleValue+CoreDataCodable.swift +++ b/fearless/Common/Extension/Storage/CDSingleValue+CoreDataCodable.swift @@ -1,6 +1,7 @@ import Foundation import RobinHood import CoreData +import SSFSingleValueCache extension CDSingleValue: CoreDataCodable { enum CodingKeys: String, CodingKey { diff --git a/fearless/Common/Extension/Storage/SortDescriptor+Storage.swift b/fearless/Common/Extension/Storage/SortDescriptor+Storage.swift index 4e57f3c409..0ea7d8594f 100644 --- a/fearless/Common/Extension/Storage/SortDescriptor+Storage.swift +++ b/fearless/Common/Extension/Storage/SortDescriptor+Storage.swift @@ -1,4 +1,5 @@ import Foundation +import SSFAccountManagmentStorage extension NSSortDescriptor { static var accountsByOrder: NSSortDescriptor { diff --git a/fearless/Common/Extension/UIControl+Disable.swift b/fearless/Common/Extension/UIControl+Disable.swift new file mode 100644 index 0000000000..f9930fff10 --- /dev/null +++ b/fearless/Common/Extension/UIControl+Disable.swift @@ -0,0 +1,13 @@ +import UIKit + +extension UIControl { + func disable(with alpha: CGFloat = 0.5) { + isEnabled = false + self.alpha = alpha + } + + func enable() { + isEnabled = true + alpha = 1.0 + } +} diff --git a/fearless/Common/Extension/UIKit/PolkadotIcon+Image.swift b/fearless/Common/Extension/UIKit/PolkadotIcon+Image.swift index 6177b66dbc..20d7ab1cf7 100644 --- a/fearless/Common/Extension/UIKit/PolkadotIcon+Image.swift +++ b/fearless/Common/Extension/UIKit/PolkadotIcon+Image.swift @@ -1,4 +1,4 @@ -import Foundation +import UIKit import SSFUtils extension DrawableIcon { diff --git a/fearless/Common/Extension/UIKit/SkeletonRow+View.swift b/fearless/Common/Extension/UIKit/SkeletonRow+View.swift index 52a2603068..e906b4caf5 100644 --- a/fearless/Common/Extension/UIKit/SkeletonRow+View.swift +++ b/fearless/Common/Extension/UIKit/SkeletonRow+View.swift @@ -62,4 +62,11 @@ extension SingleSkeleton { return SingleSkeleton(position: spaceSize.skrullMap(point: position), size: mappedSize).round() } + + static func createRow( + position: CGPoint, + size _: CGSize + ) -> SingleSkeleton { + SingleSkeleton(position: position, size: CGSize(width: 1, height: 1)).round() + } } diff --git a/fearless/Common/Extension/Wallet/AssetTransactionData+AlchemyHistory.swift b/fearless/Common/Extension/Wallet/AssetTransactionData+AlchemyHistory.swift index 1e120df54b..1a4f6fb9c7 100644 --- a/fearless/Common/Extension/Wallet/AssetTransactionData+AlchemyHistory.swift +++ b/fearless/Common/Extension/Wallet/AssetTransactionData+AlchemyHistory.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SoraFoundation import SSFModels diff --git a/fearless/Common/Extension/Wallet/AssetTransactionData+ArrowsquidHistory.swift b/fearless/Common/Extension/Wallet/AssetTransactionData+ArrowsquidHistory.swift index e40974c9c6..375fabda6c 100644 --- a/fearless/Common/Extension/Wallet/AssetTransactionData+ArrowsquidHistory.swift +++ b/fearless/Common/Extension/Wallet/AssetTransactionData+ArrowsquidHistory.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import BigInt import IrohaCrypto import SSFUtils diff --git a/fearless/Common/Extension/Wallet/AssetTransactionData+EtherscanHistory.swift b/fearless/Common/Extension/Wallet/AssetTransactionData+EtherscanHistory.swift index 2b4bd8c558..3bf6ffdb2e 100644 --- a/fearless/Common/Extension/Wallet/AssetTransactionData+EtherscanHistory.swift +++ b/fearless/Common/Extension/Wallet/AssetTransactionData+EtherscanHistory.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SoraFoundation import SSFModels diff --git a/fearless/Common/Extension/Wallet/AssetTransactionData+GiantsquidHistory.swift b/fearless/Common/Extension/Wallet/AssetTransactionData+GiantsquidHistory.swift index aff3bf5fbe..8146555f3f 100644 --- a/fearless/Common/Extension/Wallet/AssetTransactionData+GiantsquidHistory.swift +++ b/fearless/Common/Extension/Wallet/AssetTransactionData+GiantsquidHistory.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import BigInt import IrohaCrypto import SSFUtils diff --git a/fearless/Common/Extension/Wallet/AssetTransactionData+OklinkHistory.swift b/fearless/Common/Extension/Wallet/AssetTransactionData+OklinkHistory.swift index 43649e4dd1..d47ea4ed5c 100644 --- a/fearless/Common/Extension/Wallet/AssetTransactionData+OklinkHistory.swift +++ b/fearless/Common/Extension/Wallet/AssetTransactionData+OklinkHistory.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SoraFoundation import SSFModels diff --git a/fearless/Common/Extension/Wallet/AssetTransactionData+SoraSubsquidHistory.swift b/fearless/Common/Extension/Wallet/AssetTransactionData+SoraSubsquidHistory.swift index 6385964e24..30e5dafdf9 100644 --- a/fearless/Common/Extension/Wallet/AssetTransactionData+SoraSubsquidHistory.swift +++ b/fearless/Common/Extension/Wallet/AssetTransactionData+SoraSubsquidHistory.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SoraFoundation import SSFModels import BigInt diff --git a/fearless/Common/Extension/Wallet/AssetTransactionData+SubqueryHistory.swift b/fearless/Common/Extension/Wallet/AssetTransactionData+SubqueryHistory.swift index 29097c3212..e71d96447d 100644 --- a/fearless/Common/Extension/Wallet/AssetTransactionData+SubqueryHistory.swift +++ b/fearless/Common/Extension/Wallet/AssetTransactionData+SubqueryHistory.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import BigInt import IrohaCrypto import SSFUtils diff --git a/fearless/Common/Extension/Wallet/AssetTransactionData+SubsquidHistory.swift b/fearless/Common/Extension/Wallet/AssetTransactionData+SubsquidHistory.swift index 886186853e..a3c9b03db5 100644 --- a/fearless/Common/Extension/Wallet/AssetTransactionData+SubsquidHistory.swift +++ b/fearless/Common/Extension/Wallet/AssetTransactionData+SubsquidHistory.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import BigInt import IrohaCrypto import SSFUtils diff --git a/fearless/Common/Extension/Wallet/AssetTransactionData+Zeta.swift b/fearless/Common/Extension/Wallet/AssetTransactionData+Zeta.swift index 1c36f8f624..5b36362b58 100644 --- a/fearless/Common/Extension/Wallet/AssetTransactionData+Zeta.swift +++ b/fearless/Common/Extension/Wallet/AssetTransactionData+Zeta.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SoraFoundation import SSFModels diff --git a/fearless/Common/Extension/Wallet/ErrorContent+Wallet.swift b/fearless/Common/Extension/Wallet/ErrorContent+Wallet.swift index 63238d0a9c..567062f7f4 100644 --- a/fearless/Common/Extension/Wallet/ErrorContent+Wallet.swift +++ b/fearless/Common/Extension/Wallet/ErrorContent+Wallet.swift @@ -1,4 +1,3 @@ import Foundation -import CommonWallet extension ErrorContent: WalletErrorContentProtocol {} diff --git a/fearless/Common/Extension/Wallet/Logger+Wallet.swift b/fearless/Common/Extension/Wallet/Logger+Wallet.swift index 7b020157e8..c02e7d0c3e 100644 --- a/fearless/Common/Extension/Wallet/Logger+Wallet.swift +++ b/fearless/Common/Extension/Wallet/Logger+Wallet.swift @@ -1,4 +1,3 @@ import Foundation -import CommonWallet extension Logger: WalletLoggerProtocol {} diff --git a/fearless/Common/Extension/Wallet/SearchData+Contacts.swift b/fearless/Common/Extension/Wallet/SearchData+Contacts.swift index 0a0c219a30..25a7987f43 100644 --- a/fearless/Common/Extension/Wallet/SearchData+Contacts.swift +++ b/fearless/Common/Extension/Wallet/SearchData+Contacts.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import IrohaCrypto import SSFModels diff --git a/fearless/Common/Extension/Wallet/TransactionHistoryItem+Status.swift b/fearless/Common/Extension/Wallet/TransactionHistoryItem+Status.swift index 8795ad6e6d..002a4dea33 100644 --- a/fearless/Common/Extension/Wallet/TransactionHistoryItem+Status.swift +++ b/fearless/Common/Extension/Wallet/TransactionHistoryItem+Status.swift @@ -1,5 +1,4 @@ import Foundation -import CommonWallet extension TransactionHistoryItem.Status { var walletValue: AssetTransactionStatus { diff --git a/fearless/Common/Helpers/AccountProviderFactory.swift b/fearless/Common/Helpers/AccountProviderFactory.swift index 2f06ad0f54..915229769b 100644 --- a/fearless/Common/Helpers/AccountProviderFactory.swift +++ b/fearless/Common/Helpers/AccountProviderFactory.swift @@ -1,6 +1,7 @@ import Foundation import IrohaCrypto import RobinHood +import SSFAccountManagmentStorage protocol AccountProviderFactoryProtocol { var operationManager: OperationManagerProtocol { get } diff --git a/fearless/Common/Helpers/AmountFormatterFactory.swift b/fearless/Common/Helpers/AmountFormatterFactory.swift index 91c1f2f01a..5c7725a615 100644 --- a/fearless/Common/Helpers/AmountFormatterFactory.swift +++ b/fearless/Common/Helpers/AmountFormatterFactory.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SoraFoundation @available(*, deprecated, message: "Use AssetBalanceFormatterFactory instead") diff --git a/fearless/Common/Helpers/ChainAccountFetching.swift b/fearless/Common/Helpers/ChainAccountFetching.swift index dcc9551fb5..1333523f5c 100644 --- a/fearless/Common/Helpers/ChainAccountFetching.swift +++ b/fearless/Common/Helpers/ChainAccountFetching.swift @@ -1,13 +1,6 @@ import Foundation import SSFModels -struct ChainAccountRequest { - let chainId: ChainModel.Id - let addressPrefix: UInt16 - let isEthereumBased: Bool - let accountId: AccountId? -} - struct ChainAccountResponse: Equatable { let chainId: ChainModel.Id let accountId: AccountId @@ -97,14 +90,3 @@ extension MetaAccountModel { ) } } - -extension ChainModel { - func accountRequest(_ accountId: AccountId? = nil) -> ChainAccountRequest { - ChainAccountRequest( - chainId: chainId, - addressPrefix: addressPrefix, - isEthereumBased: isEthereumBased, - accountId: accountId - ) - } -} diff --git a/fearless/Common/Helpers/ChainSettingsRepositoryFactory.swift b/fearless/Common/Helpers/ChainSettingsRepositoryFactory.swift index 97ec6abb9e..5e5479121f 100644 --- a/fearless/Common/Helpers/ChainSettingsRepositoryFactory.swift +++ b/fearless/Common/Helpers/ChainSettingsRepositoryFactory.swift @@ -1,5 +1,6 @@ import Foundation import RobinHood +import SSFAccountManagmentStorage final class ChainSettingsRepositoryFactory { let storageFacade: StorageFacadeProtocol diff --git a/fearless/Common/Helpers/NumberFormatterFactoryProtocol.swift b/fearless/Common/Helpers/NumberFormatterFactoryProtocol.swift new file mode 100644 index 0000000000..37af91e27c --- /dev/null +++ b/fearless/Common/Helpers/NumberFormatterFactoryProtocol.swift @@ -0,0 +1,67 @@ +import Foundation +import SoraFoundation + +public protocol NumberFormatterFactoryProtocol { + func createInputFormatter(for asset: WalletAsset?) -> LocalizableResource + func createDisplayFormatter(for asset: WalletAsset?) -> LocalizableResource + func createTokenFormatter(for asset: WalletAsset?) -> LocalizableResource + func createFeeTokenFormatter(for asset: WalletAsset?) -> LocalizableResource +} + +public extension NumberFormatterFactoryProtocol { + func createInputFormatter(for asset: WalletAsset?) -> LocalizableResource { + let numberFormatter = NumberFormatter() + numberFormatter.usesGroupingSeparator = true + numberFormatter.numberStyle = .decimal + + if let asset = asset { + numberFormatter.maximumFractionDigits = Int(asset.precision) + } + + numberFormatter.roundingMode = .down + + return numberFormatter.localizableResource() + } + + func createDisplayFormatter(for asset: WalletAsset?) -> LocalizableResource { + let formatter = createDisplayNumberFormatter(for: asset) + + let result: LocalizableResource = LocalizableResource { locale in + formatter.locale = locale + return formatter + } + + return result + } + + func createTokenFormatter(for asset: WalletAsset?) -> LocalizableResource { + let numberFormatter = createDisplayNumberFormatter(for: asset) + + let formatter = TokenFormatter(decimalFormatter: numberFormatter, tokenSymbol: asset?.symbol ?? "") + + return LocalizableResource { locale in + formatter.locale = locale + return formatter + } + } + + func createFeeTokenFormatter(for asset: WalletAsset?) -> LocalizableResource { + createTokenFormatter(for: asset) + } + + private func createDisplayNumberFormatter(for asset: WalletAsset?) -> NumberFormatter { + let numberFormatter = NumberFormatter() + numberFormatter.usesGroupingSeparator = true + numberFormatter.numberStyle = .decimal + + if let asset = asset { + numberFormatter.maximumFractionDigits = Int(asset.precision) + } + + numberFormatter.roundingMode = .down + + return numberFormatter + } +} + +struct NumberFormatterFactory: NumberFormatterFactoryProtocol {} diff --git a/fearless/Common/Helpers/SubstrateRepositoryFactory.swift b/fearless/Common/Helpers/SubstrateRepositoryFactory.swift index a41f588113..a2cf6d43b6 100644 --- a/fearless/Common/Helpers/SubstrateRepositoryFactory.swift +++ b/fearless/Common/Helpers/SubstrateRepositoryFactory.swift @@ -1,10 +1,12 @@ import Foundation import RobinHood +import SSFSingleValueCache +import SSFAccountManagmentStorage protocol SubstrateRepositoryFactoryProtocol { func createChainStorageItemRepository() -> AnyDataProviderRepository func createStashItemRepository() -> AnyDataProviderRepository - func createSingleValueRepository() -> AnyDataProviderRepository + func createSingleValueRepository() throws -> AnyDataProviderRepository } final class SubstrateRepositoryFactory: SubstrateRepositoryFactoryProtocol { @@ -42,10 +44,8 @@ final class SubstrateRepositoryFactory: SubstrateRepositoryFactoryProtocol { return AnyDataProviderRepository(repository) } - func createSingleValueRepository() -> AnyDataProviderRepository { - let repository: CoreDataRepository = - storageFacade.createRepository() - + func createSingleValueRepository() throws -> AnyDataProviderRepository { + let repository: CoreDataRepository = try SingleValueCacheRepositoryFactoryDefault().createSingleValueCacheRepository() return AnyDataProviderRepository(repository) } } diff --git a/fearless/Common/Helpers/TransactionHistoryMergeManager.swift b/fearless/Common/Helpers/TransactionHistoryMergeManager.swift index af77593c39..46eafd6051 100644 --- a/fearless/Common/Helpers/TransactionHistoryMergeManager.swift +++ b/fearless/Common/Helpers/TransactionHistoryMergeManager.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import IrohaCrypto import SSFModels diff --git a/fearless/Common/Helpers/WalletLoggerProtocol.swift b/fearless/Common/Helpers/WalletLoggerProtocol.swift new file mode 100644 index 0000000000..1358f5375b --- /dev/null +++ b/fearless/Common/Helpers/WalletLoggerProtocol.swift @@ -0,0 +1,9 @@ +import Foundation + +public protocol WalletLoggerProtocol { + func verbose(message: String, file: String, function: String, line: Int) + func debug(message: String, file: String, function: String, line: Int) + func info(message: String, file: String, function: String, line: Int) + func warning(message: String, file: String, function: String, line: Int) + func error(message: String, file: String, function: String, line: Int) +} diff --git a/fearless/Common/Localization/L10n.swift b/fearless/Common/Localization/L10n.swift new file mode 100644 index 0000000000..2c935c420a --- /dev/null +++ b/fearless/Common/Localization/L10n.swift @@ -0,0 +1,309 @@ +import Foundation + +// swiftlint:disable all +public enum L10n { + static var sharedLanguage = WalletLanguage.defaultLanguage + + public enum Account { + /// Account details + public static var detailsTitle: String { localize("account.details_title") } + } + + public enum Amount { + /// Transaction fee + public static var defaultFee: String { localize("amount.defaultFee") } + /// Transaction fee %@ + public static func fee(_ p1: String) -> String { + localize("amount.fee", p1) + } + + /// Set Amount + public static var moduleTitle: String { localize("amount.module_title") } + /// Amount to send + public static var send: String { localize("amount.send") } + /// Amount + public static var title: String { localize("amount.title") } + /// Total amount + public static var total: String { localize("amount.total") } + + public enum Error { + /// Sorry, we couldn't find asset information you want to send. Please, try again later. + public static var asset: String { localize("amount.error.asset") } + /// Sorry, balance checking request failed. Please, try again later. + public static var balance: String { localize("amount.error.balance") } + /// Sorry, you don't have enough funds to transfer specified amount. + public static var noFunds: String { localize("amount.error.no_funds") } + /// Sorry, minimal operation amount is %@. + public static func operationMinLimit(_ p1: String) -> String { + localize("amount.error.operation_min_limit", p1) + } + + /// Sorry, we couldn't contact transfer provider. Please, try again later. + public static var transfer: String { localize("amount.error.transfer") } + } + } + + public enum AssetSelection { + /// No asset + public static var noAsset: String { localize("asset_selection.no_asset") } + } + + public enum Common { + /// My account is: + public static var accountShare: String { localize("common.account_share") } + /// All + public static var all: String { localize("common.all") } + /// Cancel + public static var cancel: String { localize("common.cancel") } + /// Close + public static var close: String { localize("common.close") } + /// Copy + public static var copy: String { localize("common.copy") } + /// Description + public static var description: String { localize("common.description") } + /// Description (optional) + public static var descriptionOptional: String { localize("common.description_optional") } + /// Done + public static var done: String { localize("common.done") } + /// Error + public static var error: String { localize("common.error") } + /// %1$@ %2$@ + public static func fullName(_ p1: String, _ p2: String) -> String { + localize("common.full_name", p1, p2) + } + + /// Incoming + public static var incoming: String { localize("common.incoming") } + /// Next + public static var next: String { localize("common.next") } + /// Not now + public static var notNow: String { localize("common.not_now") } + /// OK + public static var ok: String { localize("common.ok") } + /// Open settings + public static var openSettings: String { localize("common.open_settings") } + /// Select an option + public static var optionsTitle: String { localize("common.options_title") } + /// Outgoing + public static var outgoing: String { localize("common.outgoing") } + /// Receive + public static var receive: String { localize("common.receive") } + /// Search + public static var search: String { localize("common.search") } + /// Send + public static var send: String { localize("common.send") } + /// Show less + public static var showLess: String { localize("common.show_less") } + /// Show more + public static var showMore: String { localize("common.show_more") } + /// Today + public static var today: String { localize("common.today") } + /// Yesterday + public static var yesterday: String { localize("common.yesterday") } + + public enum Input { + /// %@ lowercase hex symbols starting with 0x + public static func validatorHint(_ p1: String) -> String { + localize("common.input.validator_hint", p1) + } + + /// Maximum %@ symbols + public static func validatorMaxHint(_ p1: String) -> String { + localize("common.input.validator_max_hint", p1) + } + } + } + + public enum Confirmation { + /// Please check and confirm details + public static var hint: String { localize("confirmation.hint") } + /// Confirmation + public static var title: String { localize("confirmation.title") } + + public enum Title { + /// Confirm Transaction + public static var v1: String { localize("confirmation.title.v1") } + } + } + + public enum Contacts { + /// Select recipient + public static var moduleTitle: String { localize("contacts.module_title") } + /// Scan QR code + public static var scan: String { localize("contacts.scan") } + /// Search results + public static var searchResults: String { localize("contacts.search_results") } + /// Contacts + public static var title: String { localize("contacts.title") } + } + + public enum Filter { + /// Assets + public static var assets: String { localize("filter.assets") } + /// Date range + public static var dateRange: String { localize("filter.dateRange") } + /// From + public static var from: String { localize("filter.from") } + /// Reset + public static var reset: String { localize("filter.reset") } + /// Set filter + public static var title: String { localize("filter.title") } + /// To + public static var to: String { localize("filter.to") } + /// Type + public static var type: String { localize("filter.type") } + } + + public enum History { + /// Recent events + public static var title: String { localize("history.title") } + } + + public enum InvoiceScan { + /// Scan code from receiver + public static var scan: String { localize("invoice_scan.scan") } + /// Scan QR + public static var title: String { localize("invoice_scan.title") } + /// Upload from gallery + public static var upload: String { localize("invoice_scan.upload") } + + public enum Error { + /// Unfortunately, access to the camera is restricted. + public static var cameraRestricted: String { localize("invoice_scan.error.camera_restricted") } + /// Unfortunately, you denied access to camera previously. Would you like to allow access now? + public static var cameraRestrictedPreviously: String { localize("invoice_scan.error.camera_restricted_previously") } + /// Camera Access + public static var cameraTitle: String { localize("invoice_scan.error.camera_title") } + /// Can't extract receiver's data + public static var extractFail: String { localize("invoice_scan.error.extract_fail") } + /// Unfortunately, access to the photos is restricted. + public static var galleryRestricted: String { localize("invoice_scan.error.gallery_restricted") } + /// Unfortunately, you denied access to photos previously. Would you like to allow access now? + public static var galleryRestrictedPreviously: String { localize("invoice_scan.error.gallery_restricted_previously") } + /// Photos Access + public static var galleryTitle: String { localize("invoice_scan.error.gallery_title") } + /// Can't process selected image + public static var invalidImage: String { localize("invoice_scan.error.invalid_image") } + /// You can't send to yourself + public static var match: String { localize("invoice_scan.error.match") } + /// QR can't be decoded + public static var noInfo: String { localize("invoice_scan.error.no_info") } + /// Please, check internet connection + public static var noInternet: String { localize("invoice_scan.error.no_internet") } + /// Receiver couldn't be found + public static var noReceiver: String { localize("invoice_scan.error.no_receiver") } + /// Can't find a user from QR + public static var userNotFound: String { localize("invoice_scan.error.user_not_found") } + } + } + + public enum Operation { + /// Transaction fee + public static var feeTitle: String { localize("operation.fee_title") } + } + + public enum Receive { + /// Can't generate QR code + public static var errorQrGeneration: String { localize("receive.error_qr_generation") } + /// Receive assets + public static var title: String { localize("receive.title") } + } + + public enum Status { + /// Pending + public static var pending: String { localize("status.pending") } + /// Rejected + public static var rejected: String { localize("status.rejected") } + /// Success + public static var success: String { localize("status.success") } + /// Status + public static var title: String { localize("status.title") } + } + + public enum Transaction { + /// Date and time + public static var date: String { localize("transaction.date") } + /// Transaction details + public static var details: String { localize("transaction.details") } + /// All done + public static var done: String { localize("transaction.done") } + /// Fee + public static var fee: String { localize("transaction.fee") } + /// Transaction ID + public static var id: String { localize("transaction.id") } + /// Funds are being sent + public static var pendingDescription: String { localize("transaction.pending_description") } + /// Reason + public static var reason: String { localize("transaction.reason") } + /// Recipient + public static var recipient: String { localize("transaction.recipient") } + /// Recipient ID + public static var recipientId: String { localize("transaction.recipient_id") } + /// Send again + public static var sendAgain: String { localize("transaction.send_again") } + /// Send back + public static var sendBack: String { localize("transaction.send_back") } + /// Sender + public static var sender: String { localize("transaction.sender") } + /// Sender ID + public static var senderId: String { localize("transaction.sender_id") } + /// Amount sent + public static var sent: String { localize("transaction.sent") } + /// Type + public static var type: String { localize("transaction.type") } + + public enum Details { + /// Transaction Details + public static var v1: String { localize("transaction.details.v1") } + } + + public enum Error { + /// Transaction failed. Please, try again later. + public static var fail: String { localize("transaction.error.fail") } + } + } + + public enum Withdraw { + /// Withdraw + public static var title: String { localize("withdraw.title") } + /// Total amount %@%@ + public static func totalAmount(_ p1: String, _ p2: String) -> String { + localize("withdraw.total_amount", p1, p2) + } + + public enum Error { + /// Sorry, balance checking request failed. Please, try again later. + public static var balance: String { localize("withdraw.error.balance") } + /// Sorry, we couldn't contact withdraw provider. Please, try again later. + public static var connection: String { localize("withdraw.error.connection") } + /// Withdraw failed. Please, try again later. + public static var fail: String { localize("withdraw.error.fail") } + /// Sorry, we couldn't find asset information you want to send. Please, try again later. + public static var noAsset: String { localize("withdraw.error.no_asset") } + /// Sorry, you don't have enough funds to transfer specified amount. + public static var tooPoor: String { localize("withdraw.error.too_poor") } + } + } +} + +private extension L10n { + static func localize(_ key: String, _ args: CVarArg...) -> String { + let format = getFormat(for: key, localization: sharedLanguage.rawValue) + return String(format: format, arguments: args) + } + + static func getFormat(for key: String, localization: String) -> String { + let bundle = Bundle(for: BundleLoadHelper.self) + + guard + let path = bundle.path(forResource: localization, ofType: "lproj"), + let langBundle = Bundle(path: path) else { + return "" + } + + return NSLocalizedString(key, tableName: nil, bundle: langBundle, value: "", comment: "") + } +} + +private final class BundleLoadHelper {} +// swiftlint:enable all diff --git a/fearless/Common/Localization/WalletLanguage.swift b/fearless/Common/Localization/WalletLanguage.swift new file mode 100644 index 0000000000..82348c064c --- /dev/null +++ b/fearless/Common/Localization/WalletLanguage.swift @@ -0,0 +1,35 @@ +import Foundation + +private let kDefaultsKeyName = "l10n_lang" + +public enum WalletLanguage: String, CaseIterable { + case english = "en" + case japan = "ja" + case russian = "ru" + case spanishCo = "es-CO" + case khmer = "km" + case bashkir = "ba-RU" + case italian = "it" + case french = "fr" + case ukrainian = "uk" + case chineseSimplified = "zh-Hans" + case chineseTaiwan = "zh-Hant-TW" + case croatian = "hr" + case estonian = "et" + case filipino = "fil" + case finnish = "fi" + case indonesian = "id" + case korean = "ko" + case malay = "ms" + case spanishEs = "es" + case swedish = "sv" + case thai = "th" +} + +public extension WalletLanguage { + static var defaultLanguage: WalletLanguage { + .english + } +} + +private final class BundleLoadHelper {} diff --git a/fearless/Common/Model/AccountItem.swift b/fearless/Common/Model/AccountItem.swift deleted file mode 100644 index 8dbf8f122b..0000000000 --- a/fearless/Common/Model/AccountItem.swift +++ /dev/null @@ -1,46 +0,0 @@ -import Foundation - -enum CryptoType: UInt8, Codable, CaseIterable { - case sr25519 - case ed25519 - case ecdsa - - var stringValue: String { - switch self { - case .sr25519: - return "sr25519" - case .ed25519: - return "ed25519" - case .ecdsa: - return "ecdsa" - } - } - - init?(rawValue: String) { - switch rawValue { - case "sr25519": - self = .sr25519 - case "ed25519": - self = .ed25519 - case "ecdsa": - self = .ecdsa - default: - return nil - } - } -} - -@available(*, deprecated, message: "Use MetaAccount instead") -struct AccountItem: Codable, Equatable { - enum CodingKeys: String, CodingKey { - case address - case cryptoType - case username - case publicKeyData - } - - let address: String - let cryptoType: CryptoType - let username: String - let publicKeyData: Data -} diff --git a/fearless/Common/Model/AmountDecimal.swift b/fearless/Common/Model/AmountDecimal.swift new file mode 100644 index 0000000000..79091e99ef --- /dev/null +++ b/fearless/Common/Model/AmountDecimal.swift @@ -0,0 +1,42 @@ +import Foundation + +public enum AmountDecimalError: Error { + case invalidStringValue +} + +public struct AmountDecimal: Codable, Equatable { + public let decimalValue: Decimal + + public var stringValue: String { + (decimalValue as NSNumber).stringValue + } + + public init(value: Decimal) { + decimalValue = value + } + + public init?(string: String) { + guard let value = Decimal(string: string) else { + return nil + } + + self.init(value: value) + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + let stringValue = try container.decode(String.self) + + guard let value = Decimal(string: stringValue) else { + throw AmountDecimalError.invalidStringValue + } + + decimalValue = value + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(stringValue) + } +} diff --git a/fearless/Common/Model/AssetTransactionData.swift b/fearless/Common/Model/AssetTransactionData.swift new file mode 100644 index 0000000000..29ef0e7ee9 --- /dev/null +++ b/fearless/Common/Model/AssetTransactionData.swift @@ -0,0 +1,86 @@ +import Foundation + +public enum AssetTransactionStatus: String, Codable { + case pending = "PENDING" + case commited = "COMMITTED" + case rejected = "REJECTED" +} + +public struct AssetTransactionData: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case transactionId + case status + case assetId + case peerId + case peerName + case peerFirstName + case peerLastName + case details + case amount + case fees + case timestamp + case type + case reason + case context + } + + public let transactionId: String + public let status: AssetTransactionStatus + public let assetId: String + public let peerId: String + public let peerFirstName: String? + public let peerLastName: String? + public let peerName: String? + public let details: String + public let amount: AmountDecimal + public let fees: [AssetTransactionFee] + public let timestamp: Int64 + public let type: String + public let reason: String? + public let context: [String: String]? + + public init( + transactionId: String, + status: AssetTransactionStatus, + assetId: String, + peerId: String, + peerFirstName: String?, + peerLastName: String?, + peerName: String?, + details: String, + amount: AmountDecimal, + fees: [AssetTransactionFee], + timestamp: Int64, + type: String, + reason: String?, + context: [String: String]? + ) { + self.transactionId = transactionId + self.status = status + self.assetId = assetId + self.peerId = peerId + self.peerFirstName = peerFirstName + self.peerLastName = peerLastName + self.peerName = peerName + self.details = details + self.amount = amount + self.fees = fees + self.timestamp = timestamp + self.type = type + self.reason = reason + self.context = context + } +} + +public struct AssetTransactionPageData: Codable, Equatable { + public let transactions: [AssetTransactionData] + public let context: PaginationContext? + + public init( + transactions: [AssetTransactionData], + context: PaginationContext? = nil + ) { + self.transactions = transactions + self.context = context + } +} diff --git a/fearless/Common/Model/AssetTransactionFee.swift b/fearless/Common/Model/AssetTransactionFee.swift new file mode 100644 index 0000000000..e3076b72ef --- /dev/null +++ b/fearless/Common/Model/AssetTransactionFee.swift @@ -0,0 +1,15 @@ +import Foundation + +public struct AssetTransactionFee: Codable, Equatable { + public let identifier: String + public let assetId: String + public let amount: AmountDecimal + public let context: [String: String]? + + public init(identifier: String, assetId: String, amount: AmountDecimal, context: [String: String]?) { + self.identifier = identifier + self.assetId = assetId + self.amount = amount + self.context = context + } +} diff --git a/fearless/Common/Model/EmptyAccountIcon.swift b/fearless/Common/Model/EmptyAccountIcon.swift index 10b58b0878..7f1fb88068 100644 --- a/fearless/Common/Model/EmptyAccountIcon.swift +++ b/fearless/Common/Model/EmptyAccountIcon.swift @@ -1,4 +1,4 @@ -import Foundation +import UIKit import SSFUtils struct EmptyAccountIcon: DrawableIcon { diff --git a/fearless/Common/Model/ManagedAccountItem.swift b/fearless/Common/Model/ManagedAccountItem.swift index a721dad371..0cd6cf9bb9 100644 --- a/fearless/Common/Model/ManagedAccountItem.swift +++ b/fearless/Common/Model/ManagedAccountItem.swift @@ -1,5 +1,6 @@ import Foundation import IrohaCrypto +import SSFModels @available(*, deprecated, message: "Use MetaAccount instead") struct ManagedAccountItem: Equatable { diff --git a/fearless/Common/Model/Pagination.swift b/fearless/Common/Model/Pagination.swift new file mode 100644 index 0000000000..f558c6368d --- /dev/null +++ b/fearless/Common/Model/Pagination.swift @@ -0,0 +1,13 @@ +import Foundation + +public typealias PaginationContext = [String: String] + +public struct Pagination: Codable, Equatable { + public let context: PaginationContext? + public let count: Int + + public init(count: Int, context: [String: String]? = nil) { + self.count = count + self.context = context + } +} diff --git a/fearless/Common/Model/ParachainStakingCandidate.swift b/fearless/Common/Model/ParachainStakingCandidate.swift index a9087c8c3b..08ecd04c3e 100644 --- a/fearless/Common/Model/ParachainStakingCandidate.swift +++ b/fearless/Common/Model/ParachainStakingCandidate.swift @@ -1,5 +1,4 @@ import Foundation -import CommonWallet struct ParachainStakingCandidateInfo: Equatable { let address: AccountAddress diff --git a/fearless/Common/Model/ParachainStakingDelegation.swift b/fearless/Common/Model/ParachainStakingDelegation.swift index c12c548f14..7dc1995f58 100644 --- a/fearless/Common/Model/ParachainStakingDelegation.swift +++ b/fearless/Common/Model/ParachainStakingDelegation.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import BigInt import SSFUtils diff --git a/fearless/Common/Model/ReceiveInfo.swift b/fearless/Common/Model/ReceiveInfo.swift new file mode 100644 index 0000000000..c52f2eebf5 --- /dev/null +++ b/fearless/Common/Model/ReceiveInfo.swift @@ -0,0 +1,15 @@ +import Foundation + +public struct ReceiveInfo: Codable, Equatable { + public var accountId: String + public var assetId: String? + public var amount: AmountDecimal? + public var details: String? + + public init(accountId: String, assetId: String?, amount: AmountDecimal?, details: String?) { + self.accountId = accountId + self.assetId = assetId + self.amount = amount + self.details = details + } +} diff --git a/fearless/Common/Model/SearchData.swift b/fearless/Common/Model/SearchData.swift new file mode 100644 index 0000000000..40c84ca495 --- /dev/null +++ b/fearless/Common/Model/SearchData.swift @@ -0,0 +1,20 @@ +import Foundation + +public struct SearchData: Codable, Equatable { + public let accountId: String + public let firstName: String + public let lastName: String + public let context: [String: String]? + + public init( + accountId: String, + firstName: String, + lastName: String, + context: [String: String]? = nil + ) { + self.accountId = accountId + self.firstName = firstName + self.lastName = lastName + self.context = context + } +} diff --git a/fearless/Common/Model/TotalRewardItem.swift b/fearless/Common/Model/TotalRewardItem.swift index 0fb1ab8c2c..8393212ef3 100644 --- a/fearless/Common/Model/TotalRewardItem.swift +++ b/fearless/Common/Model/TotalRewardItem.swift @@ -1,5 +1,4 @@ import Foundation -import CommonWallet struct TotalRewardItem: Codable, Equatable { let address: String diff --git a/fearless/Common/Model/WalletAsset.swift b/fearless/Common/Model/WalletAsset.swift new file mode 100644 index 0000000000..a36cf918af --- /dev/null +++ b/fearless/Common/Model/WalletAsset.swift @@ -0,0 +1,51 @@ +import Foundation +import SoraFoundation + +public struct WalletAssetModes: OptionSet { + public static let view = WalletAssetModes(rawValue: 1 << 0) + public static let transfer = WalletAssetModes(rawValue: 1 << 1) + public static let all: WalletAssetModes = [.view, .transfer] + + public typealias RawValue = UInt8 + + public let rawValue: UInt8 + + public init(rawValue: RawValue) { + self.rawValue = rawValue + } +} + +public struct WalletAsset { + public let symbol: String + public let name: LocalizableResource + public let identifier: String + public let precision: Int16 + public let platform: LocalizableResource? + public let modes: WalletAssetModes + + public init( + identifier: String, + name: LocalizableResource, + platform: LocalizableResource? = nil, + symbol: String, + precision: Int16, + modes: WalletAssetModes = .all + ) { + self.identifier = identifier + self.name = name + self.symbol = symbol + self.precision = precision + self.platform = platform + self.modes = modes + } +} + +extension WalletAsset: Hashable { + public static func == (lhs: WalletAsset, rhs: WalletAsset) -> Bool { + lhs.identifier == rhs.identifier + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(identifier) + } +} diff --git a/fearless/Common/Model/WalletHistoryRequest.swift b/fearless/Common/Model/WalletHistoryRequest.swift new file mode 100644 index 0000000000..00e8f325f4 --- /dev/null +++ b/fearless/Common/Model/WalletHistoryRequest.swift @@ -0,0 +1,15 @@ +import Foundation + +public struct WalletHistoryRequest: Codable, Equatable { + public var assets: [String]? + public var filter: String? + public var fromDate: Date? + public var toDate: Date? + public var type: String? + + public init(assets: [String]) { + self.assets = assets + } + + public init() {} +} diff --git a/fearless/Common/Model/WalletTransactionType.swift b/fearless/Common/Model/WalletTransactionType.swift new file mode 100644 index 0000000000..1db7e6b048 --- /dev/null +++ b/fearless/Common/Model/WalletTransactionType.swift @@ -0,0 +1,46 @@ +import Foundation +import SoraFoundation + +public struct WalletTransactionType: Equatable { + public let backendName: String + public let displayName: LocalizableResource + public let isIncome: Bool + public let typeIcon: UIImage? + + public init(backendName: String, displayName: LocalizableResource, isIncome: Bool, typeIcon: UIImage?) { + self.backendName = backendName + self.displayName = displayName + self.isIncome = isIncome + self.typeIcon = typeIcon + } + + public static func == (lhs: WalletTransactionType, rhs: WalletTransactionType) -> Bool { + lhs.backendName == rhs.backendName + } +} + +public extension WalletTransactionType { + static var incoming: WalletTransactionType { + WalletTransactionType( + backendName: "INCOMING", + displayName: LocalizableResource { _ in L10n.Common.incoming }, + isIncome: true, + typeIcon: nil + ) + } + + static var outgoing: WalletTransactionType { + WalletTransactionType( + backendName: "OUTGOING", + displayName: LocalizableResource { _ in L10n.Common.outgoing }, + isIncome: false, + typeIcon: nil + ) + } +} + +extension WalletTransactionType { + static var required: [WalletTransactionType] { + [.outgoing, .incoming] + } +} diff --git a/fearless/Common/Network/BlockExplorer/Giantsquid/GiantsquidBond.swift b/fearless/Common/Network/BlockExplorer/Giantsquid/GiantsquidBond.swift index 3a2bd9ecd8..9cdc3b7081 100644 --- a/fearless/Common/Network/BlockExplorer/Giantsquid/GiantsquidBond.swift +++ b/fearless/Common/Network/BlockExplorer/Giantsquid/GiantsquidBond.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SSFModels struct GiantsquidBond: Decodable { diff --git a/fearless/Common/Network/BlockExplorer/Giantsquid/GiantsquidExtrinsic.swift b/fearless/Common/Network/BlockExplorer/Giantsquid/GiantsquidExtrinsic.swift index 5bfedc924c..bf11abb3a0 100644 --- a/fearless/Common/Network/BlockExplorer/Giantsquid/GiantsquidExtrinsic.swift +++ b/fearless/Common/Network/BlockExplorer/Giantsquid/GiantsquidExtrinsic.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SSFModels struct GiantsquidExtrinsic: Decodable { diff --git a/fearless/Common/Network/BlockExplorer/Giantsquid/GiantsquidReward.swift b/fearless/Common/Network/BlockExplorer/Giantsquid/GiantsquidReward.swift index 8b4936dedc..baba635fc2 100644 --- a/fearless/Common/Network/BlockExplorer/Giantsquid/GiantsquidReward.swift +++ b/fearless/Common/Network/BlockExplorer/Giantsquid/GiantsquidReward.swift @@ -1,6 +1,6 @@ import Foundation import BigInt -import CommonWallet + import IrohaCrypto import SSFModels diff --git a/fearless/Common/Network/BlockExplorer/Giantsquid/GiantsquidSlash.swift b/fearless/Common/Network/BlockExplorer/Giantsquid/GiantsquidSlash.swift index 39314dbf1e..613fc4a3ec 100644 --- a/fearless/Common/Network/BlockExplorer/Giantsquid/GiantsquidSlash.swift +++ b/fearless/Common/Network/BlockExplorer/Giantsquid/GiantsquidSlash.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SSFModels struct GiantsquidSlash: Decodable { diff --git a/fearless/Common/Network/BlockExplorer/Giantsquid/GiantsquidTransfer.swift b/fearless/Common/Network/BlockExplorer/Giantsquid/GiantsquidTransfer.swift index 186e3dd12e..3754921b4b 100644 --- a/fearless/Common/Network/BlockExplorer/Giantsquid/GiantsquidTransfer.swift +++ b/fearless/Common/Network/BlockExplorer/Giantsquid/GiantsquidTransfer.swift @@ -1,6 +1,6 @@ import Foundation import BigInt -import CommonWallet + import IrohaCrypto import SoraFoundation import SSFModels diff --git a/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryDelegatorHistoryItem.swift b/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryDelegatorHistoryItem.swift index 0aa0a67766..47411b7160 100644 --- a/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryDelegatorHistoryItem.swift +++ b/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryDelegatorHistoryItem.swift @@ -1,6 +1,6 @@ import Foundation import SSFUtils -import CommonWallet + import IrohaCrypto import BigInt diff --git a/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryHistory.swift b/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryHistory.swift index ed6f887697..24fbc7e510 100644 --- a/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryHistory.swift +++ b/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryHistory.swift @@ -1,5 +1,5 @@ import Foundation import SSFUtils -import CommonWallet + import IrohaCrypto import BigInt diff --git a/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryHistoryElement.swift b/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryHistoryElement.swift index bc3b8f1fd4..728eb8241d 100644 --- a/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryHistoryElement.swift +++ b/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryHistoryElement.swift @@ -1,6 +1,6 @@ import Foundation import IrohaCrypto -import CommonWallet + import SSFModels struct SubqueryHistoryElement: Decodable, RewardOrSlashData { diff --git a/fearless/Common/Network/BlockExplorer/Subquery/Sora/TxHistoryItem+WalletRemoteHistoryItemProtocol.swift b/fearless/Common/Network/BlockExplorer/Subquery/Sora/TxHistoryItem+WalletRemoteHistoryItemProtocol.swift index a9fb486ef1..703b2ad2df 100644 --- a/fearless/Common/Network/BlockExplorer/Subquery/Sora/TxHistoryItem+WalletRemoteHistoryItemProtocol.swift +++ b/fearless/Common/Network/BlockExplorer/Subquery/Sora/TxHistoryItem+WalletRemoteHistoryItemProtocol.swift @@ -1,7 +1,7 @@ import Foundation import XNetworking import BigInt -import CommonWallet + import IrohaCrypto import SSFUtils import SSFModels diff --git a/fearless/Common/Network/BlockExplorer/Subsquid/ArrowsquidHistoryResponse.swift b/fearless/Common/Network/BlockExplorer/Subsquid/ArrowsquidHistoryResponse.swift index bcfa7d2931..406cdbf62e 100644 --- a/fearless/Common/Network/BlockExplorer/Subsquid/ArrowsquidHistoryResponse.swift +++ b/fearless/Common/Network/BlockExplorer/Subsquid/ArrowsquidHistoryResponse.swift @@ -1,6 +1,5 @@ import Foundation import SSFModels -import CommonWallet struct ArrowsquidHistoryResponse: Decodable { let historyElements: [ArrowsquidHistoryElement] diff --git a/fearless/Common/Network/BlockExplorer/Subsquid/ReefSubsquidHistory.swift b/fearless/Common/Network/BlockExplorer/Subsquid/ReefSubsquidHistory.swift index d0be74c29a..3e53a88f2d 100644 --- a/fearless/Common/Network/BlockExplorer/Subsquid/ReefSubsquidHistory.swift +++ b/fearless/Common/Network/BlockExplorer/Subsquid/ReefSubsquidHistory.swift @@ -1,6 +1,6 @@ import Foundation import BigInt -import CommonWallet + import IrohaCrypto import SoraFoundation import SSFModels diff --git a/fearless/Common/Network/BlockExplorer/Subsquid/SoraSubsquidHistoryResponse.swift b/fearless/Common/Network/BlockExplorer/Subsquid/SoraSubsquidHistoryResponse.swift index 71f4db1c99..15598d9c1f 100644 --- a/fearless/Common/Network/BlockExplorer/Subsquid/SoraSubsquidHistoryResponse.swift +++ b/fearless/Common/Network/BlockExplorer/Subsquid/SoraSubsquidHistoryResponse.swift @@ -1,6 +1,6 @@ import Foundation import SSFModels -import CommonWallet + import SSFUtils struct SoraSubsquidPageInfo: Decodable { diff --git a/fearless/Common/Network/BlockExplorer/Subsquid/SubsquidHistoryResponse.swift b/fearless/Common/Network/BlockExplorer/Subsquid/SubsquidHistoryResponse.swift index 1c56d7156e..2480f91a79 100644 --- a/fearless/Common/Network/BlockExplorer/Subsquid/SubsquidHistoryResponse.swift +++ b/fearless/Common/Network/BlockExplorer/Subsquid/SubsquidHistoryResponse.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SSFModels struct SubsquidHistoryResponse: Decodable { diff --git a/fearless/Common/Network/JSONRPC/PagedKeysRequest.swift b/fearless/Common/Network/JSONRPC/PagedKeysRequest.swift index 7f957ec4fd..168d122c96 100644 --- a/fearless/Common/Network/JSONRPC/PagedKeysRequest.swift +++ b/fearless/Common/Network/JSONRPC/PagedKeysRequest.swift @@ -1,6 +1,6 @@ import Foundation -struct PagedKeysRequest: Encodable { +struct PagedKeysRequest: Codable { let key: String let count: UInt32 let offset: String? diff --git a/fearless/Common/Network/JSONRPC/StorageQuery.swift b/fearless/Common/Network/JSONRPC/StorageQuery.swift index 7caa2b63a8..68598e1ae5 100644 --- a/fearless/Common/Network/JSONRPC/StorageQuery.swift +++ b/fearless/Common/Network/JSONRPC/StorageQuery.swift @@ -1,6 +1,6 @@ import Foundation -struct StorageQuery: Encodable { +struct StorageQuery: Codable { let keys: [Data] let blockHash: Data? diff --git a/fearless/Common/Network/Subscan/SubscanHistoryItem+Wallet.swift b/fearless/Common/Network/Subscan/SubscanHistoryItem+Wallet.swift index 8c91267261..b0b8d6b3d5 100644 --- a/fearless/Common/Network/Subscan/SubscanHistoryItem+Wallet.swift +++ b/fearless/Common/Network/Subscan/SubscanHistoryItem+Wallet.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import IrohaCrypto import SSFModels diff --git a/fearless/Common/Network/Wallet/AccountOperationFactory.swift b/fearless/Common/Network/Wallet/AccountOperationFactory.swift index c293dd1d01..121a66cef9 100644 --- a/fearless/Common/Network/Wallet/AccountOperationFactory.swift +++ b/fearless/Common/Network/Wallet/AccountOperationFactory.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import RobinHood import IrohaCrypto import BigInt diff --git a/fearless/Common/Network/Wallet/WalletRemoteHistoryProtocols.swift b/fearless/Common/Network/Wallet/WalletRemoteHistoryProtocols.swift index 7d65a0df08..f79b0dcc94 100644 --- a/fearless/Common/Network/Wallet/WalletRemoteHistoryProtocols.swift +++ b/fearless/Common/Network/Wallet/WalletRemoteHistoryProtocols.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import IrohaCrypto import RobinHood import SSFModels diff --git a/fearless/Common/Operation/MetaAccountOperationFactory.swift b/fearless/Common/Operation/MetaAccountOperationFactory.swift index cca9571449..6b01e9e000 100644 --- a/fearless/Common/Operation/MetaAccountOperationFactory.swift +++ b/fearless/Common/Operation/MetaAccountOperationFactory.swift @@ -4,6 +4,7 @@ import IrohaCrypto import RobinHood import SoraKeystore import SSFModels +import SSFCrypto protocol MetaAccountOperationFactoryProtocol { func newMetaAccountOperation(request: MetaAccountImportMnemonicRequest, isBackuped: Bool) -> BaseOperation diff --git a/fearless/Common/Protocols/ApplicationSettingsPresentable.swift b/fearless/Common/Protocols/ApplicationSettingsPresentable.swift index e6f87eb699..2db6180af6 100644 --- a/fearless/Common/Protocols/ApplicationSettingsPresentable.swift +++ b/fearless/Common/Protocols/ApplicationSettingsPresentable.swift @@ -1,6 +1,5 @@ import Foundation import UIKit -import CommonWallet protocol ApplicationSettingsPresentable { func askOpenApplicationSettings( diff --git a/fearless/Common/Protocols/FeeViewProtocol.swift b/fearless/Common/Protocols/FeeViewProtocol.swift new file mode 100644 index 0000000000..1c62b91011 --- /dev/null +++ b/fearless/Common/Protocols/FeeViewProtocol.swift @@ -0,0 +1,308 @@ +import Foundation +import SoraUI + +public protocol FeeViewProtocol: AnyObject { + var borderType: BorderType { get set } + + func bind(viewModel: FeeViewModelProtocol) +} + +public typealias BaseFeeView = UIView & FeeViewProtocol + +public enum FeeViewDisplayStyle { + case singleTitle + case separatedDetails +} + +final class FeeView: BaseFeeView { + private(set) var titleLabel = UILabel() + private(set) var detailsLabel: UILabel? + private(set) var editingIconImageView: UIImageView? + private(set) var activityIndicator = UIActivityIndicatorView() + private(set) var borderedView = BorderedContainerView() + + private(set) var viewModel: FeeViewModelProtocol? + + var borderType: BorderType { + get { + borderedView.borderType + } + + set { + borderedView.borderType = newValue + } + } + + var displayType: FeeViewDisplayStyle = .singleTitle { + didSet { + applyViewModel() + } + } + + var editIndicatorIcon: UIImage? { + didSet { + editingIconImageView?.image = editIndicatorIcon + + invalidateLayout() + } + } + + var detailsColor: UIColor? { + didSet { + detailsLabel?.textColor = detailsColor + } + } + + var detailsFont: UIFont? { + didSet { + detailsLabel?.font = detailsFont + + invalidateLayout() + } + } + + var contentInsets = UIEdgeInsets.zero { + didSet { + invalidateLayout() + } + } + + var horizontalSpacing: CGFloat = 8.0 { + didSet { + invalidateLayout() + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = .clear + + addSubview(borderedView) + addSubview(titleLabel) + + activityIndicator.hidesWhenStopped = true + addSubview(activityIndicator) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func bind(viewModel: FeeViewModelProtocol) { + self.viewModel = viewModel + + applyViewModel() + } + + // MARK: Overriding + + override var intrinsicContentSize: CGSize { + var size = CGSize(width: UIView.noIntrinsicMetric, height: 0.0) + + size.height = max(size.height, titleLabel.intrinsicContentSize.height) + size.height = max(size.height, activityIndicator.intrinsicContentSize.height) + + if let detailsLabel = detailsLabel { + size.height = max(size.height, detailsLabel.intrinsicContentSize.height) + } + + if let editingImageView = editingIconImageView { + size.height = max(size.height, editingImageView.intrinsicContentSize.height) + } + + if size.height > 0.0 { + size.height += contentInsets.top + contentInsets.bottom + } + + return size + } + + override func layoutSubviews() { + super.layoutSubviews() + + borderedView.frame = CGRect(origin: .zero, size: CGSize(width: bounds.width, height: bounds.height)) + + layoutEditingImageViewIfNeeded() + layoutDetailsLabelIfNeeded() + layoutTitleLabel() + layoutActivityIndicator() + } + + private func layoutEditingImageViewIfNeeded() { + guard let imageView = editingIconImageView else { + return + } + + let imageSize = imageView.intrinsicContentSize + let centerOffset = (contentInsets.top - contentInsets.bottom) / 2.0 + + imageView.frame = CGRect( + x: bounds.size.width - contentInsets.right - imageSize.width, + y: bounds.height / 2.0 - imageSize.height / 2.0 + centerOffset, + width: imageSize.width, + height: imageSize.height + ) + } + + private func layoutDetailsLabelIfNeeded() { + guard let detailsLabel = detailsLabel else { + return + } + + let detailsSize = detailsLabel.intrinsicContentSize + + var originX = bounds.width - contentInsets.right - detailsSize.width + + if let imageView = editingIconImageView { + originX -= imageView.frame.size.width + horizontalSpacing + } + + let centerOffset = (contentInsets.top - contentInsets.bottom) / 2.0 + + detailsLabel.frame = CGRect( + x: originX, + y: bounds.height / 2.0 - detailsSize.height / 2.0 + centerOffset, + width: detailsSize.width, + height: detailsSize.height + ) + } + + private func layoutTitleLabel() { + let rightConstraint: CGFloat + + if let detailsLabel = detailsLabel { + rightConstraint = detailsLabel.frame.minX - horizontalSpacing + } else if let editingImageView = editingIconImageView { + rightConstraint = editingImageView.frame.minX - horizontalSpacing + } else { + rightConstraint = bounds.width - contentInsets.right + } + + let titleSize = titleLabel.intrinsicContentSize + + let centerOffset = (contentInsets.top - contentInsets.bottom) / 2.0 + + titleLabel.frame = CGRect( + x: contentInsets.left, + y: bounds.height / 2.0 - titleSize.height / 2.0 + centerOffset, + width: rightConstraint - contentInsets.left, + height: titleSize.height + ) + } + + private func layoutActivityIndicator() { + let activitySize = activityIndicator.intrinsicContentSize + + let centerOffset = (contentInsets.top - contentInsets.bottom) / 2.0 + + switch displayType { + case .singleTitle: + let originX = titleLabel.frame.minX + titleLabel.intrinsicContentSize.width + horizontalSpacing + activityIndicator.frame = CGRect( + x: originX, + y: bounds.height / 2.0 - activitySize.height / 2.0 + centerOffset, + width: activitySize.width, + height: activitySize.height + ) + case .separatedDetails: + let originX: CGFloat + + if let editingImageView = editingIconImageView { + originX = editingImageView.frame.minX - horizontalSpacing - activitySize.width + } else { + originX = bounds.size.width - contentInsets.right - activitySize.width + } + + activityIndicator.frame = CGRect( + x: originX, + y: bounds.height / 2.0 - activitySize.height / 2.0 + centerOffset, + width: activitySize.width, + height: activitySize.height + ) + } + } + + // MARK: Private + + private func invalidateLayout() { + invalidateIntrinsicContentSize() + + if superview != nil { + setNeedsLayout() + } + } + + private func applyViewModel() { + guard let viewModel = viewModel else { + return + } + + let title: String + let details: String + + switch displayType { + case .singleTitle: + title = !viewModel.details.isEmpty ? "\(viewModel.title) \(viewModel.details)" : viewModel.title + details = "" + case .separatedDetails: + title = viewModel.title + details = viewModel.details + } + + titleLabel.text = title + + if !details.isEmpty { + addDetailsLabelIfNeeded() + + detailsLabel?.text = details + } else { + detailsLabel?.removeFromSuperview() + detailsLabel = nil + } + + if viewModel.allowsEditing { + addEditingImageViewIfNeeded() + } else { + editingIconImageView?.removeFromSuperview() + editingIconImageView = nil + } + + if viewModel.isLoading { + activityIndicator.startAnimating() + } else { + activityIndicator.stopAnimating() + } + + detailsLabel?.isHidden = viewModel.isLoading + + invalidateLayout() + } + + private func addDetailsLabelIfNeeded() { + guard detailsLabel == nil else { + return + } + + let detailsLabel = UILabel() + detailsLabel.font = detailsFont + detailsLabel.textColor = detailsColor + addSubview(detailsLabel) + + self.detailsLabel = detailsLabel + } + + private func addEditingImageViewIfNeeded() { + guard editingIconImageView == nil else { + return + } + + let editingImageView = UIImageView() + editingImageView.image = editIndicatorIcon + + addSubview(editingImageView) + + editingIconImageView = editingImageView + } +} diff --git a/fearless/Common/QRCoding/BokoloCashQRInfo.swift b/fearless/Common/QRCoding/BokoloCashQRInfo.swift index 1e11b8b00b..61e9686953 100644 --- a/fearless/Common/QRCoding/BokoloCashQRInfo.swift +++ b/fearless/Common/QRCoding/BokoloCashQRInfo.swift @@ -1,5 +1,5 @@ import Foundation -import SSFUtils +import SSFQRService #if F_RELEASE import MPQRCoreSDK #endif diff --git a/fearless/Common/QRCoding/CexQRInfo.swift b/fearless/Common/QRCoding/CexQRInfo.swift index 60f49b2192..e4f73478a0 100644 --- a/fearless/Common/QRCoding/CexQRInfo.swift +++ b/fearless/Common/QRCoding/CexQRInfo.swift @@ -1,5 +1,5 @@ import Foundation -import SSFUtils +import SSFQRService struct CexQRInfo: QRInfo, Equatable { let address: String diff --git a/fearless/Common/QRCoding/QRCreationOperation.swift b/fearless/Common/QRCoding/QRCreationOperation.swift index fccd350d75..a6a1187e86 100644 --- a/fearless/Common/QRCoding/QRCreationOperation.swift +++ b/fearless/Common/QRCoding/QRCreationOperation.swift @@ -1,4 +1,4 @@ -import Foundation +import UIKit import CoreImage import RobinHood diff --git a/fearless/Common/QRCoding/QRService.swift b/fearless/Common/QRCoding/QRService.swift index d303e51973..8d316cd120 100644 --- a/fearless/Common/QRCoding/QRService.swift +++ b/fearless/Common/QRCoding/QRService.swift @@ -1,5 +1,5 @@ import RobinHood -import Foundation +import UIKit protocol QROperationFactoryProtocol: AnyObject { func createCreationOperation(for payload: Data, qrSize: CGSize) -> QRCreationOperation diff --git a/fearless/Common/QRCoding/SoraQRInfo.swift b/fearless/Common/QRCoding/SoraQRInfo.swift index b972032dea..39151e1850 100644 --- a/fearless/Common/QRCoding/SoraQRInfo.swift +++ b/fearless/Common/QRCoding/SoraQRInfo.swift @@ -1,5 +1,5 @@ import Foundation -import SSFUtils +import SSFQRService struct SoraQRInfo: QRInfo, Equatable { let prefix: String @@ -10,7 +10,7 @@ struct SoraQRInfo: QRInfo, Equatable { let amount: String? init( - prefix: String = SubstrateQR.prefix, + prefix: String = SubstrateQRConstants.prefix, address: String, rawPublicKey: Data, username: String, @@ -29,7 +29,7 @@ struct SoraQRInfo: QRInfo, Equatable { final class SoraQREncoder { private let separator: String - init(separator: String = SubstrateQR.fieldsSeparator) { + init(separator: String = SubstrateQRConstants.fieldsSeparator) { self.separator = separator } @@ -57,7 +57,7 @@ final class SoraQRDecoder: QRDecoderProtocol { throw QRDecoderError.brokenFormat } - let fields = decodedString.components(separatedBy: SubstrateQR.fieldsSeparator) + let fields = decodedString.components(separatedBy: SubstrateQRConstants.fieldsSeparator) guard fields.count >= 3 else { throw QRDecoderError.unexpectedNumberOfFields diff --git a/fearless/Common/Services/ChainRegistry/ChainRegistry.swift b/fearless/Common/Services/ChainRegistry/ChainRegistry.swift index 1f7dfbae8d..61223bcecf 100644 --- a/fearless/Common/Services/ChainRegistry/ChainRegistry.swift +++ b/fearless/Common/Services/ChainRegistry/ChainRegistry.swift @@ -433,11 +433,6 @@ extension ChainRegistry: SSFChainRegistry.ChainRegistryProtocol { return runtimeProvider } -// func getRuntimeProvider(for chainId: SSFModels.ChainModel.Id) -> SSFRuntimeCodingService.RuntimeProviderProtocol? { -// let runtimeProvider = readLock.concurrentlyRead { runtimeProviderPool.getRuntimeProvider(for: chainId) } -// return runtimeProvider -// } - func getSubstrateConnection(for chain: SSFModels.ChainModel) throws -> SSFChainConnection.SubstrateConnection { let connection = getConnection(for: chain.chainId) guard let connection else { diff --git a/fearless/Common/Services/ChainRegistry/ChainRegistryError.swift b/fearless/Common/Services/ChainRegistry/ChainRegistryError.swift index aa2ad282b0..779a105fcf 100644 --- a/fearless/Common/Services/ChainRegistry/ChainRegistryError.swift +++ b/fearless/Common/Services/ChainRegistry/ChainRegistryError.swift @@ -3,4 +3,5 @@ import Foundation enum ChainRegistryError: Error { case connectionUnavailable case runtimeMetadaUnavailable + case chainUnavailable(chainId: String) } diff --git a/fearless/Common/Services/ChainRegistry/ChainRegistryFacade.swift b/fearless/Common/Services/ChainRegistry/ChainRegistryFacade.swift index 29e9ab4181..3dc01e317a 100644 --- a/fearless/Common/Services/ChainRegistry/ChainRegistryFacade.swift +++ b/fearless/Common/Services/ChainRegistry/ChainRegistryFacade.swift @@ -1,5 +1,6 @@ import Foundation +import SSFChainRegistry final class ChainRegistryFacade { - static let sharedRegistry: ChainRegistryProtocol = ChainRegistryFactory.createDefaultRegistry() + static let sharedRegistry: ChainRegistryProtocol & SSFChainRegistry.ChainRegistryProtocol = ChainRegistryFactory.createDefaultRegistry() } diff --git a/fearless/Common/Services/ChainRegistry/ChainRegistryFactory.swift b/fearless/Common/Services/ChainRegistry/ChainRegistryFactory.swift index 31e30650eb..9d2cdaf862 100644 --- a/fearless/Common/Services/ChainRegistry/ChainRegistryFactory.swift +++ b/fearless/Common/Services/ChainRegistry/ChainRegistryFactory.swift @@ -22,7 +22,7 @@ final class ChainRegistryFactory { * - Returns: new instance conforming to `ChainRegistryProtocol`. */ - static func createDefaultRegistry() -> ChainRegistryProtocol { + static func createDefaultRegistry() -> SSFChainRegistry.ChainRegistryProtocol & ChainRegistryProtocol { let repositoryFacade = SubstrateDataStorageFacade.shared return createDefaultRegistry(from: repositoryFacade) } @@ -41,7 +41,7 @@ final class ChainRegistryFactory { */ static func createDefaultRegistry( from repositoryFacade: StorageFacadeProtocol - ) -> ChainRegistryProtocol { + ) -> SSFChainRegistry.ChainRegistryProtocol & ChainRegistryProtocol { let runtimeMetadataRepository: CoreDataRepository = repositoryFacade.createRepository() diff --git a/fearless/Common/Services/ChainRegistry/ChainSyncService.swift b/fearless/Common/Services/ChainRegistry/ChainSyncService.swift index cbb835a8a1..d985598faa 100644 --- a/fearless/Common/Services/ChainRegistry/ChainSyncService.swift +++ b/fearless/Common/Services/ChainRegistry/ChainSyncService.swift @@ -13,7 +13,7 @@ enum ChainSyncServiceError: Error { } final class ChainSyncService { - static let fetchLocalData = false + static let fetchLocalData = true struct SyncChanges { let newOrUpdatedItems: [ChainModel] diff --git a/fearless/Common/Services/GitHubPhishingService/PhishingCheckExecutor.swift b/fearless/Common/Services/GitHubPhishingService/PhishingCheckExecutor.swift deleted file mode 100644 index 9137057386..0000000000 --- a/fearless/Common/Services/GitHubPhishingService/PhishingCheckExecutor.swift +++ /dev/null @@ -1,66 +0,0 @@ -import Foundation -import RobinHood -import CoreData -import SoraFoundation -import CommonWallet - -class PhishingCheckExecutor: WalletCommandProtocol { - private let storage: AnyDataProviderRepository - private let nextActionBlock: () -> Void - private let cancelActionBlock: () -> Void - private let locale: Locale - private let commandFactory: WalletCommandFactoryProtocol? - private let publicKey: String - private let displayName: String - - init( - commandFactory: WalletCommandFactoryProtocol, - storage: AnyDataProviderRepository, - nextAction nextActionBlock: @escaping () -> Void, - cancelAction cancelActionBlock: @escaping () -> Void, - locale: Locale, - publicKey: String, - walletAddress displayName: String - ) { - self.commandFactory = commandFactory - self.storage = storage - self.nextActionBlock = nextActionBlock - self.cancelActionBlock = cancelActionBlock - self.locale = locale - self.publicKey = publicKey - self.displayName = displayName - } - - func execute() throws { - let fetchOperation = storage.fetchOperation( - by: publicKey, - options: RepositoryFetchOptions() - ) - - fetchOperation.completionBlock = { [weak self] in - guard let strongSelf = self else { return } - DispatchQueue.main.async { - if let result = try? fetchOperation.extractResultData() { - guard result != nil else { - strongSelf.nextActionBlock() - return - } - - let alertController = UIAlertController.phishingWarningAlert( - onConfirm: strongSelf.nextActionBlock, - onCancel: strongSelf.cancelActionBlock, - locale: strongSelf.locale, - displayName: strongSelf.displayName - ) - - let presentationCommand = strongSelf.commandFactory?.preparePresentationCommand(for: alertController) - presentationCommand?.presentationStyle = .modal(inNavigation: false) - - try? presentationCommand?.execute() - } - } - } - - OperationManagerFacade.sharedManager.enqueue(operations: [fetchOperation], in: .transient) - } -} diff --git a/fearless/Common/Services/ServiceCoordinator.swift b/fearless/Common/Services/ServiceCoordinator.swift index 0f5125d983..f004f6dbdc 100644 --- a/fearless/Common/Services/ServiceCoordinator.swift +++ b/fearless/Common/Services/ServiceCoordinator.swift @@ -17,7 +17,6 @@ final class ServiceCoordinator { private let scamSyncService: ScamSyncServiceProtocol private let polkaswapSettingsService: PolkaswapSettingsSyncServiceProtocol private let walletConnect: WalletConnectService - private let walletAssetsObserver: WalletAssetsObserver init( walletSettings: SelectedWalletSettings, @@ -25,8 +24,7 @@ final class ServiceCoordinator { githubPhishingService: ApplicationServiceProtocol, scamSyncService: ScamSyncServiceProtocol, polkaswapSettingsService: PolkaswapSettingsSyncServiceProtocol, - walletConnect: WalletConnectService, - walletAssetsObserver: WalletAssetsObserver + walletConnect: WalletConnectService ) { self.walletSettings = walletSettings self.accountInfoService = accountInfoService @@ -34,7 +32,6 @@ final class ServiceCoordinator { self.scamSyncService = scamSyncService self.polkaswapSettingsService = polkaswapSettingsService self.walletConnect = walletConnect - self.walletAssetsObserver = walletAssetsObserver } } @@ -42,7 +39,6 @@ extension ServiceCoordinator: ServiceCoordinatorProtocol { func updateOnAccountChange() { if let seletedMetaAccount = walletSettings.value { accountInfoService.update(selectedMetaAccount: seletedMetaAccount) - walletAssetsObserver.update(wallet: seletedMetaAccount) } } @@ -55,14 +51,12 @@ extension ServiceCoordinator: ServiceCoordinatorProtocol { scamSyncService.syncUp() polkaswapSettingsService.syncUp() walletConnect.setup() - walletAssetsObserver.setup() } func throttle() { githubPhishingService.throttle() accountInfoService.throttle() walletConnect.throttle() - walletAssetsObserver.throttle() } } @@ -110,35 +104,13 @@ extension ServiceCoordinator { eventCenter: EventCenter.shared ) - let runtimeMetadataRepository: AsyncCoreDataRepositoryDefault = - SubstrateDataStorageFacade.shared.createAsyncRepository() - - let ethereumRemoteBalanceFetching = EthereumRemoteBalanceFetching( - chainRegistry: chainRegistry, - repositoryWrapper: ethereumBalanceRepositoryWrapper - ) - - let accountInfoRemote = AccountInfoRemoteServiceDefault( - runtimeItemRepository: AsyncAnyRepository(runtimeMetadataRepository), - ethereumRemoteBalanceFetching: ethereumRemoteBalanceFetching, - chainRegistry: createPackageChainRegistry() - ) - - let walletAssetsObserver = WalletAssetsObserverImpl( - wallet: selectedMetaAccount, - chainRegistry: chainRegistry, - accountInfoRemote: accountInfoRemote, - eventCenter: EventCenter.shared - ) - return ServiceCoordinator( walletSettings: walletSettings, accountInfoService: accountInfoService, githubPhishingService: githubPhishingAPIService, scamSyncService: scamSyncService, polkaswapSettingsService: polkaswapSettingsService, - walletConnect: walletConnect, - walletAssetsObserver: walletAssetsObserver + walletConnect: walletConnect ) } diff --git a/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift b/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift index a99a956c82..10fc8f9b83 100644 --- a/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift +++ b/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift @@ -263,10 +263,15 @@ final class ChainModelMapper { crowdloans = ChainModel.ExternalResource(type: type, url: url) } + var pricing: ChainModel.BlockExplorer? + if let type = entity.pricingApiType, let url = entity.pricingApiUrl { + pricing = ChainModel.BlockExplorer(type: type, url: url) + } + let explorers = createExplorers(from: entity) if staking != nil || history != nil || crowdloans != nil || explorers != nil { - return ChainModel.ExternalApiSet(staking: staking, history: history, crowdloans: crowdloans, explorers: explorers) + return ChainModel.ExternalApiSet(staking: staking, history: history, crowdloans: crowdloans, explorers: explorers, pricing: pricing) } else { return nil } @@ -364,6 +369,9 @@ final class ChainModelMapper { entity.crowdloansApiType = apis?.crowdloans?.type entity.crowdloansApiUrl = apis?.crowdloans?.url + + entity.pricingApiType = apis?.pricing?.type.rawValue + entity.pricingApiUrl = apis?.pricing?.url } private func createChainAssetModelType(from rawValue: String?) -> SubstrateAssetType? { diff --git a/fearless/Common/Storage/EntityToModel/ChainSettingsMapper.swift b/fearless/Common/Storage/EntityToModel/ChainSettingsMapper.swift index f18b199e50..9a42c47527 100644 --- a/fearless/Common/Storage/EntityToModel/ChainSettingsMapper.swift +++ b/fearless/Common/Storage/EntityToModel/ChainSettingsMapper.swift @@ -2,6 +2,7 @@ import Foundation import RobinHood import CoreData import IrohaCrypto +import SSFAccountManagmentStorage enum ChainSettingsMapperError: Error { case missedRequiredFields diff --git a/fearless/Common/Storage/EntityToModel/ManagedMetaAccountMapper.swift b/fearless/Common/Storage/EntityToModel/ManagedMetaAccountMapper.swift index 0323712c60..cfaf9c6089 100644 --- a/fearless/Common/Storage/EntityToModel/ManagedMetaAccountMapper.swift +++ b/fearless/Common/Storage/EntityToModel/ManagedMetaAccountMapper.swift @@ -1,6 +1,7 @@ import Foundation import RobinHood import CoreData +import SSFAccountManagmentStorage final class ManagedMetaAccountMapper { var entityIdentifierFieldName: String { #keyPath(CDMetaAccount.metaId) } diff --git a/fearless/Common/Storage/EntityToModel/MetaAccountMapper.swift b/fearless/Common/Storage/EntityToModel/MetaAccountMapper.swift index bff09c09f1..cc106de2f6 100644 --- a/fearless/Common/Storage/EntityToModel/MetaAccountMapper.swift +++ b/fearless/Common/Storage/EntityToModel/MetaAccountMapper.swift @@ -1,6 +1,7 @@ import Foundation import RobinHood import CoreData +import SSFAccountManagmentStorage final class MetaAccountMapper { var entityIdentifierFieldName: String { #keyPath(CDMetaAccount.metaId) } diff --git a/fearless/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v5.xcdatamodel/contents b/fearless/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v5.xcdatamodel/contents index 3bb8ef9fc5..161db312ed 100644 --- a/fearless/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v5.xcdatamodel/contents +++ b/fearless/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v5.xcdatamodel/contents @@ -38,6 +38,8 @@ + + @@ -132,10 +134,6 @@ - - - - diff --git a/fearless/Common/Storage/UserDataModel.xcdatamodeld/.xccurrentversion b/fearless/Common/Storage/UserDataModel.xcdatamodeld/.xccurrentversion index 73a62eac67..0c67376eba 100644 --- a/fearless/Common/Storage/UserDataModel.xcdatamodeld/.xccurrentversion +++ b/fearless/Common/Storage/UserDataModel.xcdatamodeld/.xccurrentversion @@ -1,8 +1,5 @@ - - _XCCurrentVersionName - MultiassetUserDataModel_v11.xcdatamodel - + diff --git a/fearless/Common/Storage/UserDataStorageFacade.swift b/fearless/Common/Storage/UserDataStorageFacade.swift index 3bfe761d56..877361bfaa 100644 --- a/fearless/Common/Storage/UserDataStorageFacade.swift +++ b/fearless/Common/Storage/UserDataStorageFacade.swift @@ -4,7 +4,7 @@ import CoreData enum UserStorageParams { static let modelVersion: UserStorageVersion = .version12 - static let modelDirectory: String = "UserDataModel.momd" + static let modelDirectory: String = "Modules_SSFAccountManagmentStorage.bundle//UserDataModel.momd" static let databaseName = "UserDataModel.sqlite" static let storageDirectoryURL: URL = { diff --git a/fearless/Common/Substrate/Types/EraRewardPoints.swift b/fearless/Common/Substrate/Types/EraRewardPoints.swift index 59f1fee873..18f74c984d 100644 --- a/fearless/Common/Substrate/Types/EraRewardPoints.swift +++ b/fearless/Common/Substrate/Types/EraRewardPoints.swift @@ -1,4 +1,5 @@ import SSFUtils +import Foundation typealias RewardPoint = UInt32 diff --git a/fearless/Common/View/AccessoryView.swift b/fearless/Common/View/AccessoryView.swift new file mode 100644 index 0000000000..b519b75a41 --- /dev/null +++ b/fearless/Common/View/AccessoryView.swift @@ -0,0 +1,77 @@ +import Foundation +import SoraUI + +public protocol AccessoryViewProtocol: AnyObject { + var contentView: UIView { get } + + var isActionEnabled: Bool { get set } + + var extendsUnderSafeArea: Bool { get } + + func bind(viewModel: AccessoryViewModelProtocol) +} + +public extension AccessoryViewProtocol { + var extendsUnderSafeArea: Bool { false } +} + +final class AccessoryView: UIView { + private enum Constants { + static let titleLeadingWithIcon: CGFloat = 45.0 + static let titleLeadingWithoutIcon: CGFloat = 0.0 + } + + @IBOutlet private(set) var borderView: BorderedContainerView! + @IBOutlet private(set) var iconImageView: UIImageView! + @IBOutlet private(set) var titleLabel: UILabel! + @IBOutlet private var titleLeading: NSLayoutConstraint! + @IBOutlet private(set) var actionButton: RoundedButton! + + private var viewModel: AccessoryViewModelProtocol? +} + +extension AccessoryView: AccessoryViewProtocol { + var contentView: UIView { + self + } + + var isActionEnabled: Bool { + get { + actionButton.isEnabled + } + + set { + let shouldAllowAction = viewModel?.shouldAllowAction ?? true + + if newValue, shouldAllowAction { + actionButton.enable() + } else { + actionButton.disable() + } + } + } + + func bind(viewModel: AccessoryViewModelProtocol) { + iconImageView.image = viewModel.icon + iconImageView.isHidden = (viewModel.icon == nil) + + titleLabel.text = viewModel.title + titleLeading.constant = iconImageView.isHidden ? Constants.titleLeadingWithoutIcon + : Constants.titleLeadingWithIcon + titleLabel.numberOfLines = viewModel.numberOfLines + + actionButton.imageWithTitleView?.title = viewModel.action + + if viewModel.shouldAllowAction { + actionButton.enable() + } else { + actionButton.disable() + } + + actionButton.invalidateLayout() + + self.viewModel = viewModel + + setNeedsLayout() + } +} diff --git a/fearless/Common/View/NetworkFeeFooterView+Protocol.swift b/fearless/Common/View/NetworkFeeFooterView+Protocol.swift index 8990cdab50..d356cabd4f 100644 --- a/fearless/Common/View/NetworkFeeFooterView+Protocol.swift +++ b/fearless/Common/View/NetworkFeeFooterView+Protocol.swift @@ -1,4 +1,4 @@ -import CommonWallet +import UIKit extension NetworkFeeFooterView: AccessoryViewProtocol { var extendsUnderSafeArea: Bool { true } diff --git a/fearless/Common/View/NetworkFeeView+Protocol.swift b/fearless/Common/View/NetworkFeeView+Protocol.swift index 5cc80eecb6..7681f9b2d5 100644 --- a/fearless/Common/View/NetworkFeeView+Protocol.swift +++ b/fearless/Common/View/NetworkFeeView+Protocol.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SoraUI extension NetworkFeeView: FeeViewProtocol { diff --git a/fearless/Common/View/ShimmeredLabel.swift b/fearless/Common/View/ShimmeredLabel.swift index 6a4328a5d9..0c28dcdfbd 100644 --- a/fearless/Common/View/ShimmeredLabel.swift +++ b/fearless/Common/View/ShimmeredLabel.swift @@ -32,12 +32,25 @@ enum ShimmeredLabelState: Hashable { } } -final class ShimmeredLabel: UILabel, ShimmeredProtocol { +final class ShimmeredLabel: SkeletonLabel, ShimmeredProtocol { private var state: ShimmeredLabelState? // MARK: - Public methods - func apply(state: ShimmeredLabelState) { + init() { + super.init(skeletonSize: .zero) + } + + override init(skeletonSize: CGSize) { + super.init(skeletonSize: skeletonSize) + } + + func apply(state: ShimmeredLabelState?) { + guard let state else { + updateTextWithLoading(nil) + return + } + self.state = state switch state { case .stopShimmering: diff --git a/fearless/Common/View/ShimmeredProtocol.swift b/fearless/Common/View/ShimmeredProtocol.swift index b434ac4ad9..ae6d7a6798 100644 --- a/fearless/Common/View/ShimmeredProtocol.swift +++ b/fearless/Common/View/ShimmeredProtocol.swift @@ -52,4 +52,8 @@ extension ShimmeredProtocol { layer.mask = nil layer.removeAnimation(forKey: Constants.animationKey) } + + func startSkeletonAnimation() {} + + func stopSkeletonAnimation() {} } diff --git a/fearless/Common/View/SkeletonLabel.swift b/fearless/Common/View/SkeletonLabel.swift new file mode 100644 index 0000000000..af6672818d --- /dev/null +++ b/fearless/Common/View/SkeletonLabel.swift @@ -0,0 +1,39 @@ +import UIKit +import SoraUI + +class SkeletonLabel: UILabel, SkeletonLoadableView { + var container: UIView { + self + } + + var skeletonSize: CGSize + var skeletonView: SkrullableView? + + init(skeletonSize: CGSize) { + self.skeletonSize = skeletonSize + super.init(frame: .zero) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updateTextWithLoading(_ text: String?) { + self.text = text + + if text != nil { + stopSkeletonAnimation() + } else { + startSkeletonAnimation() + } + } + + override func layoutSubviews() { + super.layoutSubviews() + + if text == nil { + updateSkeletonLayout() + } + } +} diff --git a/fearless/Common/View/SkeletonLoadableView.swift b/fearless/Common/View/SkeletonLoadableView.swift new file mode 100644 index 0000000000..8012c18672 --- /dev/null +++ b/fearless/Common/View/SkeletonLoadableView.swift @@ -0,0 +1,52 @@ +import UIKit +import SoraUI + +protocol SkeletonLoadableView: UIView { + var container: UIView { get } + var skeletonSize: CGSize { get set } + var skeletonView: SkrullableView? { get set } + + func startSkeletonAnimation() + func stopSkeletonAnimation() + func updateSkeletonLayout() +} + +extension SkeletonLoadableView { + func startSkeletonAnimation() { + guard skeletonView == nil else { + return + } + let skeletonView = Skrull( + size: skeletonSize, + decorations: [], + skeletons: [ + SingleSkeleton.createRow(position: CGPoint(x: 0, y: 0), size: skeletonSize) + ] + ) + .fillSkeletonStart(R.color.colorSkeletonStart()!) + .fillSkeletonEnd(color: R.color.colorSkeletonEnd()!) + .build() + + skeletonView.frame = CGRect(origin: CGPoint(x: -skeletonSize.width, y: skeletonSize.height / 2), size: skeletonSize) + skeletonView.autoresizingMask = [] + container.addSubview(skeletonView) + + self.skeletonView = skeletonView + + skeletonView.startSkrulling() + } + + func stopSkeletonAnimation() { + skeletonView?.stopSkrulling() + skeletonView?.removeFromSuperview() + skeletonView = nil + } + + func updateSkeletonLayout() { + guard let skeletonView = skeletonView else { + return + } + + skeletonView.frame = CGRect(origin: CGPoint(x: -skeletonSize.width / 2, y: skeletonSize.height / 2), size: skeletonSize) + } +} diff --git a/fearless/Common/View/TokenPairIconsView.swift b/fearless/Common/View/TokenPairIconsView.swift index 14846d18e3..e3ff3be76f 100644 --- a/fearless/Common/View/TokenPairIconsView.swift +++ b/fearless/Common/View/TokenPairIconsView.swift @@ -27,8 +27,8 @@ class TokenPairIconsView: UIView { func bind(viewModel: TokenPairsIconViewModel) { let iconSize = CGSize(width: frame.size.height / 1.5, height: frame.size.height / 1.5) - viewModel.firstTokenIconViewModel.loadImage(on: firstTokenIconView, targetSize: iconSize, animated: false) - viewModel.secondTokenIconViewModel.loadImage(on: secondTokenIconView, targetSize: iconSize, animated: false) + viewModel.firstTokenIconViewModel?.loadImage(on: firstTokenIconView, targetSize: iconSize, animated: false) + viewModel.secondTokenIconViewModel?.loadImage(on: secondTokenIconView, targetSize: iconSize, animated: false) } private func drawSubviews() { diff --git a/fearless/Common/ViewController/ModalPicker/ModalInfoFactory.swift b/fearless/Common/ViewController/ModalPicker/ModalInfoFactory.swift index 27c5184930..90fc8967dd 100644 --- a/fearless/Common/ViewController/ModalPicker/ModalInfoFactory.swift +++ b/fearless/Common/ViewController/ModalPicker/ModalInfoFactory.swift @@ -1,7 +1,6 @@ import UIKit import SoraFoundation import SoraUI -import CommonWallet import SSFModels struct ModalInfoFactory { @@ -105,82 +104,6 @@ struct ModalInfoFactory { return viewController } - static func createTransferExistentialState( - _ state: TransferExistentialState, - amountFormatter: LocalizableResource - ) -> UIViewController { - let viewController: ModalPickerViewController - = ModalPickerViewController(nib: R.nib.modalPickerViewController) - viewController.cellHeight = Self.rowHeight - viewController.headerHeight = Self.headerHeight - viewController.footerHeight = Self.footerHeight - viewController.allowsSelection = false - viewController.hasCloseItem = true - - viewController.localizedTitle = LocalizableResource { locale in - R.string.localizable.walletSendBalanceDetails(preferredLanguages: locale.rLanguages) - } - - viewController.cellNib = UINib(resource: R.nib.detailsDisplayTableViewCell) - viewController.modalPresentationStyle = .custom - - let viewModels = createTransferStateViewModels( - state, - amountFormatter: amountFormatter - ) - viewController.viewModels = viewModels - - let factory = ModalSheetPresentationFactory(configuration: ModalSheetPresentationConfiguration.fearless) - viewController.modalTransitioningFactory = factory - - let height = viewController.headerHeight + CGFloat(viewModels.count) * viewController.cellHeight + - viewController.footerHeight - viewController.preferredContentSize = CGSize(width: 0.0, height: height) - - viewController.localizationManager = LocalizationManager.shared - - return viewController - } - - private static func createTransferStateViewModels( - _ state: TransferExistentialState, - amountFormatter: LocalizableResource - ) -> [LocalizableResource] { - [ - LocalizableResource { locale in - let title = R.string.localizable - .walletSendAvailableBalance(preferredLanguages: locale.rLanguages) - let details = amountFormatter.value(for: locale).stringFromDecimal(state.availableAmount) ?? "" - - return TitleWithSubtitleViewModel(title: title, subtitle: details) - }, - - LocalizableResource { locale in - let title = R.string.localizable - .walletSendBalanceTotal(preferredLanguages: locale.rLanguages) - let details = amountFormatter.value(for: locale).stringFromDecimal(state.totalAmount) ?? "" - - return TitleWithSubtitleViewModel(title: title, subtitle: details) - }, - - LocalizableResource { locale in - let title = R.string.localizable - .walletSendBalanceTotalAfterTransfer(preferredLanguages: locale.rLanguages) - let details = amountFormatter.value(for: locale).stringFromDecimal(state.totalAfterTransfer) ?? "" - - return TitleWithSubtitleViewModel(title: title, subtitle: details) - }, - - LocalizableResource { locale in - let title = R.string.localizable - .walletSendBalanceMinimal(preferredLanguages: locale.rLanguages) - let details = amountFormatter.value(for: locale).stringFromDecimal(state.existentialDeposit) ?? "" - - return TitleWithSubtitleViewModel(title: title, subtitle: details) - } - ] - } - private static func createViewModelsForContext( _ balanceContext: BalanceContext, amountFormatter: LocalizableResource, diff --git a/fearless/Common/ViewController/ModalPicker/ModalPickerFactory.swift b/fearless/Common/ViewController/ModalPicker/ModalPickerFactory.swift index 038bf0f894..0557993371 100644 --- a/fearless/Common/ViewController/ModalPicker/ModalPickerFactory.swift +++ b/fearless/Common/ViewController/ModalPicker/ModalPickerFactory.swift @@ -3,6 +3,7 @@ import SoraUI import SoraFoundation import IrohaCrypto import SSFUtils +import SSFModels enum AccountHeaderType { case title(_ title: LocalizableResource) diff --git a/fearless/Common/ViewModel/AccessoryViewModel.swift b/fearless/Common/ViewModel/AccessoryViewModel.swift new file mode 100644 index 0000000000..7ccde0ed9d --- /dev/null +++ b/fearless/Common/ViewModel/AccessoryViewModel.swift @@ -0,0 +1,32 @@ +import Foundation +import UIKit + +public protocol AccessoryViewModelProtocol { + var title: String { get } + var icon: UIImage? { get } + var action: String { get } + var numberOfLines: Int { get } + var shouldAllowAction: Bool { get } +} + +public struct AccessoryViewModel: AccessoryViewModelProtocol { + public var title: String + public var icon: UIImage? + public var action: String + public var numberOfLines: Int + public var shouldAllowAction: Bool + + public init( + title: String, + action: String, + icon: UIImage? = nil, + numberOfLines: Int = 1, + shouldAllowAction: Bool = true + ) { + self.title = title + self.icon = icon + self.action = action + self.numberOfLines = numberOfLines + self.shouldAllowAction = shouldAllowAction + } +} diff --git a/fearless/Common/ViewModel/ExtrinsicConfirmViewModel.swift b/fearless/Common/ViewModel/ExtrinsicConfirmViewModel.swift index 7877cddc6b..a1f4d0304b 100644 --- a/fearless/Common/ViewModel/ExtrinsicConfirmViewModel.swift +++ b/fearless/Common/ViewModel/ExtrinsicConfirmViewModel.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet +import UIKit struct ExtrinisicConfirmViewModel: AccessoryViewModelProtocol { let title: String diff --git a/fearless/Common/ViewModel/FeeViewModel.swift b/fearless/Common/ViewModel/FeeViewModel.swift new file mode 100644 index 0000000000..d34292f509 --- /dev/null +++ b/fearless/Common/ViewModel/FeeViewModel.swift @@ -0,0 +1,22 @@ +import Foundation + +public protocol FeeViewModelProtocol { + var title: String { get } + var details: String { get } + var isLoading: Bool { get } + var allowsEditing: Bool { get } +} + +public struct FeeViewModel: FeeViewModelProtocol { + public let title: String + public let details: String + public let isLoading: Bool + public let allowsEditing: Bool + + public init(title: String, details: String, isLoading: Bool, allowsEditing: Bool) { + self.title = title + self.details = details + self.isLoading = isLoading + self.allowsEditing = allowsEditing + } +} diff --git a/fearless/Common/ViewModel/TokenPairsIconViewModel.swift b/fearless/Common/ViewModel/TokenPairsIconViewModel.swift index dead1b2406..f1e6aa7fe1 100644 --- a/fearless/Common/ViewModel/TokenPairsIconViewModel.swift +++ b/fearless/Common/ViewModel/TokenPairsIconViewModel.swift @@ -1,6 +1,6 @@ import Foundation struct TokenPairsIconViewModel { - let firstTokenIconViewModel: RemoteImageViewModel - let secondTokenIconViewModel: RemoteImageViewModel + let firstTokenIconViewModel: RemoteImageViewModel? + let secondTokenIconViewModel: RemoteImageViewModel? } diff --git a/fearless/Common/ViewModel/WalletImageViewModelProtocol.swift b/fearless/Common/ViewModel/WalletImageViewModelProtocol.swift new file mode 100644 index 0000000000..093f86b04b --- /dev/null +++ b/fearless/Common/ViewModel/WalletImageViewModelProtocol.swift @@ -0,0 +1,7 @@ +import UIKit + +public protocol WalletImageViewModelProtocol: AnyObject { + var image: UIImage? { get } + func loadImage(with completionBlock: @escaping (UIImage?, Error?) -> Void) + func cancel() +} diff --git a/fearless/CoreLayer/CoreComponents/QR/QRParser.swift b/fearless/CoreLayer/CoreComponents/QR/QRParser.swift index 9b0439dbc6..536a0fbfa1 100644 --- a/fearless/CoreLayer/CoreComponents/QR/QRParser.swift +++ b/fearless/CoreLayer/CoreComponents/QR/QRParser.swift @@ -1,13 +1,13 @@ import Foundation -import SSFUtils +import SSFQRService protocol QRParser { func extractAddress(from code: String) throws -> String } final class SubstrateQRParser: QRParser { - private let prefix: String = SubstrateQR.prefix - private let separator: String = SubstrateQR.fieldsSeparator + private let prefix: String = SubstrateQRConstants.prefix + private let separator: String = SubstrateQRConstants.fieldsSeparator func extractAddress(from code: String) throws -> String { let fields = code diff --git a/fearless/CoreLayer/CoreComponents/Storage/RequestFactory/AsyncStorageRequestFactoryDefault.swift b/fearless/CoreLayer/CoreComponents/Storage/RequestFactory/AsyncStorageRequestFactoryDefault.swift index bea5455f7b..ababa561c4 100644 --- a/fearless/CoreLayer/CoreComponents/Storage/RequestFactory/AsyncStorageRequestFactoryDefault.swift +++ b/fearless/CoreLayer/CoreComponents/Storage/RequestFactory/AsyncStorageRequestFactoryDefault.swift @@ -23,7 +23,7 @@ final class AsyncStorageRequestDefault: AsyncStorageRequestFactory { storageKeyFactory: storageKeyFactory, keyParams: keyParams ) - let keys = try keysWorker.performEncoding() + let keys = try await keysWorker.performEncoding() let queryItems: [StorageResponse] = try await queryItems( engine: engine, @@ -105,7 +105,7 @@ final class AsyncStorageRequestDefault: AsyncStorageRequestFactory { storageKeyFactory: storageKeyFactory, keyParams: keyParams ) - let keys = try keysWorker.performEncoding() + let keys = try await keysWorker.performEncoding() let queryItems: [StorageResponse] = try await queryItems( engine: engine, diff --git a/fearless/CoreLayer/CoreComponents/Storage/StorageFactoryWorkers/JSONRPCWorkerDefault.swift b/fearless/CoreLayer/CoreComponents/Storage/StorageFactoryWorkers/JSONRPCWorkerDefault.swift index 120c05952b..779950dd47 100644 --- a/fearless/CoreLayer/CoreComponents/Storage/StorageFactoryWorkers/JSONRPCWorkerDefault.swift +++ b/fearless/CoreLayer/CoreComponents/Storage/StorageFactoryWorkers/JSONRPCWorkerDefault.swift @@ -1,7 +1,7 @@ import Foundation import SSFUtils -class JSONRPCWorker { +class JSONRPCWorker { private let engine: JSONRPCEngine private let method: String private let parameters: P diff --git a/fearless/CoreLayer/CoreComponents/Storage/StorageFactoryWorkers/MapKeyEncodingWorker.swift b/fearless/CoreLayer/CoreComponents/Storage/StorageFactoryWorkers/MapKeyEncodingWorker.swift index 1dd91e53a8..e081ca256e 100644 --- a/fearless/CoreLayer/CoreComponents/Storage/StorageFactoryWorkers/MapKeyEncodingWorker.swift +++ b/fearless/CoreLayer/CoreComponents/Storage/StorageFactoryWorkers/MapKeyEncodingWorker.swift @@ -3,7 +3,7 @@ import SSFRuntimeCodingService import SSFModels import SSFUtils -final class MapKeyEncodingWorker { +actor MapKeyEncodingWorker { private let keyParams: [any Encodable] private let codingFactory: RuntimeCoderFactoryProtocol private let path: StorageCodingPath diff --git a/fearless/CoreLayer/CoreComponents/Storage/StorageFactoryWorkers/NMapKeyEncodingWorker.swift b/fearless/CoreLayer/CoreComponents/Storage/StorageFactoryWorkers/NMapKeyEncodingWorker.swift index 67c0c2209e..1603a1744b 100644 --- a/fearless/CoreLayer/CoreComponents/Storage/StorageFactoryWorkers/NMapKeyEncodingWorker.swift +++ b/fearless/CoreLayer/CoreComponents/Storage/StorageFactoryWorkers/NMapKeyEncodingWorker.swift @@ -3,7 +3,7 @@ import SSFRuntimeCodingService import SSFModels import SSFUtils -final class NMapKeyEncodingWorker { +actor NMapKeyEncodingWorker { private let keyParams: [[any NMapKeyParamProtocol]] private let codingFactory: RuntimeCoderFactoryProtocol private let path: StorageCodingPath diff --git a/fearless/CoreLayer/CoreComponents/Storage/StorageRequestPerformer.swift b/fearless/CoreLayer/CoreComponents/Storage/StorageRequestPerformer.swift index 44910e69cb..2591cf409c 100644 --- a/fearless/CoreLayer/CoreComponents/Storage/StorageRequestPerformer.swift +++ b/fearless/CoreLayer/CoreComponents/Storage/StorageRequestPerformer.swift @@ -28,7 +28,7 @@ protocol StorageRequestPerformer { ) async throws -> [K: T]? } -final class StorageRequestPerformerDefault: StorageRequestPerformer { +actor StorageRequestPerformerDefault: StorageRequestPerformer { private let runtimeService: RuntimeCodingServiceProtocol private let connection: JSONRPCEngine private lazy var storageRequestFactory: AsyncStorageRequestFactory = { diff --git a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/AlchemyHistoryOperationFactory.swift b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/AlchemyHistoryOperationFactory.swift index 07c35e75b1..7891f2458b 100644 --- a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/AlchemyHistoryOperationFactory.swift +++ b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/AlchemyHistoryOperationFactory.swift @@ -1,6 +1,6 @@ import Foundation import RobinHood -import CommonWallet + import IrohaCrypto import SSFUtils import SSFModels diff --git a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/ArrowsquidHistoryOperationFactory.swift b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/ArrowsquidHistoryOperationFactory.swift index b67f5212c8..a2a840dc75 100644 --- a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/ArrowsquidHistoryOperationFactory.swift +++ b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/ArrowsquidHistoryOperationFactory.swift @@ -1,6 +1,6 @@ import Foundation import RobinHood -import CommonWallet + import IrohaCrypto import SSFUtils import SSFModels diff --git a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/EtherscanHistoryOperationFactory.swift b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/EtherscanHistoryOperationFactory.swift index b968139ab3..529a6b914d 100644 --- a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/EtherscanHistoryOperationFactory.swift +++ b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/EtherscanHistoryOperationFactory.swift @@ -1,6 +1,6 @@ import Foundation import RobinHood -import CommonWallet + import IrohaCrypto import SSFUtils import SSFModels diff --git a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/GiantsquidHistoryOperationFactory.swift b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/GiantsquidHistoryOperationFactory.swift index cc4ebcbe76..c92fec2f2b 100644 --- a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/GiantsquidHistoryOperationFactory.swift +++ b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/GiantsquidHistoryOperationFactory.swift @@ -1,6 +1,6 @@ import Foundation import RobinHood -import CommonWallet + import IrohaCrypto import SSFUtils import SSFModels diff --git a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/HistoryOperationFactory.swift b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/HistoryOperationFactory.swift index 094b6d084a..b9f03cfc34 100644 --- a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/HistoryOperationFactory.swift +++ b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/HistoryOperationFactory.swift @@ -1,4 +1,4 @@ -import CommonWallet + import RobinHood import IrohaCrypto import SSFUtils diff --git a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/OklinkHistoryOperationFactory.swift b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/OklinkHistoryOperationFactory.swift index 0dc213b4bd..56a6240ee1 100644 --- a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/OklinkHistoryOperationFactory.swift +++ b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/OklinkHistoryOperationFactory.swift @@ -1,6 +1,6 @@ import Foundation import RobinHood -import CommonWallet + import IrohaCrypto import SSFUtils import SSFModels diff --git a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/ReefSubsquidHistoryOperationFactory.swift b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/ReefSubsquidHistoryOperationFactory.swift index 2c497c669b..84f724434c 100644 --- a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/ReefSubsquidHistoryOperationFactory.swift +++ b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/ReefSubsquidHistoryOperationFactory.swift @@ -1,6 +1,6 @@ import Foundation import RobinHood -import CommonWallet + import IrohaCrypto import SSFUtils import SSFModels diff --git a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/SoraSubsquidHistoryOperationFactory.swift b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/SoraSubsquidHistoryOperationFactory.swift index a107c02cc5..3c20c81c16 100644 --- a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/SoraSubsquidHistoryOperationFactory.swift +++ b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/SoraSubsquidHistoryOperationFactory.swift @@ -1,6 +1,6 @@ import Foundation import RobinHood -import CommonWallet + import IrohaCrypto import SSFUtils import SSFModels diff --git a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/SubqueryHistoryOperationFactory.swift b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/SubqueryHistoryOperationFactory.swift index 6bff1e8038..f21954ff50 100644 --- a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/SubqueryHistoryOperationFactory.swift +++ b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/SubqueryHistoryOperationFactory.swift @@ -1,6 +1,6 @@ import Foundation import RobinHood -import CommonWallet + import IrohaCrypto import SSFUtils import SSFModels diff --git a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/SubsquidHistoryOperationFactory.swift b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/SubsquidHistoryOperationFactory.swift index 3eb4c78d2f..399aa433d4 100644 --- a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/SubsquidHistoryOperationFactory.swift +++ b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/SubsquidHistoryOperationFactory.swift @@ -1,6 +1,6 @@ import Foundation import RobinHood -import CommonWallet + import IrohaCrypto import SSFUtils import SSFModels diff --git a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/ZetaHistoryOperationFactory.swift b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/ZetaHistoryOperationFactory.swift index b0e5c4b87b..8421f12f6d 100644 --- a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/ZetaHistoryOperationFactory.swift +++ b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/ZetaHistoryOperationFactory.swift @@ -1,6 +1,6 @@ import Foundation import RobinHood -import CommonWallet + import IrohaCrypto import SSFUtils import SSFModels diff --git a/fearless/CoreLayer/OperationFactory/NFT/BlockExplorerNFTOperationFactory.swift b/fearless/CoreLayer/OperationFactory/NFT/BlockExplorerNFTOperationFactory.swift index e832e1b2ed..252b01e34e 100644 --- a/fearless/CoreLayer/OperationFactory/NFT/BlockExplorerNFTOperationFactory.swift +++ b/fearless/CoreLayer/OperationFactory/NFT/BlockExplorerNFTOperationFactory.swift @@ -1,6 +1,6 @@ import Foundation import RobinHood -import CommonWallet + import IrohaCrypto import SSFUtils import SSFModels diff --git a/fearless/CoreLayer/OperationFactory/SoraHistoryOperationFactory.swift b/fearless/CoreLayer/OperationFactory/SoraHistoryOperationFactory.swift index 9c77ff8eef..ac9550362f 100644 --- a/fearless/CoreLayer/OperationFactory/SoraHistoryOperationFactory.swift +++ b/fearless/CoreLayer/OperationFactory/SoraHistoryOperationFactory.swift @@ -1,4 +1,4 @@ -import CommonWallet + import RobinHood import IrohaCrypto import SSFUtils diff --git a/fearless/CoreLayer/ProviderFactory/BaseDataProviderFactory.swift b/fearless/CoreLayer/ProviderFactory/BaseDataProviderFactory.swift index 048fe8143c..03bcc0f5aa 100644 --- a/fearless/CoreLayer/ProviderFactory/BaseDataProviderFactory.swift +++ b/fearless/CoreLayer/ProviderFactory/BaseDataProviderFactory.swift @@ -1,15 +1,9 @@ -import CommonWallet import RobinHood +import SSFSingleValueCache class BaseDataProviderFactory { - let cacheFacade: StorageFacadeProtocol - - init(cacheFacade: StorageFacadeProtocol) { - self.cacheFacade = cacheFacade - } - func createSingleValueCache() - -> CoreDataRepository { - cacheFacade.createRepository() + throws -> CoreDataRepository { + try SingleValueCacheRepositoryFactoryDefault().createSingleValueCacheRepository() } } diff --git a/fearless/CoreLayer/ProviderFactory/HistoryDataProviderFactory.swift b/fearless/CoreLayer/ProviderFactory/HistoryDataProviderFactory.swift index 37ee4cdb7e..c1d15b80eb 100644 --- a/fearless/CoreLayer/ProviderFactory/HistoryDataProviderFactory.swift +++ b/fearless/CoreLayer/ProviderFactory/HistoryDataProviderFactory.swift @@ -1,6 +1,6 @@ import Foundation import RobinHood -import CommonWallet + import SSFModels protocol HistoryDataProviderFactoryProtocol { @@ -9,7 +9,6 @@ protocol HistoryDataProviderFactoryProtocol { asset: AssetModel, chain: ChainModel, targetIdentifier: String, - using _: DispatchQueue, filters: [WalletTransactionHistoryFilter] ) throws -> SingleValueProvider @@ -20,13 +19,10 @@ class HistoryDataProviderFactory: BaseDataProviderFactory, HistoryDataProviderFa let executionQueue = OperationQueue() let operationFactory: HistoryOperationFactoryProtocol - init( - cacheFacade: StorageFacadeProtocol, - operationFactory: HistoryOperationFactoryProtocol - ) { + init(operationFactory: HistoryOperationFactoryProtocol) { self.operationFactory = operationFactory - super.init(cacheFacade: cacheFacade) + super.init() } enum Constants { @@ -38,7 +34,6 @@ class HistoryDataProviderFactory: BaseDataProviderFactory, HistoryDataProviderFa asset: AssetModel, chain: ChainModel, targetIdentifier: String, - using _: DispatchQueue, filters: [WalletTransactionHistoryFilter] ) throws -> SingleValueProvider { @@ -57,7 +52,7 @@ class HistoryDataProviderFactory: BaseDataProviderFactory, HistoryDataProviderFa return operation } - let cache = createSingleValueCache() + let cache = try createSingleValueCache() let updateTrigger = DataProviderEventTrigger.onAddObserver diff --git a/fearless/Modules/About/AboutProtocols.swift b/fearless/Modules/About/AboutProtocols.swift index c2dcbdad2b..69ad84664e 100644 --- a/fearless/Modules/About/AboutProtocols.swift +++ b/fearless/Modules/About/AboutProtocols.swift @@ -1,4 +1,4 @@ -import CommonWallet +import Foundation protocol AboutViewProtocol: ControllerBackedProtocol { func didReceive(state: AboutViewState) diff --git a/fearless/Modules/About/AboutViewController.swift b/fearless/Modules/About/AboutViewController.swift index 8c6ddfc303..30c44318e6 100644 --- a/fearless/Modules/About/AboutViewController.swift +++ b/fearless/Modules/About/AboutViewController.swift @@ -1,6 +1,5 @@ import UIKit import SoraUI -import CommonWallet final class AboutViewController: UIViewController, AdaptiveDesignable, ViewHolder { typealias RootViewType = AboutViewLayout diff --git a/fearless/Modules/AccountCreate/AccountCreatePresenter.swift b/fearless/Modules/AccountCreate/AccountCreatePresenter.swift index 1d8ef26606..7436a70a3d 100644 --- a/fearless/Modules/AccountCreate/AccountCreatePresenter.swift +++ b/fearless/Modules/AccountCreate/AccountCreatePresenter.swift @@ -2,6 +2,7 @@ import UIKit import IrohaCrypto import SoraFoundation import SSFUtils +import SSFModels final class AccountCreatePresenter { static let maxEthereumDerivationPathLength: Int = 15 @@ -121,7 +122,7 @@ final class AccountCreatePresenter { locale: locale ) } else { - switch cryptoType.utilsType { + switch cryptoType { case .sr25519: _ = wireframe.present( error: AccountCreationError.invalidDerivationHardSoftPassword, diff --git a/fearless/Modules/AccountCreate/AccountCreateProtocols.swift b/fearless/Modules/AccountCreate/AccountCreateProtocols.swift index bdd6353ebf..e812453bfb 100644 --- a/fearless/Modules/AccountCreate/AccountCreateProtocols.swift +++ b/fearless/Modules/AccountCreate/AccountCreateProtocols.swift @@ -1,5 +1,6 @@ import IrohaCrypto import SoraFoundation +import SSFModels protocol AccountCreateViewProtocol: ControllerBackedProtocol { func set(mnemonic: [String]) diff --git a/fearless/Modules/AccountCreate/AccountCreateWireframe.swift b/fearless/Modules/AccountCreate/AccountCreateWireframe.swift index 434750ca13..14dd717466 100644 --- a/fearless/Modules/AccountCreate/AccountCreateWireframe.swift +++ b/fearless/Modules/AccountCreate/AccountCreateWireframe.swift @@ -1,5 +1,6 @@ import Foundation import IrohaCrypto +import SSFModels final class AccountCreateWireframe: AccountCreateWireframeProtocol { func confirm( diff --git a/fearless/Modules/AccountCreate/Model/AccountCreateChainType.swift b/fearless/Modules/AccountCreate/Model/AccountCreateChainType.swift index ae2c0a208a..8336692a40 100644 --- a/fearless/Modules/AccountCreate/Model/AccountCreateChainType.swift +++ b/fearless/Modules/AccountCreate/Model/AccountCreateChainType.swift @@ -1,4 +1,6 @@ import Rswift +import SSFModels + enum AccountCreateChainType { case substrate case ethereum diff --git a/fearless/Modules/AccountCreate/Model/AccountCreationRequest.swift b/fearless/Modules/AccountCreate/Model/AccountCreationRequest.swift index 596b8ef40a..6eb6976e04 100644 --- a/fearless/Modules/AccountCreate/Model/AccountCreationRequest.swift +++ b/fearless/Modules/AccountCreate/Model/AccountCreationRequest.swift @@ -1,5 +1,6 @@ import Foundation import IrohaCrypto +import SSFModels struct MetaAccountCreationRequest { let username: String diff --git a/fearless/Modules/AccountCreate/Model/MetaAccountCreationMetadata.swift b/fearless/Modules/AccountCreate/Model/MetaAccountCreationMetadata.swift index e2152ffe72..64bca26dfb 100644 --- a/fearless/Modules/AccountCreate/Model/MetaAccountCreationMetadata.swift +++ b/fearless/Modules/AccountCreate/Model/MetaAccountCreationMetadata.swift @@ -1,4 +1,5 @@ import Foundation +import SSFModels struct MetaAccountCreationMetadata { let mnemonic: [String] diff --git a/fearless/Modules/AccountImport/AccountImportProtocols.swift b/fearless/Modules/AccountImport/AccountImportProtocols.swift index 1de08a9cdd..826470f3db 100644 --- a/fearless/Modules/AccountImport/AccountImportProtocols.swift +++ b/fearless/Modules/AccountImport/AccountImportProtocols.swift @@ -1,5 +1,6 @@ import IrohaCrypto import SoraFoundation +import SSFModels protocol AccountImportViewProtocol: ControllerBackedProtocol { func show(chainType: AccountCreateChainType) diff --git a/fearless/Modules/AccountImport/AccountImportWireframe.swift b/fearless/Modules/AccountImport/AccountImportWireframe.swift index e8eda06c79..8f5ba4391d 100644 --- a/fearless/Modules/AccountImport/AccountImportWireframe.swift +++ b/fearless/Modules/AccountImport/AccountImportWireframe.swift @@ -1,5 +1,6 @@ import Foundation import IrohaCrypto +import SSFModels final class AccountImportWireframe: AccountImportWireframeProtocol { lazy var rootAnimator: RootControllerAnimationCoordinatorProtocol = RootControllerAnimationCoordinator() diff --git a/fearless/Modules/AccountImport/BaseAccountImportInteractor.swift b/fearless/Modules/AccountImport/BaseAccountImportInteractor.swift index cc88ef68aa..70dcce9f0b 100644 --- a/fearless/Modules/AccountImport/BaseAccountImportInteractor.swift +++ b/fearless/Modules/AccountImport/BaseAccountImportInteractor.swift @@ -3,6 +3,7 @@ import IrohaCrypto import SSFUtils import RobinHood import SoraKeystore +import SSFModels class BaseAccountImportInteractor { weak var presenter: AccountImportInteractorOutputProtocol! diff --git a/fearless/Modules/AccountImport/Model/AccountImportJsonFactory.swift b/fearless/Modules/AccountImport/Model/AccountImportJsonFactory.swift index 54f8f943ae..4e210078d3 100644 --- a/fearless/Modules/AccountImport/Model/AccountImportJsonFactory.swift +++ b/fearless/Modules/AccountImport/Model/AccountImportJsonFactory.swift @@ -1,5 +1,6 @@ import Foundation import SSFUtils +import SSFModels protocol AccountImportJsonFactoryProtocol { func createInfo(from definition: KeystoreDefinition) throws -> MetaAccountImportPreferredInfo @@ -11,7 +12,7 @@ final class AccountImportJsonFactory { return MetaAccountImportPreferredInfo( username: info.meta?.name, - cryptoType: CryptoType(info.cryptoType), + cryptoType: info.cryptoType, isEthereum: info.isEthereum ) } diff --git a/fearless/Modules/AccountImport/Model/MetaAccountImportMetadata.swift b/fearless/Modules/AccountImport/Model/MetaAccountImportMetadata.swift index 455f9837c7..d7e501d2cf 100644 --- a/fearless/Modules/AccountImport/Model/MetaAccountImportMetadata.swift +++ b/fearless/Modules/AccountImport/Model/MetaAccountImportMetadata.swift @@ -1,4 +1,5 @@ import Foundation +import SSFModels struct MetaAccountImportMetadata { let availableSources: [AccountImportSource] diff --git a/fearless/Modules/AccountImport/Model/MetaAccountImportPreferredInfo.swift b/fearless/Modules/AccountImport/Model/MetaAccountImportPreferredInfo.swift index 1ca7376ce8..47c7f0ec25 100644 --- a/fearless/Modules/AccountImport/Model/MetaAccountImportPreferredInfo.swift +++ b/fearless/Modules/AccountImport/Model/MetaAccountImportPreferredInfo.swift @@ -1,4 +1,5 @@ import Foundation +import SSFModels struct MetaAccountImportPreferredInfo { let username: String? diff --git a/fearless/Modules/AccountManagement/AccountManagementViewFactory.swift b/fearless/Modules/AccountManagement/AccountManagementViewFactory.swift index 94c850829a..dd338cafd8 100644 --- a/fearless/Modules/AccountManagement/AccountManagementViewFactory.swift +++ b/fearless/Modules/AccountManagement/AccountManagementViewFactory.swift @@ -4,6 +4,7 @@ import RobinHood import SSFUtils import IrohaCrypto import SoraKeystore +import SSFAccountManagmentStorage final class AccountManagementViewFactory: AccountManagementViewFactoryProtocol { static func createViewForSettings() -> AccountManagementViewProtocol? { diff --git a/fearless/Modules/AddAccount/Wireframes/AddAccount+AccountCreateWireframe.swift b/fearless/Modules/AddAccount/Wireframes/AddAccount+AccountCreateWireframe.swift index b7837b4940..162854ab48 100644 --- a/fearless/Modules/AddAccount/Wireframes/AddAccount+AccountCreateWireframe.swift +++ b/fearless/Modules/AddAccount/Wireframes/AddAccount+AccountCreateWireframe.swift @@ -1,5 +1,6 @@ import Foundation import IrohaCrypto +import SSFModels extension AddAccount { final class AccountCreateWireframe: AccountCreateWireframeProtocol { diff --git a/fearless/Modules/AddAccount/Wireframes/AddAccount+AccountImportWireframe.swift b/fearless/Modules/AddAccount/Wireframes/AddAccount+AccountImportWireframe.swift index f30d61818a..9a26720f4b 100644 --- a/fearless/Modules/AddAccount/Wireframes/AddAccount+AccountImportWireframe.swift +++ b/fearless/Modules/AddAccount/Wireframes/AddAccount+AccountImportWireframe.swift @@ -1,5 +1,6 @@ import Foundation import IrohaCrypto +import SSFModels extension AddAccount { final class AccountImportWireframe: AccountImportWireframeProtocol { diff --git a/fearless/Modules/BackupCreatePassword/BackupCreatePasswordInteractor.swift b/fearless/Modules/BackupCreatePassword/BackupCreatePasswordInteractor.swift index 13cc071748..62b334f65a 100644 --- a/fearless/Modules/BackupCreatePassword/BackupCreatePasswordInteractor.swift +++ b/fearless/Modules/BackupCreatePassword/BackupCreatePasswordInteractor.swift @@ -3,6 +3,7 @@ import SSFCloudStorage import RobinHood import SoraKeystore import IrohaCrypto +import SSFModels protocol BackupCreatePasswordInteractorOutput: AnyObject { func didReceive(error: Error) @@ -10,7 +11,7 @@ protocol BackupCreatePasswordInteractorOutput: AnyObject { } final class BackupCreatePasswordInteractor: BaseAccountConfirmInteractor { - var cloudStorage: FearlessCompatibilityProtocol? + var cloudStorage: CloudStorageServiceProtocol? // MARK: - Private properties @@ -199,18 +200,15 @@ final class BackupCreatePasswordInteractor: BaseAccountConfirmInteractor { password: String, wallet: MetaAccountModel ) { - cloudStorage?.saveBackupAccount( - account: account, - password: password - ) { [weak self] result in - switch result { - case .success: - self?.didBackuped(wallet: wallet) - DispatchQueue.main.async { - self?.output?.didComplete() + Task { + do { + try await cloudStorage?.saveBackup(account: account, password: password) + didBackuped(wallet: wallet) + await MainActor.run { + self.output?.didComplete() } - case let .failure(failure): - self?.output?.didReceive(error: failure) + } catch { + self.output?.didReceive(error: error) } } } diff --git a/fearless/Modules/BackupPassword/BackupPasswordInteractor.swift b/fearless/Modules/BackupPassword/BackupPasswordInteractor.swift index 76c292a0b1..6494f9ef12 100644 --- a/fearless/Modules/BackupPassword/BackupPasswordInteractor.swift +++ b/fearless/Modules/BackupPassword/BackupPasswordInteractor.swift @@ -10,7 +10,7 @@ protocol BackupPasswordInteractorOutput: AnyObject { } final class BackupPasswordInteractor: BaseAccountImportInteractor { - var cloudStorage: FearlessCompatibilityProtocol? + var cloudStorage: CloudStorageServiceProtocol? // MARK: - Private properties @@ -126,8 +126,8 @@ extension BackupPasswordInteractor: BackupPasswordInteractorInput { } func signInIfNeeded() { - DispatchQueue.main.async { [weak self] in - self?.cloudStorage?.signInIfNeeded(completion: nil) + Task { + try await cloudStorage?.signInIfNeeded() } } } diff --git a/fearless/Modules/BackupPassword/BackupPasswordPresenter.swift b/fearless/Modules/BackupPassword/BackupPasswordPresenter.swift index 1caf901c0d..64435c56d0 100644 --- a/fearless/Modules/BackupPassword/BackupPasswordPresenter.swift +++ b/fearless/Modules/BackupPassword/BackupPasswordPresenter.swift @@ -1,6 +1,7 @@ import Foundation import SoraFoundation import SSFCloudStorage +import SSFModels protocol BackupPasswordViewInput: ControllerBackedProtocol, HiddableBarWhenPushed, LoadableViewProtocol { func didReceive(walletName: String) diff --git a/fearless/Modules/BackupSelectWallet/BackupSelectWalletInteractor.swift b/fearless/Modules/BackupSelectWallet/BackupSelectWalletInteractor.swift index 8532b3c644..eca66219df 100644 --- a/fearless/Modules/BackupSelectWallet/BackupSelectWalletInteractor.swift +++ b/fearless/Modules/BackupSelectWallet/BackupSelectWalletInteractor.swift @@ -7,7 +7,7 @@ protocol BackupSelectWalletInteractorOutput: AnyObject { } final class BackupSelectWalletInteractor { - var cloudStorageService: FearlessCompatibilityProtocol? + var cloudStorageService: CloudStorageServiceProtocol? // MARK: - Private properties @@ -24,7 +24,7 @@ extension BackupSelectWalletInteractor: BackupSelectWalletInteractorInput { do { if let cloudStorageService = cloudStorageService { cloudStorageService.disconnect() - let accounts = try await cloudStorageService.getFearlessBackupAccounts() + let accounts = try await cloudStorageService.getBackupAccounts() await MainActor.run { output?.didReceiveBackupAccounts(result: .success(accounts)) } diff --git a/fearless/Modules/BackupWallet/BackupWalletInteractor.swift b/fearless/Modules/BackupWallet/BackupWalletInteractor.swift index 079ebbc5b2..09cd24344d 100644 --- a/fearless/Modules/BackupWallet/BackupWalletInteractor.swift +++ b/fearless/Modules/BackupWallet/BackupWalletInteractor.swift @@ -15,7 +15,7 @@ protocol BackupWalletInteractorOutput: AnyObject { } final class BackupWalletInteractor { - var cloudStorage: FearlessCompatibilityProtocol? + var cloudStorage: CloudStorageServiceProtocol? // MARK: - Private properties @@ -85,7 +85,7 @@ final class BackupWalletInteractor { Task { do { if let cloudStorage = cloudStorage { - let accounts = try await cloudStorage.getFearlessBackupAccounts() + let accounts = try await cloudStorage.getBackupAccounts() await MainActor.run { output?.didReceiveBackupAccounts(result: .success(accounts)) } diff --git a/fearless/Modules/BackupWallet/BackupWalletPresenter.swift b/fearless/Modules/BackupWallet/BackupWalletPresenter.swift index 7ca461d990..f9d90dd190 100644 --- a/fearless/Modules/BackupWallet/BackupWalletPresenter.swift +++ b/fearless/Modules/BackupWallet/BackupWalletPresenter.swift @@ -164,7 +164,7 @@ final class BackupWalletPresenter { private func showDelete(error: Error) { var presentingError: ConvenienceError? - if let error = error as? FearlessCompatibilityError { + if let error = error as? FearlessExtensionError { switch error { case .cantRemoveExtensionBackup: let title = R.string.localizable.commonWarning(preferredLanguages: selectedLocale.rLanguages) diff --git a/fearless/Modules/BackupWalletImported/BackupWalletImportedInteractor.swift b/fearless/Modules/BackupWalletImported/BackupWalletImportedInteractor.swift index c1e5039d45..2bb09d9e76 100644 --- a/fearless/Modules/BackupWalletImported/BackupWalletImportedInteractor.swift +++ b/fearless/Modules/BackupWalletImported/BackupWalletImportedInteractor.swift @@ -5,7 +5,7 @@ import SSFCloudStorage protocol BackupWalletImportedInteractorOutput: AnyObject {} final class BackupWalletImportedInteractor { - var cloudStorageService: FearlessCompatibilityProtocol? + var cloudStorageService: CloudStorageServiceProtocol? // MARK: - Private properties diff --git a/fearless/Modules/Contacts/ContactsInteractor.swift b/fearless/Modules/Contacts/ContactsInteractor.swift index f3861a4b29..76bb45e63d 100644 --- a/fearless/Modules/Contacts/ContactsInteractor.swift +++ b/fearless/Modules/Contacts/ContactsInteractor.swift @@ -1,6 +1,6 @@ import UIKit import RobinHood -import CommonWallet + import SSFModels final class ContactsInteractor { diff --git a/fearless/Modules/Contacts/ContactsPresenter.swift b/fearless/Modules/Contacts/ContactsPresenter.swift index 52f98023f0..48589c08ac 100644 --- a/fearless/Modules/Contacts/ContactsPresenter.swift +++ b/fearless/Modules/Contacts/ContactsPresenter.swift @@ -1,6 +1,6 @@ import Foundation import SoraFoundation -import CommonWallet + import SSFModels final class ContactsPresenter { diff --git a/fearless/Modules/CrossChainConfirmation/CrossChainDepsContainer.swift b/fearless/Modules/CrossChainConfirmation/CrossChainDepsContainer.swift index bf46448239..37dfa28fac 100644 --- a/fearless/Modules/CrossChainConfirmation/CrossChainDepsContainer.swift +++ b/fearless/Modules/CrossChainConfirmation/CrossChainDepsContainer.swift @@ -98,10 +98,11 @@ final class CrossChainDepsContainer { let fromChainData = XcmAssembly.FromChainData( chainId: originalChainAsset.chain.chainId, - cryptoType: SFCryptoType(utilsType: cryptoType.utilsType, isEthereum: response.isEthereumBased), + cryptoType: cryptoType, chainMetadata: originalRuntimeMetadataItem, accountId: accountId, - signingWrapperData: signingWrapperData + signingWrapperData: signingWrapperData, + chainType: originalChainAsset.chain.chainBaseType ) let sourceConfig = ApplicationConfig.shared diff --git a/fearless/Modules/Crowdloan/CrowdloanContribution/CrowdloanContributionInteractor.swift b/fearless/Modules/Crowdloan/CrowdloanContribution/CrowdloanContributionInteractor.swift index c14859eeb0..7f81920eb2 100644 --- a/fearless/Modules/Crowdloan/CrowdloanContribution/CrowdloanContributionInteractor.swift +++ b/fearless/Modules/Crowdloan/CrowdloanContribution/CrowdloanContributionInteractor.swift @@ -101,7 +101,7 @@ class CrowdloanContributionInteractor: CrowdloanContributionInteractorInputProto private func subscribeToDisplayInfo() { if let displayInfoUrl = chainAsset.chain.externalApi?.crowdloans?.url { - displayInfoProvider = subscribeToCrowdloanDisplayInfo( + displayInfoProvider = try? subscribeToCrowdloanDisplayInfo( for: displayInfoUrl, chainId: chainAsset.chain.chainId ) diff --git a/fearless/Modules/Crowdloan/CrowdloanContribution/CrowdloanContributionViewModelFactory.swift b/fearless/Modules/Crowdloan/CrowdloanContribution/CrowdloanContributionViewModelFactory.swift index e32d0acfa3..78fe7a8760 100644 --- a/fearless/Modules/Crowdloan/CrowdloanContribution/CrowdloanContributionViewModelFactory.swift +++ b/fearless/Modules/Crowdloan/CrowdloanContribution/CrowdloanContributionViewModelFactory.swift @@ -1,6 +1,6 @@ import Foundation import SoraFoundation -import CommonWallet + import SSFUtils import SSFModels diff --git a/fearless/Modules/Crowdloan/CrowdloanContributionSetup/CrowdloanContributionSetupProtocols.swift b/fearless/Modules/Crowdloan/CrowdloanContributionSetup/CrowdloanContributionSetupProtocols.swift index a78a1fd908..f20d65db2d 100644 --- a/fearless/Modules/Crowdloan/CrowdloanContributionSetup/CrowdloanContributionSetupProtocols.swift +++ b/fearless/Modules/Crowdloan/CrowdloanContributionSetup/CrowdloanContributionSetupProtocols.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import BigInt import SoraFoundation diff --git a/fearless/Modules/Crowdloan/CrowdloanContributionSetup/CrowdloanContributionSetupViewController.swift b/fearless/Modules/Crowdloan/CrowdloanContributionSetup/CrowdloanContributionSetupViewController.swift index a48eae11b6..74f4f61c0d 100644 --- a/fearless/Modules/Crowdloan/CrowdloanContributionSetup/CrowdloanContributionSetupViewController.swift +++ b/fearless/Modules/Crowdloan/CrowdloanContributionSetup/CrowdloanContributionSetupViewController.swift @@ -1,5 +1,5 @@ import UIKit -import CommonWallet + import SoraFoundation final class CrowdloanContributionSetupViewController: UIViewController, ViewHolder { diff --git a/fearless/Modules/Crowdloan/CrowdloanList/CrowdloanListInteractor.swift b/fearless/Modules/Crowdloan/CrowdloanList/CrowdloanListInteractor.swift index 64a7f6e12c..e9a5ebb28e 100644 --- a/fearless/Modules/Crowdloan/CrowdloanList/CrowdloanListInteractor.swift +++ b/fearless/Modules/Crowdloan/CrowdloanList/CrowdloanListInteractor.swift @@ -161,7 +161,7 @@ final class CrowdloanListInteractor: RuntimeConstantFetching { return } - displayInfoProvider = jsonDataProviderFactory.getJson(for: crowdloanUrl) + displayInfoProvider = try? jsonDataProviderFactory.getJson(for: crowdloanUrl) let updateClosure: ([DataProviderChange]) -> Void = { [weak self] changes in if let result = changes.reduceToLastChange() { diff --git a/fearless/Modules/Crowdloan/CrowdloanList/ViewModel/CrowdloanViewModel.swift b/fearless/Modules/Crowdloan/CrowdloanList/ViewModel/CrowdloanViewModel.swift index 007f2731b6..5da2ff7c9b 100644 --- a/fearless/Modules/Crowdloan/CrowdloanList/ViewModel/CrowdloanViewModel.swift +++ b/fearless/Modules/Crowdloan/CrowdloanList/ViewModel/CrowdloanViewModel.swift @@ -1,6 +1,5 @@ import Foundation import SoraFoundation -import CommonWallet enum CrowdloanListState { case loading diff --git a/fearless/Modules/Crowdloan/CrowdloanList/ViewModel/CrowdloansViewModelFactory.swift b/fearless/Modules/Crowdloan/CrowdloanList/ViewModel/CrowdloansViewModelFactory.swift index a75f2cfa7b..b88cf5594d 100644 --- a/fearless/Modules/Crowdloan/CrowdloanList/ViewModel/CrowdloansViewModelFactory.swift +++ b/fearless/Modules/Crowdloan/CrowdloanList/ViewModel/CrowdloansViewModelFactory.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SoraFoundation import SSFUtils import BigInt diff --git a/fearless/Modules/Crowdloan/CustomCrowdloan/Astar/AstarBonusService.swift b/fearless/Modules/Crowdloan/CustomCrowdloan/Astar/AstarBonusService.swift index 07441a0907..75941f9630 100644 --- a/fearless/Modules/Crowdloan/CustomCrowdloan/Astar/AstarBonusService.swift +++ b/fearless/Modules/Crowdloan/CustomCrowdloan/Astar/AstarBonusService.swift @@ -2,7 +2,7 @@ import Foundation import RobinHood import SSFUtils import BigInt -import CommonWallet + import IrohaCrypto final class AstarBonusService { diff --git a/fearless/Modules/Crowdloan/Validation/CrowdloanDataValidatorFactory.swift b/fearless/Modules/Crowdloan/Validation/CrowdloanDataValidatorFactory.swift index 41b5da5ccb..645c6b08ea 100644 --- a/fearless/Modules/Crowdloan/Validation/CrowdloanDataValidatorFactory.swift +++ b/fearless/Modules/Crowdloan/Validation/CrowdloanDataValidatorFactory.swift @@ -1,7 +1,7 @@ import Foundation import SoraFoundation import BigInt -import CommonWallet + import SSFModels protocol CrowdloanDataValidatorFactoryProtocol: BaseDataValidatingFactoryProtocol { diff --git a/fearless/Modules/GetPreinstalledWallet/GetPreinstalledWalletPresenter.swift b/fearless/Modules/GetPreinstalledWallet/GetPreinstalledWalletPresenter.swift index 547ba88302..da2043db21 100644 --- a/fearless/Modules/GetPreinstalledWallet/GetPreinstalledWalletPresenter.swift +++ b/fearless/Modules/GetPreinstalledWallet/GetPreinstalledWalletPresenter.swift @@ -1,7 +1,7 @@ import Foundation import SoraFoundation import AVFoundation -import CommonWallet + import SSFUtils final class GetPreinstalledWalletPresenter: NSObject { diff --git a/fearless/Modules/LiquidityPools/Common/LiquidityPools+ViewModel.swift b/fearless/Modules/LiquidityPools/Common/LiquidityPools+ViewModel.swift new file mode 100644 index 0000000000..9218c85f82 --- /dev/null +++ b/fearless/Modules/LiquidityPools/Common/LiquidityPools+ViewModel.swift @@ -0,0 +1,35 @@ +import Foundation +import SSFPolkaswap +import SSFModels +import SSFPools + +protocol LiquidityPoolsModelFactory { + func buildReserves(pool: LiquidityPair, chain: ChainModel, reserves: PolkaswapPoolReservesInfo?, baseAssetPrice: PriceData?, targetAssetPrice: PriceData?) -> Decimal? +} + +final class LiquidityPoolsModelFactoryDefault: LiquidityPoolsModelFactory { + func buildReserves(pool: LiquidityPair, chain: ChainModel, reserves: PolkaswapPoolReservesInfo?, baseAssetPrice: PriceData?, targetAssetPrice: PriceData?) -> Decimal? { + let baseAsset = chain.assets.first(where: { $0.currencyId == pool.baseAssetId }) + let targetAsset = chain.assets.first(where: { $0.currencyId == pool.targetAssetId }) + + guard let baseAsset, let targetAsset else { + return nil + } + + let poolReservesValue = (reserves?.reserves.reserves).flatMap { Decimal.fromSubstrateAmount($0, precision: Int16(baseAsset.precision)) } + let baseAssetPriceValue = (baseAssetPrice?.price).flatMap { Decimal(string: $0) } + + let poolFeeValue = (reserves?.reserves.fee).flatMap { Decimal.fromSubstrateAmount($0, precision: Int16(targetAsset.precision)) } + let targetAssetPriceValue = (targetAssetPrice?.price).flatMap { Decimal(string: $0) } + + let poolReservesFiatValue: Decimal? = poolReservesValue.flatMap { poolReserves in + guard let baseAssetPriceValue, let poolFeeValue, let targetAssetPriceValue else { + return nil + } + + return (poolReserves * baseAssetPriceValue) + (poolFeeValue * targetAssetPriceValue) + } + + return poolReservesFiatValue + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsAssembly.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsAssembly.swift new file mode 100644 index 0000000000..63c54ce246 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsAssembly.swift @@ -0,0 +1,24 @@ +import UIKit +import SoraFoundation + +final class LiquidityPoolDetailsAssembly { + static func configureModule() -> LiquidityPoolDetailsModuleCreationResult? { + let localizationManager = LocalizationManager.shared + + let interactor = LiquidityPoolDetailsInteractor() + let router = LiquidityPoolDetailsRouter() + + let presenter = LiquidityPoolDetailsPresenter( + interactor: interactor, + router: router, + localizationManager: localizationManager + ) + + let view = LiquidityPoolDetailsViewController( + output: presenter, + localizationManager: localizationManager + ) + + return (view, presenter) + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsInteractor.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsInteractor.swift new file mode 100644 index 0000000000..c6df638530 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsInteractor.swift @@ -0,0 +1,13 @@ +import UIKit + +final class LiquidityPoolDetailsInteractor { + // MARK: - Private properties + private weak var output: LiquidityPoolDetailsInteractorOutput? +} + +// MARK: - LiquidityPoolDetailsInteractorInput +extension LiquidityPoolDetailsInteractor: LiquidityPoolDetailsInteractorInput { + func setup(with output: LiquidityPoolDetailsInteractorOutput) { + self.output = output + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift new file mode 100644 index 0000000000..33312ef17d --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift @@ -0,0 +1,40 @@ +import Foundation +import SoraFoundation + +final class LiquidityPoolDetailsPresenter { + // MARK: Private properties + private weak var view: LiquidityPoolDetailsViewInput? + private let router: LiquidityPoolDetailsRouterInput + private let interactor: LiquidityPoolDetailsInteractorInput + + // MARK: - Constructors + init( + interactor: LiquidityPoolDetailsInteractorInput, + router: LiquidityPoolDetailsRouterInput, + localizationManager: LocalizationManagerProtocol + ) { + self.interactor = interactor + self.router = router + self.localizationManager = localizationManager + } + + // MARK: - Private methods +} + +// MARK: - LiquidityPoolDetailsViewOutput +extension LiquidityPoolDetailsPresenter: LiquidityPoolDetailsViewOutput { + func didLoad(view: LiquidityPoolDetailsViewInput) { + self.view = view + interactor.setup(with: self) + } +} + +// MARK: - LiquidityPoolDetailsInteractorOutput +extension LiquidityPoolDetailsPresenter: LiquidityPoolDetailsInteractorOutput {} + +// MARK: - Localizable +extension LiquidityPoolDetailsPresenter: Localizable { + func applyLocalization() {} +} + +extension LiquidityPoolDetailsPresenter: LiquidityPoolDetailsModuleInput {} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsProtocols.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsProtocols.swift new file mode 100644 index 0000000000..9a5f941277 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsProtocols.swift @@ -0,0 +1,19 @@ +typealias LiquidityPoolDetailsModuleCreationResult = (view: LiquidityPoolDetailsViewInput, input: LiquidityPoolDetailsModuleInput) + +protocol LiquidityPoolDetailsViewInput: ControllerBackedProtocol {} + +protocol LiquidityPoolDetailsViewOutput: AnyObject { + func didLoad(view: LiquidityPoolDetailsViewInput) +} + +protocol LiquidityPoolDetailsInteractorInput: AnyObject { + func setup(with output: LiquidityPoolDetailsInteractorOutput) +} + +protocol LiquidityPoolDetailsInteractorOutput: AnyObject {} + +protocol LiquidityPoolDetailsRouterInput: AnyObject {} + +protocol LiquidityPoolDetailsModuleInput: AnyObject {} + +protocol LiquidityPoolDetailsModuleOutput: AnyObject {} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsRouter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsRouter.swift new file mode 100644 index 0000000000..451317e9ff --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsRouter.swift @@ -0,0 +1,3 @@ +import Foundation + +final class LiquidityPoolDetailsRouter: LiquidityPoolDetailsRouterInput {} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewController.swift new file mode 100644 index 0000000000..0701addf5d --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewController.swift @@ -0,0 +1,44 @@ +import UIKit +import SoraFoundation + +final class LiquidityPoolDetailsViewController: UIViewController, ViewHolder { + typealias RootViewType = LiquidityPoolDetailsViewLayout + + // MARK: Private properties + private let output: LiquidityPoolDetailsViewOutput + + // MARK: - Constructor + init( + output: LiquidityPoolDetailsViewOutput, + localizationManager: LocalizationManagerProtocol? + ) { + self.output = output + super.init(nibName: nil, bundle: nil) + self.localizationManager = localizationManager + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Life cycle + override func loadView() { + view = LiquidityPoolDetailsViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + output.didLoad(view: self) + } + + // MARK: - Private methods +} + +// MARK: - LiquidityPoolDetailsViewInput +extension LiquidityPoolDetailsViewController: LiquidityPoolDetailsViewInput {} + +// MARK: - Localizable +extension LiquidityPoolDetailsViewController: Localizable { + func applyLocalization() {} +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift new file mode 100644 index 0000000000..b6be4d5660 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift @@ -0,0 +1,13 @@ +import UIKit + +final class LiquidityPoolDetailsViewLayout: UIView { + + override init(frame: CGRect) { + super.init(frame: frame) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} \ No newline at end of file diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListInteractor.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListInteractor.swift new file mode 100644 index 0000000000..5943e30511 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListInteractor.swift @@ -0,0 +1,111 @@ +import Foundation +import SSFPools +import SSFPolkaswap +import SSFModels +import SSFStorageQueryKit + +protocol AvailableLiquidityPoolsListInteractorOutput { + func didReceiveLiquidityPairs(pairs: [LiquidityPair]?) + func didReceivePoolsReserves(reserves: CachedStorageResponse<[PolkaswapPoolReservesInfo]>) + func didReceivePoolsAPY(apy: [PoolApyInfo]?) + func didReceivePrices(result: Result<[PriceData], Error>) + + func didReceiveLiquidityPairsError(error: Error) + func didReceivePoolsReservesError(error: Error) + func didReceivePoolsApyError(error: Error) +} + +final class AvailableLiquidityPoolsListInteractor { + private let liquidityPoolService: PolkaswapLiquidityPoolService + private var output: AvailableLiquidityPoolsListInteractorOutput? + private let priceLocalSubscriber: PriceLocalStorageSubscriber + private let chain: ChainModel + private var priceProvider: AnySingleValueProvider<[PriceData]>? + + init( + liquidityPoolService: PolkaswapLiquidityPoolService, + priceLocalSubscriber: PriceLocalStorageSubscriber, + chain: ChainModel + ) { + self.liquidityPoolService = liquidityPoolService + self.priceLocalSubscriber = priceLocalSubscriber + self.chain = chain + } + + private func fetchReserves(pools: [LiquidityPair]) { + Task { + do { + let reservesStream = try await liquidityPoolService.subscribePoolReserves(pools: pools) + + for try await reserves in reservesStream { + await MainActor.run { + output?.didReceivePoolsReserves(reserves: reserves) + } + } + } catch { + await MainActor.run { + output?.didReceivePoolsReservesError(error: error) + } + } + } + } + + private func subscribeForPrices() { + let chainAssets = chain.chainAssets + priceProvider = priceLocalSubscriber.subscribeToPrices(for: chainAssets, listener: self) + } +} + +extension AvailableLiquidityPoolsListInteractor: AvailableLiquidityPoolsListInteractorInput { + func setup(with output: AvailableLiquidityPoolsListInteractorOutput) { + self.output = output + + fetchPools() + fetchApy() + subscribeForPrices() + } + + func fetchPools() { + Task { + do { + let availablePoolsStream = try await liquidityPoolService.subscribeAvailablePools() + + for try await availablePools in availablePoolsStream { + await MainActor.run { + output?.didReceiveLiquidityPairs(pairs: availablePools.value) + } + + if let pools = availablePools.value { + fetchReserves(pools: pools) + } + } + } catch { + await MainActor.run { + output?.didReceiveLiquidityPairsError(error: error) + } + } + } + } + + func fetchApy() { + Task { + do { + let apy = try await liquidityPoolService.fetchPoolsAPY() + + await MainActor.run { + output?.didReceivePoolsAPY(apy: apy) + } + } catch { + await MainActor.run { + output?.didReceivePoolsApyError(error: error) + } + } + } + } +} + +extension AvailableLiquidityPoolsListInteractor: PriceLocalSubscriptionHandler { + func handlePrices(result: Result<[PriceData], Error>) { + output?.didReceivePrices(result: result) + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListPresenter.swift new file mode 100644 index 0000000000..8ee23110ce --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListPresenter.swift @@ -0,0 +1,110 @@ +import Foundation +import SSFPolkaswap +import SSFPools +import SSFModels +import SoraFoundation +import SSFStorageQueryKit + +protocol AvailableLiquidityPoolsListInteractorInput { + func setup(with output: AvailableLiquidityPoolsListInteractorOutput) + + func fetchPools() + func fetchApy() +} + +final class AvailableLiquidityPoolsListPresenter { + private let logger: Logger + private let interactor: AvailableLiquidityPoolsListInteractorInput + private let chain: ChainModel + private let wallet: MetaAccountModel + private let viewModelFactory: AvailableLiquidityPoolsListViewModelFactory + private weak var view: LiquidityPoolsListViewInput? + + private var pairs: [LiquidityPair]? + private var reserves: CachedStorageResponse<[PolkaswapPoolReservesInfo]>? + private var apy: [PoolApyInfo]? + private var prices: [PriceData]? + + init( + logger: Logger, + interactor: AvailableLiquidityPoolsListInteractorInput, + chain: ChainModel, + wallet: MetaAccountModel, + viewModelFactory: AvailableLiquidityPoolsListViewModelFactory, + localizationManager: LocalizationManagerProtocol + ) { + self.logger = logger + self.interactor = interactor + self.chain = chain + self.wallet = wallet + self.viewModelFactory = viewModelFactory + self.localizationManager = localizationManager + } + + private func provideViewModel() { + let viewModel = viewModelFactory.buildViewModel( + pairs: pairs, + reserves: reserves, + apyInfos: apy, + chain: chain, + prices: prices, + locale: selectedLocale, + wallet: wallet + ) + + view?.didReceive(viewModel: viewModel) + } +} + +extension AvailableLiquidityPoolsListPresenter: LiquidityPoolsListViewOutput { + func didLoad(view: LiquidityPoolsListViewInput) { + self.view = view + + interactor.setup(with: self) + } +} + +extension AvailableLiquidityPoolsListPresenter: AvailableLiquidityPoolsListInteractorOutput { + func didReceiveLiquidityPairs(pairs: [LiquidityPair]?) { + self.pairs = pairs + provideViewModel() + } + + func didReceivePoolsReserves(reserves: CachedStorageResponse<[PolkaswapPoolReservesInfo]>) { + self.reserves = reserves.merge(with: self.reserves, priorityType: .remote) + provideViewModel() + } + + func didReceivePoolsAPY(apy: [PoolApyInfo]?) { + self.apy = apy + provideViewModel() + } + + func didReceivePrices(result: Result<[PriceData], Error>) { + switch result { + case let .success(prices): + self.prices = prices + provideViewModel() + case let .failure(error): + logger.customError(error) + } + } + + func didReceiveLiquidityPairsError(error: Error) { + logger.customError(error) + } + + func didReceivePoolsReservesError(error: Error) { + logger.customError(error) + } + + func didReceivePoolsApyError(error: Error) { + logger.customError(error) + } +} + +extension AvailableLiquidityPoolsListPresenter: LiquidityPoolsListModuleInput {} + +extension AvailableLiquidityPoolsListPresenter: Localizable { + func applyLocalization() {} +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListViewModelFactory.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListViewModelFactory.swift new file mode 100644 index 0000000000..505aa9c70d --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListViewModelFactory.swift @@ -0,0 +1,102 @@ +import Foundation +import SoraFoundation +import SSFPools +import SSFPolkaswap +import SSFModels +import BigInt +import SSFStorageQueryKit + +protocol AvailableLiquidityPoolsListViewModelFactory { + func buildViewModel( + pairs: [LiquidityPair]?, + reserves: CachedStorageResponse<[PolkaswapPoolReservesInfo]>?, + apyInfos: [PoolApyInfo]?, + chain: ChainModel, + prices: [PriceData]?, + locale: Locale, + wallet: MetaAccountModel + ) -> LiquidityPoolListViewModel +} + +final class AvailableLiquidityPoolsListViewModelFactoryDefault: AvailableLiquidityPoolsListViewModelFactory { + private let modelFactory: LiquidityPoolsModelFactory + private let assetBalanceFormatterFactory: AssetBalanceFormatterFactoryProtocol + + init(modelFactory: LiquidityPoolsModelFactory, assetBalanceFormatterFactory: AssetBalanceFormatterFactoryProtocol) { + self.modelFactory = modelFactory + self.assetBalanceFormatterFactory = assetBalanceFormatterFactory + } + + func buildViewModel( + pairs: [LiquidityPair]?, + reserves: CachedStorageResponse<[PolkaswapPoolReservesInfo]>?, + apyInfos: [PoolApyInfo]?, + chain: ChainModel, + prices: [PriceData]?, + locale: Locale, + wallet: MetaAccountModel + ) -> LiquidityPoolListViewModel { + let poolViewModels: [LiquidityPoolListCellModel]? = pairs?.sorted().compactMap { pair in + let baseAsset = chain.assets.first(where: { $0.currencyId == pair.baseAssetId }) + let targetAsset = chain.assets.first(where: { $0.currencyId == pair.targetAssetId }) + let rewardAsset = chain.assets.first(where: { $0.currencyId == pair.rewardAssetId }) + + guard let baseAsset = baseAsset, let targetAsset = targetAsset, let rewardAsset = rewardAsset else { + return nil + } + + let fiatFormatter = fiatFormatter(for: wallet.selectedCurrency, locale: locale) + + let baseAssetIconViewModel = RemoteImageViewModel(url: baseAsset.icon) + let targetAssetIconViewModel = RemoteImageViewModel(url: targetAsset.icon) + + let iconsViewModel = TokenPairsIconViewModel(firstTokenIconViewModel: baseAssetIconViewModel, secondTokenIconViewModel: targetAssetIconViewModel) + let tokenPairName = "\(baseAsset.symbol.uppercased())-\(targetAsset.symbol.uppercased())" + + let reservesAddress = pair.reservesId.map { try? AddressFactory.address(for: Data(hex: $0), chain: chain) } + let rewardTokenNameLabelText = "Earn \(rewardAsset.symbol.uppercased())" + let apyValue = apyInfos?.first(where: { $0.poolId == reservesAddress })?.apy + let apyLabelText = apyValue.flatMap { NumberFormatter.percentAPY.stringFromDecimal($0) } + + let baseAssetPrice = prices?.first(where: { $0.priceId == baseAsset.priceId }) + let targetAssetPrice = prices?.first(where: { $0.priceId == targetAsset.priceId }) + let poolReservesInfo = reserves?.value?.first(where: { $0.poolId == pair.pairId }) + let reservesValue = modelFactory.buildReserves( + pool: pair, + chain: chain, + reserves: poolReservesInfo, + baseAssetPrice: baseAssetPrice, + targetAssetPrice: targetAssetPrice + ) + let reservesString = reservesValue.flatMap { fiatFormatter.stringFromDecimal($0) } + let reservesLabelText: String? = reservesString.flatMap { "\($0) TVL" } + + let reservesLabelValue: ShimmeredLabelState = reserves?.type == .cache ? .updating(reservesLabelText) : .normal(reservesLabelText) + return LiquidityPoolListCellModel( + tokenPairIconsVieWModel: iconsViewModel, + tokenPairNameLabelText: tokenPairName, + rewardTokenNameLabelText: rewardTokenNameLabelText, + apyLabelText: apyLabelText, + stakingStatusLabelText: nil, + reservesLabelValue: reservesLabelValue, + sortValue: reservesValue.or(.zero) + ) + }.sorted(by: { $0.sortValue > $1.sortValue }) + + return LiquidityPoolListViewModel( + poolViewModels: poolViewModels, + titleLabelText: "Available pools", + moreButtonVisible: true + ) + } + + private func fiatFormatter( + for currency: Currency, + locale: Locale + ) -> TokenFormatter { + let displayInfo = AssetBalanceDisplayInfo.forCurrency(currency) + let tokenFormatter = assetBalanceFormatterFactory.createTokenFormatter(for: displayInfo, usageCase: .fiat) + let tokenFormatterValue = tokenFormatter.value(for: locale) + return tokenFormatterValue + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListInteractor.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListInteractor.swift new file mode 100644 index 0000000000..e9e1e96d60 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListInteractor.swift @@ -0,0 +1,92 @@ +import Foundation +import SSFPools +import SSFPolkaswap +import SSFModels +import SSFStorageQueryKit + +protocol UserLiquidityPoolsListInteractorOutput { + func didReceiveUserPools(pools: [LiquidityPair]?) + func didReceivePoolsReserves(reserves: CachedStorageResponse<[PolkaswapPoolReservesInfo]>?) + func didReceivePoolsAPY(apy: [PoolApyInfo]) + func didReceivePrices(result: Result<[PriceData], Error>) + + func didReceiveLiquidityPairsError(error: Error) + func didReceivePoolsReservesError(error: Error) + func didReceivePoolsApyError(error: Error) +} + +final class UserLiquidityPoolsListInteractor { + private let liquidityPoolService: PolkaswapLiquidityPoolService + private var output: UserLiquidityPoolsListInteractorOutput? + private let priceLocalSubscriber: PriceLocalStorageSubscriber + private let chain: ChainModel + private let wallet: MetaAccountModel + private var priceProvider: AnySingleValueProvider<[PriceData]>? + + init( + liquidityPoolService: PolkaswapLiquidityPoolService, + priceLocalSubscriber: PriceLocalStorageSubscriber, + chain: ChainModel, + wallet: MetaAccountModel + ) { + self.liquidityPoolService = liquidityPoolService + self.priceLocalSubscriber = priceLocalSubscriber + self.chain = chain + self.wallet = wallet + } + + private func subscribeForPrices() { + let chainAssets = chain.chainAssets + priceProvider = priceLocalSubscriber.subscribeToPrices(for: chainAssets, listener: self) + } +} + +extension UserLiquidityPoolsListInteractor: UserLiquidityPoolsListInteractorInput { + func setup(with output: UserLiquidityPoolsListInteractorOutput) { + self.output = output + + fetchPools() + fetchApy() + subscribeForPrices() + } + + func fetchPools() { + guard let accountId = wallet.fetch(for: chain.accountRequest())?.accountId else { + output?.didReceiveLiquidityPairsError(error: ChainAccountFetchingError.accountNotExists) + return + } + Task { + do { + let userPoolsStream = try await liquidityPoolService.subscribeUserPools(accountId: accountId) + + for try await userPools in userPoolsStream { + await MainActor.run { + output?.didReceiveUserPools(pools: userPools.value) + } + } + } catch { + output?.didReceiveLiquidityPairsError(error: error) + } + } + } + + func fetchApy() { + Task { + do { + let apy = try await liquidityPoolService.fetchPoolsAPY() + + await MainActor.run { + output?.didReceivePoolsAPY(apy: apy) + } + } catch { + output?.didReceivePoolsApyError(error: error) + } + } + } +} + +extension UserLiquidityPoolsListInteractor: PriceLocalSubscriptionHandler { + func handlePrices(result: Result<[PriceData], Error>) { + output?.didReceivePrices(result: result) + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift new file mode 100644 index 0000000000..daa9c3d169 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift @@ -0,0 +1,110 @@ +import Foundation +import SSFPolkaswap +import SSFPools +import SSFModels +import SoraFoundation +import SSFStorageQueryKit + +protocol UserLiquidityPoolsListInteractorInput { + func setup(with output: UserLiquidityPoolsListInteractorOutput) + + func fetchPools() + func fetchApy() +} + +final class UserLiquidityPoolsListPresenter { + private let logger: Logger + private let interactor: UserLiquidityPoolsListInteractorInput + private let chain: ChainModel + private let wallet: MetaAccountModel + private let viewModelFactory: UserLiquidityPoolsListViewModelFactory + private weak var view: LiquidityPoolsListViewInput? + + private var pools: [LiquidityPair]? + private var reserves: CachedStorageResponse<[PolkaswapPoolReservesInfo]>? + private var apy: [PoolApyInfo]? + private var prices: [PriceData]? + + init( + logger: Logger, + interactor: UserLiquidityPoolsListInteractorInput, + chain: ChainModel, + wallet: MetaAccountModel, + viewModelFactory: UserLiquidityPoolsListViewModelFactory, + localizationManager: LocalizationManagerProtocol + ) { + self.logger = logger + self.interactor = interactor + self.chain = chain + self.wallet = wallet + self.viewModelFactory = viewModelFactory + self.localizationManager = localizationManager + } + + private func provideViewModel() { + let viewModel = viewModelFactory.buildViewModel( + pools: pools, + reserves: reserves, + apyInfos: apy, + chain: chain, + prices: prices, + locale: selectedLocale, + wallet: wallet + ) + + view?.didReceive(viewModel: viewModel) + } +} + +extension UserLiquidityPoolsListPresenter: LiquidityPoolsListViewOutput { + func didLoad(view: LiquidityPoolsListViewInput) { + self.view = view + + interactor.setup(with: self) + } +} + +extension UserLiquidityPoolsListPresenter: UserLiquidityPoolsListInteractorOutput { + func didReceiveUserPools(pools: [LiquidityPair]?) { + self.pools = pools + provideViewModel() + } + + func didReceivePoolsReserves(reserves: CachedStorageResponse<[PolkaswapPoolReservesInfo]>?) { + self.reserves = reserves + provideViewModel() + } + + func didReceivePoolsAPY(apy: [PoolApyInfo]) { + self.apy = apy + provideViewModel() + } + + func didReceivePrices(result: Result<[PriceData], Error>) { + switch result { + case let .success(prices): + self.prices = prices + provideViewModel() + case let .failure(error): + logger.customError(error) + } + } + + func didReceiveLiquidityPairsError(error: Error) { + logger.customError(error) + } + + func didReceivePoolsReservesError(error: Error) { + logger.customError(error) + } + + func didReceivePoolsApyError(error: Error) { + logger.customError(error) + } +} + +extension UserLiquidityPoolsListPresenter: LiquidityPoolsListModuleInput {} + +extension UserLiquidityPoolsListPresenter: Localizable { + func applyLocalization() {} +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListViewModelFactory.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListViewModelFactory.swift new file mode 100644 index 0000000000..1d32556726 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListViewModelFactory.swift @@ -0,0 +1,102 @@ +import Foundation +import SoraFoundation +import SSFPools +import SSFPolkaswap +import SSFModels +import BigInt +import SSFStorageQueryKit + +protocol UserLiquidityPoolsListViewModelFactory { + func buildViewModel( + pools: [LiquidityPair]?, + reserves: CachedStorageResponse<[PolkaswapPoolReservesInfo]>?, + apyInfos: [PoolApyInfo]?, + chain: ChainModel, + prices: [PriceData]?, + locale: Locale, + wallet: MetaAccountModel + ) -> LiquidityPoolListViewModel +} + +final class UserLiquidityPoolsListViewModelFactoryDefault: UserLiquidityPoolsListViewModelFactory { + private let modelFactory: LiquidityPoolsModelFactory + private let assetBalanceFormatterFactory: AssetBalanceFormatterFactoryProtocol + + init(modelFactory: LiquidityPoolsModelFactory, assetBalanceFormatterFactory: AssetBalanceFormatterFactoryProtocol) { + self.modelFactory = modelFactory + self.assetBalanceFormatterFactory = assetBalanceFormatterFactory + } + + func buildViewModel( + pools: [LiquidityPair]?, + reserves: CachedStorageResponse<[PolkaswapPoolReservesInfo]>?, + apyInfos: [PoolApyInfo]?, + chain: ChainModel, + prices: [PriceData]?, + locale: Locale, + wallet: MetaAccountModel + ) -> LiquidityPoolListViewModel { + let poolViewModels: [LiquidityPoolListCellModel]? = pools?.sorted().compactMap { pair in + let baseAsset = chain.assets.first(where: { $0.currencyId == pair.baseAssetId }) + let targetAsset = chain.assets.first(where: { $0.currencyId == pair.targetAssetId }) + let rewardAsset = chain.assets.first(where: { $0.currencyId == pair.rewardAssetId }) + + guard let baseAsset = baseAsset, let targetAsset = targetAsset, let rewardAsset = rewardAsset else { + return nil + } + + let fiatFormatter = fiatFormatter(for: wallet.selectedCurrency, locale: locale) + + let baseAssetIconViewModel = RemoteImageViewModel(url: baseAsset.icon) + let targetAssetIconViewModel = RemoteImageViewModel(url: targetAsset.icon) + + let iconsViewModel = TokenPairsIconViewModel(firstTokenIconViewModel: baseAssetIconViewModel, secondTokenIconViewModel: targetAssetIconViewModel) + let tokenPairName = "\(baseAsset.symbol.uppercased())-\(targetAsset.symbol.uppercased())" + + let reservesAddress = pair.reservesId.map { try? AddressFactory.address(for: Data(hex: $0), chain: chain) } + let rewardTokenNameLabelText = "Earn \(rewardAsset.symbol.uppercased())" + let apyValue = apyInfos?.first(where: { $0.poolId == reservesAddress })?.apy + let apyLabelText = apyValue.flatMap { "\($0)% APY" } + + let baseAssetPrice = prices?.first(where: { $0.priceId == baseAsset.priceId }) + let targetAssetPrice = prices?.first(where: { $0.priceId == targetAsset.priceId }) + let poolReservesInfo = reserves?.value?.first(where: { $0.poolId == pair.pairId }) + let reservesValue = modelFactory.buildReserves( + pool: pair, + chain: chain, + reserves: poolReservesInfo, + baseAssetPrice: baseAssetPrice, + targetAssetPrice: targetAssetPrice + ) + let reservesString = reservesValue.flatMap { fiatFormatter.stringFromDecimal($0) } + let reservesLabelText: String? = reservesString.flatMap { "\($0) TVL" } + let reservesLabelValue: ShimmeredLabelState = reserves?.type == .cache ? .updating(reservesLabelText) : .normal(reservesLabelText) + + return LiquidityPoolListCellModel( + tokenPairIconsVieWModel: iconsViewModel, + tokenPairNameLabelText: tokenPairName, + rewardTokenNameLabelText: rewardTokenNameLabelText, + apyLabelText: apyLabelText, + stakingStatusLabelText: nil, + reservesLabelValue: reservesLabelValue, + sortValue: reservesValue.or(.zero) + ) + }.sorted(by: { $0.sortValue > $1.sortValue }) + + return LiquidityPoolListViewModel( + poolViewModels: poolViewModels, + titleLabelText: "User pools", + moreButtonVisible: true + ) + } + + private func fiatFormatter( + for currency: Currency, + locale: Locale + ) -> TokenFormatter { + let displayInfo = AssetBalanceDisplayInfo.forCurrency(currency) + let tokenFormatter = assetBalanceFormatterFactory.createTokenFormatter(for: displayInfo, usageCase: .fiat) + let tokenFormatterValue = tokenFormatter.value(for: locale) + return tokenFormatterValue + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListAssembly.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListAssembly.swift new file mode 100644 index 0000000000..29e45958f9 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListAssembly.swift @@ -0,0 +1,75 @@ +import UIKit +import SoraFoundation +import SSFPolkaswap +import SSFModels +import SSFStorageQueryKit +import SSFChainRegistry +import sorawallet +import SSFRuntimeCodingService + +final class LiquidityPoolsListAssembly { + static func configureAvailablePoolsModule(chain: ChainModel, wallet: MetaAccountModel) -> LiquidityPoolsListModuleCreationResult? { + let localizationManager = LocalizationManager.shared + let chainRegistry = ChainRegistryFacade.sharedRegistry + + let poolService = PolkaswapLiquidityPoolServiceAssembly.buildService(for: chain, chainRegistry: chainRegistry) + let interactor = AvailableLiquidityPoolsListInteractor( + liquidityPoolService: poolService, + priceLocalSubscriber: PriceLocalStorageSubscriberImpl.shared, + chain: chain + ) + let router = LiquidityPoolsListRouter() + let viewModelFactory = AvailableLiquidityPoolsListViewModelFactoryDefault( + modelFactory: LiquidityPoolsModelFactoryDefault(), + assetBalanceFormatterFactory: AssetBalanceFormatterFactory() + ) + let presenter = AvailableLiquidityPoolsListPresenter( + logger: Logger.shared, + interactor: interactor, + chain: chain, + wallet: wallet, + viewModelFactory: viewModelFactory, + localizationManager: LocalizationManager.shared + ) + + let view = LiquidityPoolsListViewController( + output: presenter, + localizationManager: localizationManager + ) + + return (view, presenter) + } + + static func configureUserPoolsModule(chain: ChainModel, wallet: MetaAccountModel) -> LiquidityPoolsListModuleCreationResult? { + let localizationManager = LocalizationManager.shared + let chainRegistry = ChainRegistryFacade.sharedRegistry + + let poolService = PolkaswapLiquidityPoolServiceAssembly.buildService(for: chain, chainRegistry: chainRegistry) + let interactor = UserLiquidityPoolsListInteractor( + liquidityPoolService: poolService, + priceLocalSubscriber: PriceLocalStorageSubscriberImpl.shared, + chain: chain, + wallet: wallet + ) + let router = LiquidityPoolsListRouter() + let viewModelFactory = UserLiquidityPoolsListViewModelFactoryDefault( + modelFactory: LiquidityPoolsModelFactoryDefault(), + assetBalanceFormatterFactory: AssetBalanceFormatterFactory() + ) + let presenter = UserLiquidityPoolsListPresenter( + logger: Logger.shared, + interactor: interactor, + chain: chain, + wallet: wallet, + viewModelFactory: viewModelFactory, + localizationManager: LocalizationManager.shared + ) + + let view = LiquidityPoolsListViewController( + output: presenter, + localizationManager: localizationManager + ) + + return (view, presenter) + } +} diff --git a/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListProtocols.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListProtocols.swift similarity index 77% rename from fearless/Modules/LiquidityPoolsList/LiquidityPoolsListProtocols.swift rename to fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListProtocols.swift index 5b3c88f29d..3683188d17 100644 --- a/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListProtocols.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListProtocols.swift @@ -1,17 +1,18 @@ typealias LiquidityPoolsListModuleCreationResult = (view: LiquidityPoolsListViewInput, input: LiquidityPoolsListModuleInput) -protocol LiquidityPoolsListViewInput: ControllerBackedProtocol {} +protocol LiquidityPoolsListViewInput: ControllerBackedProtocol { + func didReceive(viewModel: LiquidityPoolListViewModel) +} protocol LiquidityPoolsListViewOutput: AnyObject { func didLoad(view: LiquidityPoolsListViewInput) } protocol LiquidityPoolsListInteractorInput: AnyObject { - func setup(with output: LiquidityPoolsListInteractorOutput) + func setup(with output: AvailableLiquidityPoolsListInteractorOutput) + func fetchPools() } -protocol LiquidityPoolsListInteractorOutput: AnyObject {} - protocol LiquidityPoolsListRouterInput: AnyObject {} protocol LiquidityPoolsListModuleInput: AnyObject {} diff --git a/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListRouter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListRouter.swift similarity index 100% rename from fearless/Modules/LiquidityPoolsList/LiquidityPoolsListRouter.swift rename to fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListRouter.swift diff --git a/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift similarity index 63% rename from fearless/Modules/LiquidityPoolsList/LiquidityPoolsListViewController.swift rename to fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift index 7f2d9461f9..7496b0b93d 100644 --- a/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListViewController.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift @@ -6,7 +6,7 @@ final class LiquidityPoolsListViewController: UIViewController, ViewHolder { // MARK: Private properties - private var cellModels: [LiquidityPoolListCellModel] = [] + private var cellModels: [LiquidityPoolListCellModel]? private let output: LiquidityPoolsListViewOutput // MARK: - Constructor @@ -34,7 +34,7 @@ final class LiquidityPoolsListViewController: UIViewController, ViewHolder { override func viewDidLoad() { super.viewDidLoad() output.didLoad(view: self) - + rootView.tableView.delegate = self rootView.tableView.dataSource = self rootView.tableView.registerClassForCell(LiquidityPoolListCell.self) @@ -44,29 +44,42 @@ final class LiquidityPoolsListViewController: UIViewController, ViewHolder { } extension LiquidityPoolsListViewController: UITableViewDataSource, UITableViewDelegate { - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - cellModels.count + func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { + guard let cellModels else { + return 0 + } + + return cellModels.count } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - return tableView.dequeueReusableCellWithType(LiquidityPoolListCell.self) ?? UITableViewCell() + + func tableView(_ tableView: UITableView, cellForRowAt _: IndexPath) -> UITableViewCell { + tableView.dequeueReusableCellWithType(LiquidityPoolListCell.self) ?? UITableViewCell() } - - func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - guard let lpCell = cell as? LiquidityPoolListCell else { + + func tableView(_: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + guard let lpCell = cell as? LiquidityPoolListCell, let cellModels else { return } - + lpCell.bind(viewModel: cellModels[indexPath.row]) } + + func tableView(_: UITableView, heightForRowAt _: IndexPath) -> CGFloat { + 44 + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + } } // MARK: - LiquidityPoolsListViewInput extension LiquidityPoolsListViewController: LiquidityPoolsListViewInput { - func bind(viewModel: LiquidityPoolListViewModel) { + func didReceive(viewModel: LiquidityPoolListViewModel) { cellModels = viewModel.poolViewModels - + rootView.bind(viewModel: viewModel) + rootView.tableView.reloadData() } } diff --git a/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift similarity index 60% rename from fearless/Modules/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift rename to fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift index 6a7c8bd6e9..8733c76fee 100644 --- a/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift @@ -5,39 +5,45 @@ final class LiquidityPoolsListViewLayout: UIView { let topBar: BorderedContainerView = { let view = BorderedContainerView() view.borderType = .bottom - view.fillColor = R.color.colorWhite8() return view }() - + let titleLabel: UILabel = { let label = UILabel() label.font = .h5Title label.textColor = .white return label }() - + let moreButton: UIButton = { let button = UIButton(type: .custom) - button.titleLabel.font = .capsTitle + button.titleLabel?.font = .capsTitle button.setTitleColor(.white, for: .normal) + button.setImage(R.image.iconChevronRight(), for: .normal) + button.semanticContentAttribute = .forceRightToLeft button.backgroundColor = R.color.colorWhite8() return button }() - - let tableView: UITableView = { - let tableView = UITableView() - return tableView + + let tableView: SelfSizingTableView = { + let view = SelfSizingTableView(frame: .zero) + view.backgroundColor = .clear + view.separatorStyle = .none + view.contentInset = .zero + view.refreshControl = UIRefreshControl() + return view }() - + var locale: Locale = .current { didSet { applyLocalization() } } - + override init(frame: CGRect) { super.init(frame: frame) - +// backgroundColor = R.color.colorWhite4() + drawSubviews() setupConstraints() } @@ -45,37 +51,51 @@ final class LiquidityPoolsListViewLayout: UIView { @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") - - drawSubviews() - setupConstraints() } - + func bind(viewModel: LiquidityPoolListViewModel) { titleLabel.text = viewModel.titleLabelText moreButton.isHidden = !viewModel.moreButtonVisible } - + + override func layoutSubviews() { + super.layoutSubviews() + moreButton.rounded() + } + private func drawSubviews() { addSubview(topBar) addSubview(tableView) - + topBar.addSubview(titleLabel) topBar.addSubview(moreButton) } - + private func setupConstraints() { topBar.snp.makeConstraints { make in make.height.equalTo(42) make.leading.trailing.top.equalToSuperview() } - + tableView.snp.makeConstraints { make in make.top.equalTo(topBar.snp.bottom).offset(8) make.leading.trailing.bottom.equalToSuperview() - + } + + titleLabel.snp.makeConstraints { make in + make.leading.equalToSuperview().inset(12) + make.centerY.equalToSuperview() + } + + moreButton.snp.makeConstraints { make in + make.width.equalTo(61) + make.height.equalTo(24) + make.trailing.equalToSuperview().inset(12) + make.centerY.equalToSuperview() } } - + private func applyLocalization() { + moreButton.setTitle(R.string.localizable.commonMore(preferredLanguages: locale.rLanguages).uppercased(), for: .normal) } } diff --git a/fearless/Modules/LiquidityPoolsList/View/LiquidityPoolListCell.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/View/LiquidityPoolListCell.swift similarity index 63% rename from fearless/Modules/LiquidityPoolsList/View/LiquidityPoolListCell.swift rename to fearless/Modules/LiquidityPools/LiquidityPoolsList/View/LiquidityPoolListCell.swift index 2143faecd6..9108e6fd4a 100644 --- a/fearless/Modules/LiquidityPoolsList/View/LiquidityPoolListCell.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/View/LiquidityPoolListCell.swift @@ -16,10 +16,11 @@ class LiquidityPoolListCell: UITableViewCell { return label }() - let apyLabel: UILabel = { - let label = UILabel() + let apyLabel: SkeletonLabel = { + let label = SkeletonLabel(skeletonSize: CGSize(width: 60, height: 12)) label.font = .capsTitle label.textColor = R.color.colorPink() + label.textAlignment = .right return label }() @@ -30,16 +31,19 @@ class LiquidityPoolListCell: UITableViewCell { return label }() - let reservesLabel: UILabel = { - let label = UILabel() + let reservesLabel: ShimmeredLabel = { + let label = ShimmeredLabel(skeletonSize: CGSize(width: 90, height: 12)) label.font = .p2Paragraph label.textColor = R.color.colorWhite50() + label.textAlignment = .right return label }() override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) + contentView.backgroundColor = .clear + backgroundColor = .clear drawSubviews() setupConstraints() } @@ -47,64 +51,74 @@ class LiquidityPoolListCell: UITableViewCell { required init?(coder: NSCoder) { super.init(coder: coder) + contentView.backgroundColor = .clear + backgroundColor = .clear + drawSubviews() setupConstraints() } - + func bind(viewModel: LiquidityPoolListCellModel) { tokenPairIconsView.bind(viewModel: viewModel.tokenPairIconsVieWModel) tokenPairNameLabel.text = viewModel.tokenPairNameLabelText rewardTokenNameLabel.text = viewModel.rewardTokenNameLabelText - apyLabel.text = viewModel.apyLabelText stakingStatusLabel.text = viewModel.stakingStatusLabelText - reservesLabel.text = viewModel.reservesLabelText - + + apyLabel.updateTextWithLoading(viewModel.apyLabelText) + reservesLabel.apply(state: viewModel.reservesLabelValue) + stakingStatusLabel.isHidden = viewModel.stakingStatusLabelText == nil - reservesLabel.isHidden = viewModel.reservesLabelText == nil } private func drawSubviews() { - addSubview(tokenPairIconsView) - addSubview(tokenPairNameLabel) - addSubview(rewardTokenNameLabel) - addSubview(apyLabel) - addSubview(stakingStatusLabel) - addSubview(reservesLabel) + contentView.addSubview(tokenPairIconsView) + contentView.addSubview(tokenPairNameLabel) + contentView.addSubview(rewardTokenNameLabel) + contentView.addSubview(apyLabel) + contentView.addSubview(stakingStatusLabel) + contentView.addSubview(reservesLabel) } private func setupConstraints() { tokenPairIconsView.snp.makeConstraints { make in make.leading.equalToSuperview().inset(16) - make.top.bottom.equalToSuperview().inset(4) + make.width.equalTo(40) + make.height.equalTo(37) + make.centerY.equalToSuperview() } - + tokenPairNameLabel.snp.makeConstraints { make in make.leading.equalTo(tokenPairIconsView.snp.trailing).offset(8) - make.top.equalToSuperview().inset(4) + make.top.equalToSuperview().inset(5) + make.height.equalTo(15) } - + rewardTokenNameLabel.snp.makeConstraints { make in make.leading.equalTo(tokenPairIconsView.snp.trailing).offset(8) - make.bottom.equalToSuperview().inset(4) + make.bottom.equalToSuperview().inset(5) make.top.equalTo(tokenPairNameLabel.snp.bottom).offset(4) + make.height.equalTo(15) } - + apyLabel.snp.makeConstraints { make in make.trailing.equalToSuperview().inset(16) - make.leading.equalTo(tokenPairNameLabel.snp.trailing).offset(8) + make.leading.greaterThanOrEqualTo(tokenPairNameLabel.snp.trailing).offset(8) make.centerY.equalTo(tokenPairNameLabel.snp.centerY) + make.height.equalTo(15) } - + stakingStatusLabel.snp.makeConstraints { make in make.trailing.equalToSuperview().inset(16) - make.leading.equalTo(rewardTokenNameLabel.snp.trailing).offset(8) + make.leading.greaterThanOrEqualTo(rewardTokenNameLabel.snp.trailing).offset(8) make.centerY.equalTo(rewardTokenNameLabel.snp.centerY) + make.height.equalTo(15) } - + reservesLabel.snp.makeConstraints { make in make.trailing.equalToSuperview().inset(16) - make.leading.equalTo(rewardTokenNameLabel.snp.trailing).offset(8) + make.leading.greaterThanOrEqualTo(rewardTokenNameLabel.snp.trailing).offset(8) make.centerY.equalTo(rewardTokenNameLabel.snp.centerY) + make.height.equalTo(15) } } } diff --git a/fearless/Modules/LiquidityPoolsList/ViewModel/LiquidityPoolListCellModel.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListCellModel.swift similarity index 68% rename from fearless/Modules/LiquidityPoolsList/ViewModel/LiquidityPoolListCellModel.swift rename to fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListCellModel.swift index 5876df7f94..cf96499c71 100644 --- a/fearless/Modules/LiquidityPoolsList/ViewModel/LiquidityPoolListCellModel.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListCellModel.swift @@ -4,7 +4,8 @@ struct LiquidityPoolListCellModel { let tokenPairIconsVieWModel: TokenPairsIconViewModel let tokenPairNameLabelText: String let rewardTokenNameLabelText: String - let apyLabelText: String + let apyLabelText: String? let stakingStatusLabelText: String? - let reservesLabelText: String? + let reservesLabelValue: ShimmeredLabelState? + let sortValue: Decimal } diff --git a/fearless/Modules/LiquidityPoolsList/ViewModel/LiquidityPoolListViewModel.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListViewModel.swift similarity index 68% rename from fearless/Modules/LiquidityPoolsList/ViewModel/LiquidityPoolListViewModel.swift rename to fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListViewModel.swift index e108ca5834..377fb42fbf 100644 --- a/fearless/Modules/LiquidityPoolsList/ViewModel/LiquidityPoolListViewModel.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListViewModel.swift @@ -1,7 +1,7 @@ import Foundation struct LiquidityPoolListViewModel { - let poolViewModels: [LiquidityPoolListCellModel] + let poolViewModels: [LiquidityPoolListCellModel]? let titleLabelText: String let moreButtonVisible: Bool } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewAssembly.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewAssembly.swift new file mode 100644 index 0000000000..0e2932e1a7 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewAssembly.swift @@ -0,0 +1,48 @@ +import UIKit +import SoraFoundation +import SSFModels + +final class LiquidityPoolsOverviewAssembly { + static func configureModule(chain: ChainModel, wallet: MetaAccountModel) -> LiquidityPoolsOverviewModuleCreationResult? { + let localizationManager = LocalizationManager.shared + + let interactor = LiquidityPoolsOverviewInteractor() + let router = LiquidityPoolsOverviewRouter() + + let presenter = LiquidityPoolsOverviewPresenter( + interactor: interactor, + router: router, + localizationManager: localizationManager + ) + + let userPoolsModule = configureUserPoolsModule(chain: chain, wallet: wallet) + let availablePoolsModule = configureAvailablePoolsModule(chain: chain, wallet: wallet) + + guard let userPoolsModule, let availablePoolsModule else { + return nil + } + + let view = LiquidityPoolsOverviewViewController( + output: presenter, + localizationManager: localizationManager, + userPoolsViewController: userPoolsModule.view.controller, + availablePoolsViewController: availablePoolsModule.view.controller + ) + + return (view, presenter) + } + + private static func configureUserPoolsModule( + chain: ChainModel, + wallet: MetaAccountModel + ) -> LiquidityPoolsListModuleCreationResult? { + LiquidityPoolsListAssembly.configureUserPoolsModule(chain: chain, wallet: wallet) + } + + private static func configureAvailablePoolsModule( + chain: ChainModel, + wallet: MetaAccountModel + ) -> LiquidityPoolsListModuleCreationResult? { + LiquidityPoolsListAssembly.configureAvailablePoolsModule(chain: chain, wallet: wallet) + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewInteractor.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewInteractor.swift new file mode 100644 index 0000000000..2d9f122205 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewInteractor.swift @@ -0,0 +1,15 @@ +import UIKit + +final class LiquidityPoolsOverviewInteractor { + // MARK: - Private properties + + private weak var output: LiquidityPoolsOverviewInteractorOutput? +} + +// MARK: - LiquidityPoolsOverviewInteractorInput + +extension LiquidityPoolsOverviewInteractor: LiquidityPoolsOverviewInteractorInput { + func setup(with output: LiquidityPoolsOverviewInteractorOutput) { + self.output = output + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewPresenter.swift new file mode 100644 index 0000000000..3ae1fe6115 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewPresenter.swift @@ -0,0 +1,45 @@ +import Foundation +import SoraFoundation + +final class LiquidityPoolsOverviewPresenter { + // MARK: Private properties + + private weak var view: LiquidityPoolsOverviewViewInput? + private let router: LiquidityPoolsOverviewRouterInput + private let interactor: LiquidityPoolsOverviewInteractorInput + + // MARK: - Constructors + + init( + interactor: LiquidityPoolsOverviewInteractorInput, + router: LiquidityPoolsOverviewRouterInput, + localizationManager: LocalizationManagerProtocol + ) { + self.interactor = interactor + self.router = router + self.localizationManager = localizationManager + } + + // MARK: - Private methods +} + +// MARK: - LiquidityPoolsOverviewViewOutput + +extension LiquidityPoolsOverviewPresenter: LiquidityPoolsOverviewViewOutput { + func didLoad(view: LiquidityPoolsOverviewViewInput) { + self.view = view + interactor.setup(with: self) + } +} + +// MARK: - LiquidityPoolsOverviewInteractorOutput + +extension LiquidityPoolsOverviewPresenter: LiquidityPoolsOverviewInteractorOutput {} + +// MARK: - Localizable + +extension LiquidityPoolsOverviewPresenter: Localizable { + func applyLocalization() {} +} + +extension LiquidityPoolsOverviewPresenter: LiquidityPoolsOverviewModuleInput {} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewProtocols.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewProtocols.swift new file mode 100644 index 0000000000..df67f0f1db --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewProtocols.swift @@ -0,0 +1,19 @@ +typealias LiquidityPoolsOverviewModuleCreationResult = (view: LiquidityPoolsOverviewViewInput, input: LiquidityPoolsOverviewModuleInput) + +protocol LiquidityPoolsOverviewViewInput: ControllerBackedProtocol {} + +protocol LiquidityPoolsOverviewViewOutput: AnyObject { + func didLoad(view: LiquidityPoolsOverviewViewInput) +} + +protocol LiquidityPoolsOverviewInteractorInput: AnyObject { + func setup(with output: LiquidityPoolsOverviewInteractorOutput) +} + +protocol LiquidityPoolsOverviewInteractorOutput: AnyObject {} + +protocol LiquidityPoolsOverviewRouterInput: AnyObject {} + +protocol LiquidityPoolsOverviewModuleInput: AnyObject {} + +protocol LiquidityPoolsOverviewModuleOutput: AnyObject {} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewRouter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewRouter.swift new file mode 100644 index 0000000000..9c3359bd88 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewRouter.swift @@ -0,0 +1,3 @@ +import Foundation + +final class LiquidityPoolsOverviewRouter: LiquidityPoolsOverviewRouterInput {} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewController.swift new file mode 100644 index 0000000000..2b827e7101 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewController.swift @@ -0,0 +1,81 @@ +import UIKit +import SoraFoundation + +final class LiquidityPoolsOverviewViewController: UIViewController, ViewHolder, HiddableBarWhenPushed { + typealias RootViewType = LiquidityPoolsOverviewViewLayout + + // MARK: Private properties + + private let output: LiquidityPoolsOverviewViewOutput + + private let userPoolsViewController: UIViewController + private let availablePoolsViewController: UIViewController + + // MARK: - Constructor + + init( + output: LiquidityPoolsOverviewViewOutput, + localizationManager: LocalizationManagerProtocol?, + userPoolsViewController: UIViewController, + availablePoolsViewController: UIViewController + ) { + self.output = output + self.userPoolsViewController = userPoolsViewController + self.availablePoolsViewController = availablePoolsViewController + super.init(nibName: nil, bundle: nil) + self.localizationManager = localizationManager + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Life cycle + + override func loadView() { + view = LiquidityPoolsOverviewViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + output.didLoad(view: self) + + setupEmbededUserPoolsView() + setupEmbededAvailablePoolsView() + } + + // MARK: - Private methods + + private func setupEmbededUserPoolsView() { + addChild(userPoolsViewController) + + guard let view = userPoolsViewController.view else { + return + } + + rootView.addUserPoolsView(view) + controller.didMove(toParent: self) + } + + private func setupEmbededAvailablePoolsView() { + addChild(availablePoolsViewController) + + guard let view = availablePoolsViewController.view else { + return + } + + rootView.addAvailablePoolsView(view) + controller.didMove(toParent: self) + } +} + +// MARK: - LiquidityPoolsOverviewViewInput + +extension LiquidityPoolsOverviewViewController: LiquidityPoolsOverviewViewInput {} + +// MARK: - Localizable + +extension LiquidityPoolsOverviewViewController: Localizable { + func applyLocalization() {} +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewLayout.swift new file mode 100644 index 0000000000..05851388cc --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewLayout.swift @@ -0,0 +1,72 @@ +import UIKit + +final class LiquidityPoolsOverviewViewLayout: UIView { + private let backgroundImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFill + imageView.image = R.image.backgroundImage() + return imageView + }() + + let scrollView = UIScrollView() + let scrollViewBackgroundView = UIView() + let userPoolsContainerView = TriangularedBlurView() + let availablePoolsContainerView = TriangularedBlurView() + + override init(frame: CGRect) { + super.init(frame: frame) + drawSubviews() + setupConstraints() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func addUserPoolsView(_ view: UIView) { + userPoolsContainerView.addSubview(view) + view.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + + func addAvailablePoolsView(_ view: UIView) { + availablePoolsContainerView.addSubview(view) + view.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + + private func drawSubviews() { + addSubview(backgroundImageView) + addSubview(scrollView) + scrollView.addSubview(scrollViewBackgroundView) + scrollViewBackgroundView.addSubview(userPoolsContainerView) + scrollViewBackgroundView.addSubview(availablePoolsContainerView) + } + + private func setupConstraints() { + backgroundImageView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + scrollView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + scrollViewBackgroundView.snp.makeConstraints { make in + make.edges.equalToSuperview() + make.width.height.equalTo(self) + } + + userPoolsContainerView.snp.makeConstraints { make in + make.leading.trailing.top.equalToSuperview().inset(16) + } + + availablePoolsContainerView.snp.makeConstraints { make in + make.leading.trailing.bottom.equalToSuperview().inset(16) + make.top.equalTo(userPoolsContainerView.snp.bottom).offset(16) + } + } +} diff --git a/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListAssembly.swift b/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListAssembly.swift deleted file mode 100644 index 4d9a0a722f..0000000000 --- a/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListAssembly.swift +++ /dev/null @@ -1,24 +0,0 @@ -import UIKit -import SoraFoundation - -final class LiquidityPoolsListAssembly { - static func configureModule() -> LiquidityPoolsListModuleCreationResult? { - let localizationManager = LocalizationManager.shared - - let interactor = LiquidityPoolsListInteractor() - let router = LiquidityPoolsListRouter() - - let presenter = LiquidityPoolsListPresenter( - interactor: interactor, - router: router, - localizationManager: localizationManager - ) - - let view = LiquidityPoolsListViewController( - output: presenter, - localizationManager: localizationManager - ) - - return (view, presenter) - } -} diff --git a/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListInteractor.swift b/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListInteractor.swift deleted file mode 100644 index 2656d5fa45..0000000000 --- a/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListInteractor.swift +++ /dev/null @@ -1,15 +0,0 @@ -import UIKit - -final class LiquidityPoolsListInteractor { - // MARK: - Private properties - - private weak var output: LiquidityPoolsListInteractorOutput? -} - -// MARK: - LiquidityPoolsListInteractorInput - -extension LiquidityPoolsListInteractor: LiquidityPoolsListInteractorInput { - func setup(with output: LiquidityPoolsListInteractorOutput) { - self.output = output - } -} diff --git a/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListPresenter.swift b/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListPresenter.swift deleted file mode 100644 index 84bb5304d3..0000000000 --- a/fearless/Modules/LiquidityPoolsList/LiquidityPoolsListPresenter.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Foundation -import SoraFoundation - -final class LiquidityPoolsListPresenter { - // MARK: Private properties - - private weak var view: LiquidityPoolsListViewInput? - private let router: LiquidityPoolsListRouterInput - private let interactor: LiquidityPoolsListInteractorInput - - // MARK: - Constructors - - init( - interactor: LiquidityPoolsListInteractorInput, - router: LiquidityPoolsListRouterInput, - localizationManager: LocalizationManagerProtocol - ) { - self.interactor = interactor - self.router = router - self.localizationManager = localizationManager - } - - // MARK: - Private methods -} - -// MARK: - LiquidityPoolsListViewOutput - -extension LiquidityPoolsListPresenter: LiquidityPoolsListViewOutput { - func didLoad(view: LiquidityPoolsListViewInput) { - self.view = view - interactor.setup(with: self) - } -} - -// MARK: - LiquidityPoolsListInteractorOutput - -extension LiquidityPoolsListPresenter: LiquidityPoolsListInteractorOutput {} - -// MARK: - Localizable - -extension LiquidityPoolsListPresenter: Localizable { - func applyLocalization() {} -} - -extension LiquidityPoolsListPresenter: LiquidityPoolsListModuleInput {} diff --git a/fearless/Modules/MainTabBar/MainTabBarInteractor.swift b/fearless/Modules/MainTabBar/MainTabBarInteractor.swift index 63e99c2602..426ad92666 100644 --- a/fearless/Modules/MainTabBar/MainTabBarInteractor.swift +++ b/fearless/Modules/MainTabBar/MainTabBarInteractor.swift @@ -1,7 +1,7 @@ import Foundation import WalletConnectSign import SoraKeystore -import CommonWallet + import SSFUtils import SoraFoundation diff --git a/fearless/Modules/MainTabBar/MainTabBarProtocol.swift b/fearless/Modules/MainTabBar/MainTabBarProtocol.swift index 3c815ecc62..9ce7dc6dbb 100644 --- a/fearless/Modules/MainTabBar/MainTabBarProtocol.swift +++ b/fearless/Modules/MainTabBar/MainTabBarProtocol.swift @@ -1,6 +1,5 @@ import UIKit import WalletConnectSign -import CommonWallet protocol MainTabBarViewProtocol: ControllerBackedProtocol { func didReplaceView(for newView: UIViewController, for index: Int) diff --git a/fearless/Modules/MainTabBar/MainTabBarViewFactory.swift b/fearless/Modules/MainTabBar/MainTabBarViewFactory.swift index e4dba7199e..322ab9cfd7 100644 --- a/fearless/Modules/MainTabBar/MainTabBarViewFactory.swift +++ b/fearless/Modules/MainTabBar/MainTabBarViewFactory.swift @@ -1,7 +1,7 @@ import UIKit import SoraFoundation import SoraKeystore -import CommonWallet + import SSFUtils final class MainTabBarViewFactory: MainTabBarViewFactoryProtocol { diff --git a/fearless/Modules/MainTabBar/MainTabBarWireframe.swift b/fearless/Modules/MainTabBar/MainTabBarWireframe.swift index de37b529ce..25f35201e3 100644 --- a/fearless/Modules/MainTabBar/MainTabBarWireframe.swift +++ b/fearless/Modules/MainTabBar/MainTabBarWireframe.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet +import UIKit import WalletConnectSign final class MainTabBarWireframe: MainTabBarWireframeProtocol { diff --git a/fearless/Modules/NewWallet/ChainAccount/ChainAccountWireframe.swift b/fearless/Modules/NewWallet/ChainAccount/ChainAccountWireframe.swift index 4df0cde6ce..1366193ce2 100644 --- a/fearless/Modules/NewWallet/ChainAccount/ChainAccountWireframe.swift +++ b/fearless/Modules/NewWallet/ChainAccount/ChainAccountWireframe.swift @@ -221,7 +221,7 @@ final class ChainAccountWireframe: ChainAccountWireframeProtocol { chainAsset: ChainAsset, wallet: MetaAccountModel ) { - guard let module = PolkaswapAdjustmentAssembly.configureModule(chainAsset: chainAsset, wallet: wallet) else { + guard let module = LiquidityPoolsOverviewAssembly.configureModule(chain: chainAsset.chain, wallet: wallet) else { return } let navigationController = FearlessNavigationController(rootViewController: module.view.controller) diff --git a/fearless/Modules/NewWallet/WalletTransactionDetails/ViewModel/WalletTransactionDetailsViewModel.swift b/fearless/Modules/NewWallet/WalletTransactionDetails/ViewModel/WalletTransactionDetailsViewModel.swift index e586f17b8a..ee69eb4a55 100644 --- a/fearless/Modules/NewWallet/WalletTransactionDetails/ViewModel/WalletTransactionDetailsViewModel.swift +++ b/fearless/Modules/NewWallet/WalletTransactionDetails/ViewModel/WalletTransactionDetailsViewModel.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet +import UIKit class WalletTransactionDetailsViewModel { let transaction: AssetTransactionData diff --git a/fearless/Modules/NewWallet/WalletTransactionDetails/ViewModel/WalletTransactionDetailsViewModelFactory.swift b/fearless/Modules/NewWallet/WalletTransactionDetails/ViewModel/WalletTransactionDetailsViewModelFactory.swift index e1187047d1..c8593b69ab 100644 --- a/fearless/Modules/NewWallet/WalletTransactionDetails/ViewModel/WalletTransactionDetailsViewModelFactory.swift +++ b/fearless/Modules/NewWallet/WalletTransactionDetails/ViewModel/WalletTransactionDetailsViewModelFactory.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SoraFoundation import SSFModels diff --git a/fearless/Modules/NewWallet/WalletTransactionDetails/WalletTransactionDetailsInteractor.swift b/fearless/Modules/NewWallet/WalletTransactionDetails/WalletTransactionDetailsInteractor.swift index 29b05992a2..123803a1f3 100644 --- a/fearless/Modules/NewWallet/WalletTransactionDetails/WalletTransactionDetailsInteractor.swift +++ b/fearless/Modules/NewWallet/WalletTransactionDetails/WalletTransactionDetailsInteractor.swift @@ -1,5 +1,4 @@ import UIKit -import CommonWallet final class WalletTransactionDetailsInteractor { let transaction: AssetTransactionData diff --git a/fearless/Modules/NewWallet/WalletTransactionDetails/WalletTransactionDetailsPresenter.swift b/fearless/Modules/NewWallet/WalletTransactionDetails/WalletTransactionDetailsPresenter.swift index cf50355024..1cd1c3f2c5 100644 --- a/fearless/Modules/NewWallet/WalletTransactionDetails/WalletTransactionDetailsPresenter.swift +++ b/fearless/Modules/NewWallet/WalletTransactionDetails/WalletTransactionDetailsPresenter.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SoraFoundation import SSFModels diff --git a/fearless/Modules/NewWallet/WalletTransactionDetails/WalletTransactionDetailsProtocols.swift b/fearless/Modules/NewWallet/WalletTransactionDetails/WalletTransactionDetailsProtocols.swift index 361ed9a9a6..4e323cb509 100644 --- a/fearless/Modules/NewWallet/WalletTransactionDetails/WalletTransactionDetailsProtocols.swift +++ b/fearless/Modules/NewWallet/WalletTransactionDetails/WalletTransactionDetailsProtocols.swift @@ -1,4 +1,4 @@ -import CommonWallet + protocol WalletTransactionDetailsViewProtocol: ControllerBackedProtocol { func didReceiveState(_ state: WalletTransactionDetailsViewState) } diff --git a/fearless/Modules/NewWallet/WalletTransactionDetails/WalletTransactionDetailsViewController.swift b/fearless/Modules/NewWallet/WalletTransactionDetails/WalletTransactionDetailsViewController.swift index 18894e55a5..12e35a10c8 100644 --- a/fearless/Modules/NewWallet/WalletTransactionDetails/WalletTransactionDetailsViewController.swift +++ b/fearless/Modules/NewWallet/WalletTransactionDetails/WalletTransactionDetailsViewController.swift @@ -1,5 +1,5 @@ import UIKit -import CommonWallet + import SoraFoundation final class WalletTransactionDetailsViewController: UIViewController, ViewHolder { diff --git a/fearless/Modules/NewWallet/WalletTransactionDetails/WalletTransactionDetailsViewFactory.swift b/fearless/Modules/NewWallet/WalletTransactionDetails/WalletTransactionDetailsViewFactory.swift index 9b4030cd26..aa3071d4c3 100644 --- a/fearless/Modules/NewWallet/WalletTransactionDetails/WalletTransactionDetailsViewFactory.swift +++ b/fearless/Modules/NewWallet/WalletTransactionDetails/WalletTransactionDetailsViewFactory.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SoraFoundation import SSFModels diff --git a/fearless/Modules/NewWallet/WalletTransactionDetails/WalletTransactionDetailsViewLayout.swift b/fearless/Modules/NewWallet/WalletTransactionDetails/WalletTransactionDetailsViewLayout.swift index 80a4049fe4..220d63efa5 100644 --- a/fearless/Modules/NewWallet/WalletTransactionDetails/WalletTransactionDetailsViewLayout.swift +++ b/fearless/Modules/NewWallet/WalletTransactionDetails/WalletTransactionDetailsViewLayout.swift @@ -1,5 +1,5 @@ import UIKit -import CommonWallet + import SSFUtils final class WalletTransactionDetailsViewLayout: UIView { diff --git a/fearless/Modules/NewWallet/WalletTransactionHistory/Model/WalletTransactionHistoryDataState.swift b/fearless/Modules/NewWallet/WalletTransactionHistory/Model/WalletTransactionHistoryDataState.swift index 89ecfcb15d..0b2be7706b 100644 --- a/fearless/Modules/NewWallet/WalletTransactionHistory/Model/WalletTransactionHistoryDataState.swift +++ b/fearless/Modules/NewWallet/WalletTransactionHistory/Model/WalletTransactionHistoryDataState.swift @@ -1,4 +1,4 @@ -import CommonWallet + enum WalletTransactionHistoryDataState { case waitingCached diff --git a/fearless/Modules/NewWallet/WalletTransactionHistory/ViewModel/WalletTransactionHistoryCellViewModel.swift b/fearless/Modules/NewWallet/WalletTransactionHistory/ViewModel/WalletTransactionHistoryCellViewModel.swift index 089b6d2d31..9ef34786ed 100644 --- a/fearless/Modules/NewWallet/WalletTransactionHistory/ViewModel/WalletTransactionHistoryCellViewModel.swift +++ b/fearless/Modules/NewWallet/WalletTransactionHistory/ViewModel/WalletTransactionHistoryCellViewModel.swift @@ -1,7 +1,6 @@ import Foundation import SSFUtils import UIKit -import CommonWallet struct WalletTransactionHistoryCellViewModel { let transaction: AssetTransactionData diff --git a/fearless/Modules/NewWallet/WalletTransactionHistory/ViewModel/WalletTransactionHistoryViewModelFactory.swift b/fearless/Modules/NewWallet/WalletTransactionHistory/ViewModel/WalletTransactionHistoryViewModelFactory.swift index e783c18098..ba664d1cf5 100644 --- a/fearless/Modules/NewWallet/WalletTransactionHistory/ViewModel/WalletTransactionHistoryViewModelFactory.swift +++ b/fearless/Modules/NewWallet/WalletTransactionHistory/ViewModel/WalletTransactionHistoryViewModelFactory.swift @@ -1,4 +1,4 @@ -import CommonWallet + import RobinHood import SSFUtils import UIKit diff --git a/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryDependencyContainer.swift b/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryDependencyContainer.swift index 276d0857aa..9059fe8eb8 100644 --- a/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryDependencyContainer.swift +++ b/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryDependencyContainer.swift @@ -1,5 +1,5 @@ import RobinHood -import CommonWallet + import SSFModels import SoraFoundation @@ -30,7 +30,6 @@ final class WalletTransactionHistoryDependencyContainer { } let dataProviderFactory = HistoryDataProviderFactory( - cacheFacade: SubstrateDataStorageFacade.shared, operationFactory: operationFactory ) @@ -46,7 +45,6 @@ final class WalletTransactionHistoryDependencyContainer { asset: chainAsset.asset, chain: chainAsset.chain, targetIdentifier: "wallet.transaction.history.\(address).\(chainAsset.chainAssetId)", - using: .main, filters: filters ) } diff --git a/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryInteractor.swift b/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryInteractor.swift index 93be6252ce..968ce15d24 100644 --- a/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryInteractor.swift +++ b/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryInteractor.swift @@ -1,5 +1,5 @@ import UIKit -import CommonWallet + import RobinHood import SoraFoundation import SSFModels diff --git a/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryPresenter.swift b/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryPresenter.swift index da04cb763f..d5b47459aa 100644 --- a/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryPresenter.swift +++ b/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryPresenter.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SoraFoundation import SSFModels diff --git a/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryProtocols.swift b/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryProtocols.swift index bc82519b43..3ee4e1343f 100644 --- a/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryProtocols.swift +++ b/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryProtocols.swift @@ -1,4 +1,4 @@ -import CommonWallet + import SSFModels protocol WalletTransactionHistoryViewProtocol: ControllerBackedProtocol, Draggable, LoadableViewProtocol { diff --git a/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryViewController.swift b/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryViewController.swift index cbf8bc1373..fa16e9ecdc 100644 --- a/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryViewController.swift +++ b/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryViewController.swift @@ -1,5 +1,5 @@ import UIKit -import CommonWallet + import RobinHood import SoraUI import SoraFoundation diff --git a/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryViewFactory.swift b/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryViewFactory.swift index 3ac27f5eb4..510736dd1d 100644 --- a/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryViewFactory.swift +++ b/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryViewFactory.swift @@ -1,6 +1,5 @@ import Foundation import SSFUtils -import CommonWallet import RobinHood import SoraFoundation import SSFModels @@ -99,7 +98,6 @@ enum WalletTransactionHistoryViewFactory { } let dataProviderFactory = HistoryDataProviderFactory( - cacheFacade: SubstrateDataStorageFacade.shared, operationFactory: operationFactory ) diff --git a/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryWireframe.swift b/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryWireframe.swift index c31a128250..894534c199 100644 --- a/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryWireframe.swift +++ b/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryWireframe.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet +import UIKit import SSFModels final class WalletTransactionHistoryWireframe: WalletTransactionHistoryWireframeProtocol { diff --git a/fearless/Modules/OnbordingMain/OnboardingMainInteractor.swift b/fearless/Modules/OnbordingMain/OnboardingMainInteractor.swift index 07acb910a7..b3fe806a48 100644 --- a/fearless/Modules/OnbordingMain/OnboardingMainInteractor.swift +++ b/fearless/Modules/OnbordingMain/OnboardingMainInteractor.swift @@ -6,13 +6,13 @@ final class OnboardingMainInteractor { weak var presenter: OnboardingMainInteractorOutputProtocol? private let keystoreImportService: KeystoreImportServiceProtocol - private let cloudStorage: FearlessCompatibilityProtocol + private let cloudStorage: CloudStorageServiceProtocol private let featureToggleService: FeatureToggleProviderProtocol private let operationQueue: OperationQueue init( keystoreImportService: KeystoreImportServiceProtocol, - cloudStorage: FearlessCompatibilityProtocol, + cloudStorage: CloudStorageServiceProtocol, featureToggleService: FeatureToggleProviderProtocol, operationQueue: OperationQueue ) { @@ -54,7 +54,7 @@ extension OnboardingMainInteractor: OnboardingMainInteractorInputProtocol { Task { do { cloudStorage.disconnect() - let accounts = try await cloudStorage.getFearlessBackupAccounts() + let accounts = try await cloudStorage.getBackupAccounts() await MainActor.run { presenter?.didReceiveBackupAccounts(result: .success(accounts)) } diff --git a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentProtocols.swift b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentProtocols.swift index 85d1f7f89c..af936446f4 100644 --- a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentProtocols.swift +++ b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentProtocols.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SSFModels typealias PolkaswapAdjustmentModuleCreationResult = ( diff --git a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewController.swift b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewController.swift index f995a1816e..4edbd66e07 100644 --- a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewController.swift +++ b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewController.swift @@ -1,6 +1,6 @@ import UIKit import SoraFoundation -import CommonWallet + import SnapKit final class PolkaswapAdjustmentViewController: UIViewController, ViewHolder, HiddableBarWhenPushed { diff --git a/fearless/Modules/Purchase/PurchaseProtocols.swift b/fearless/Modules/Purchase/PurchaseProtocols.swift index 808c02c7af..c8dfe9017f 100644 --- a/fearless/Modules/Purchase/PurchaseProtocols.swift +++ b/fearless/Modules/Purchase/PurchaseProtocols.swift @@ -1,5 +1,4 @@ import UIKit -import CommonWallet protocol PurchaseViewProtocol: ControllerBackedProtocol {} @@ -21,7 +20,6 @@ protocol PurchaseWireframeProtocol: AnyObject { protocol PurchaseViewFactoryProtocol: AnyObject { static func createView( - for action: PurchaseAction, - commandFactory: WalletCommandFactoryProtocol + for action: PurchaseAction ) -> PurchaseViewProtocol? } diff --git a/fearless/Modules/Purchase/PurchaseViewFactory.swift b/fearless/Modules/Purchase/PurchaseViewFactory.swift index d0af5ef08c..4fc9a590d5 100644 --- a/fearless/Modules/Purchase/PurchaseViewFactory.swift +++ b/fearless/Modules/Purchase/PurchaseViewFactory.swift @@ -1,29 +1,7 @@ import Foundation import SoraFoundation -import CommonWallet final class PurchaseViewFactory: PurchaseViewFactoryProtocol { - static func createView( - for action: PurchaseAction, - commandFactory _: WalletCommandFactoryProtocol - ) -> PurchaseViewProtocol? { - let view = PurchaseViewController(url: action.url) - - let presenter = PurchasePresenter(action: action) - let interactor = PurchaseInteractor(eventCenter: EventCenter.shared) - let wireframe = PurchaseWireframe( - localizationManager: LocalizationManager.shared - ) - - view.presenter = presenter - presenter.view = view - presenter.interactor = interactor - presenter.wireframe = wireframe - interactor.presenter = presenter - - return view - } - static func createView( for action: PurchaseAction ) -> PurchaseViewProtocol? { diff --git a/fearless/Modules/Purchase/PurchaseWireframe.swift b/fearless/Modules/Purchase/PurchaseWireframe.swift index b2432210f1..d8ef6356cd 100644 --- a/fearless/Modules/Purchase/PurchaseWireframe.swift +++ b/fearless/Modules/Purchase/PurchaseWireframe.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SoraFoundation final class PurchaseWireframe: PurchaseWireframeProtocol { diff --git a/fearless/Modules/ReceiveAndRequestAsset/ReceiveAndRequestAssetPresenter.swift b/fearless/Modules/ReceiveAndRequestAsset/ReceiveAndRequestAssetPresenter.swift index d13f217df1..f2ef04ba87 100644 --- a/fearless/Modules/ReceiveAndRequestAsset/ReceiveAndRequestAssetPresenter.swift +++ b/fearless/Modules/ReceiveAndRequestAsset/ReceiveAndRequestAssetPresenter.swift @@ -2,6 +2,7 @@ import Foundation import SoraFoundation import SSFModels import SSFUtils +import SSFQRService protocol ReceiveAndRequestAssetViewInput: ControllerBackedProtocol { func didReceive(viewModel: ReceiveAssetViewModel) @@ -149,7 +150,7 @@ final class ReceiveAndRequestAssetPresenter { inputAmount = input.stringWithPointSeparator } let addressInfo = SoraQRInfo( - prefix: SubstrateQR.prefix, + prefix: SubstrateQRConstants.prefix, address: address, rawPublicKey: account.publicKey, username: wallet.name, diff --git a/fearless/Modules/ScanQR/ScanQRInteractor.swift b/fearless/Modules/ScanQR/ScanQRInteractor.swift index e6a5196f9e..86e957f4c5 100644 --- a/fearless/Modules/ScanQR/ScanQRInteractor.swift +++ b/fearless/Modules/ScanQR/ScanQRInteractor.swift @@ -1,5 +1,4 @@ import UIKit -import CommonWallet final class ScanQRInteractor { // MARK: - Private properties diff --git a/fearless/Modules/ScanQR/ScanQRPresenter.swift b/fearless/Modules/ScanQR/ScanQRPresenter.swift index 66c306832a..91b9073fc6 100644 --- a/fearless/Modules/ScanQR/ScanQRPresenter.swift +++ b/fearless/Modules/ScanQR/ScanQRPresenter.swift @@ -1,6 +1,6 @@ import Foundation import SoraFoundation -import CommonWallet + import RobinHood import AVFoundation import SSFUtils diff --git a/fearless/Modules/SelectCurrency/SelectCurrencyInteractor.swift b/fearless/Modules/SelectCurrency/SelectCurrencyInteractor.swift index f88c964528..721b76b0f3 100644 --- a/fearless/Modules/SelectCurrency/SelectCurrencyInteractor.swift +++ b/fearless/Modules/SelectCurrency/SelectCurrencyInteractor.swift @@ -33,7 +33,7 @@ final class SelectCurrencyInteractor { fiatInfoProvider = nil guard let fiatUrl = ApplicationConfig.shared.fiatsURL else { return } - fiatInfoProvider = jsonDataProviderFactory.getJson(for: fiatUrl) + fiatInfoProvider = try? jsonDataProviderFactory.getJson(for: fiatUrl) let updateClosure: ([DataProviderChange<[Currency]>]) -> Void = { [weak self] changes in if let result = changes.reduceToLastChange() { diff --git a/fearless/Modules/Send/SendProtocols.swift b/fearless/Modules/Send/SendProtocols.swift index abf60682db..062f8e576e 100644 --- a/fearless/Modules/Send/SendProtocols.swift +++ b/fearless/Modules/Send/SendProtocols.swift @@ -1,4 +1,4 @@ -import CommonWallet +import Foundation import BigInt import SSFModels diff --git a/fearless/Modules/Send/SendViewController.swift b/fearless/Modules/Send/SendViewController.swift index 9a9ce8420b..6fcc1e3c07 100644 --- a/fearless/Modules/Send/SendViewController.swift +++ b/fearless/Modules/Send/SendViewController.swift @@ -1,6 +1,6 @@ import UIKit import SoraFoundation -import CommonWallet + import SnapKit final class SendViewController: UIViewController, ViewHolder { diff --git a/fearless/Modules/Staking/Analytics/Stake/AnalyticsStakeInteractor.swift b/fearless/Modules/Staking/Analytics/Stake/AnalyticsStakeInteractor.swift index 314ae5b999..ac6e58173b 100644 --- a/fearless/Modules/Staking/Analytics/Stake/AnalyticsStakeInteractor.swift +++ b/fearless/Modules/Staking/Analytics/Stake/AnalyticsStakeInteractor.swift @@ -1,6 +1,7 @@ import RobinHood import BigInt import SSFModels +import Foundation final class AnalyticsStakeInteractor { weak var presenter: AnalyticsStakeInteractorOutputProtocol! diff --git a/fearless/Modules/Staking/ControllerAccount/ControllerAccountViewFactory.swift b/fearless/Modules/Staking/ControllerAccount/ControllerAccountViewFactory.swift index ae73ee5940..3e532a462f 100644 --- a/fearless/Modules/Staking/ControllerAccount/ControllerAccountViewFactory.swift +++ b/fearless/Modules/Staking/ControllerAccount/ControllerAccountViewFactory.swift @@ -4,6 +4,7 @@ import SoraKeystore import SSFUtils import RobinHood import SSFModels +import SSFAccountManagmentStorage struct ControllerAccountViewFactory { static func createView( diff --git a/fearless/Modules/Staking/ControllerAccountConfirmation/ControllerAccountConfirmationViewFactory.swift b/fearless/Modules/Staking/ControllerAccountConfirmation/ControllerAccountConfirmationViewFactory.swift index 1719724fc8..48da762b73 100644 --- a/fearless/Modules/Staking/ControllerAccountConfirmation/ControllerAccountConfirmationViewFactory.swift +++ b/fearless/Modules/Staking/ControllerAccountConfirmation/ControllerAccountConfirmationViewFactory.swift @@ -4,6 +4,7 @@ import SSFUtils import SoraKeystore import RobinHood import SSFModels +import SSFAccountManagmentStorage struct ControllerAccountConfirmationViewFactory { static func createView( diff --git a/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/Model/SelectValidatorsConfirmationModel.swift b/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/Model/SelectValidatorsConfirmationModel.swift index 1c684bf8c1..ea3b866541 100644 --- a/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/Model/SelectValidatorsConfirmationModel.swift +++ b/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/Model/SelectValidatorsConfirmationModel.swift @@ -1,5 +1,4 @@ import Foundation -import CommonWallet struct SelectValidatorsConfirmRelaychainModel { let wallet: DisplayAddress diff --git a/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/SelectValidatorsConfirmPresenter.swift b/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/SelectValidatorsConfirmPresenter.swift index a5de074d9e..c09c8c8058 100644 --- a/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/SelectValidatorsConfirmPresenter.swift +++ b/fearless/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/SelectValidatorsConfirmPresenter.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import BigInt import SSFModels diff --git a/fearless/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/ViewModel/SelectedValidatorListViewModel.swift b/fearless/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/ViewModel/SelectedValidatorListViewModel.swift index 0c46a352a5..f2c2b1ecca 100644 --- a/fearless/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/ViewModel/SelectedValidatorListViewModel.swift +++ b/fearless/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/ViewModel/SelectedValidatorListViewModel.swift @@ -1,4 +1,5 @@ import SSFUtils +import Foundation struct SelectedValidatorCellViewModel { let icon: DrawableIcon? diff --git a/fearless/Modules/Staking/SelectValidatorsFlow/ValidatorListFilter/ValidatorListFilterPresenter.swift b/fearless/Modules/Staking/SelectValidatorsFlow/ValidatorListFilter/ValidatorListFilterPresenter.swift index 0e793d42c1..242b49ffe1 100644 --- a/fearless/Modules/Staking/SelectValidatorsFlow/ValidatorListFilter/ValidatorListFilterPresenter.swift +++ b/fearless/Modules/Staking/SelectValidatorsFlow/ValidatorListFilter/ValidatorListFilterPresenter.swift @@ -1,5 +1,5 @@ import SoraFoundation -import CommonWallet + import SSFModels final class ValidatorListFilterPresenter { diff --git a/fearless/Modules/Staking/SelectValidatorsFlow/ValidatorSearch/ViewModel/ValidatorSearchViewModel.swift b/fearless/Modules/Staking/SelectValidatorsFlow/ValidatorSearch/ViewModel/ValidatorSearchViewModel.swift index ff5f712c0f..597d93b8df 100644 --- a/fearless/Modules/Staking/SelectValidatorsFlow/ValidatorSearch/ViewModel/ValidatorSearchViewModel.swift +++ b/fearless/Modules/Staking/SelectValidatorsFlow/ValidatorSearch/ViewModel/ValidatorSearchViewModel.swift @@ -1,4 +1,5 @@ import SSFUtils +import Foundation struct ValidatorSearchCellViewModel { let icon: DrawableIcon? diff --git a/fearless/Modules/Staking/SelectValidatorsFlow/YourValidatorList/View/YourValidatorTableCell.swift b/fearless/Modules/Staking/SelectValidatorsFlow/YourValidatorList/View/YourValidatorTableCell.swift index f5dea82957..e859646494 100644 --- a/fearless/Modules/Staking/SelectValidatorsFlow/YourValidatorList/View/YourValidatorTableCell.swift +++ b/fearless/Modules/Staking/SelectValidatorsFlow/YourValidatorList/View/YourValidatorTableCell.swift @@ -1,5 +1,6 @@ import Foundation import SSFUtils +import UIKit class YourValidatorTableCell: UITableViewCell { private enum LayoutConstants { diff --git a/fearless/Modules/Staking/SelectValidatorsFlow/YourValidatorList/YourValidatorListViewFactory.swift b/fearless/Modules/Staking/SelectValidatorsFlow/YourValidatorList/YourValidatorListViewFactory.swift index ae3106b614..1a61540213 100644 --- a/fearless/Modules/Staking/SelectValidatorsFlow/YourValidatorList/YourValidatorListViewFactory.swift +++ b/fearless/Modules/Staking/SelectValidatorsFlow/YourValidatorList/YourValidatorListViewFactory.swift @@ -4,6 +4,7 @@ import RobinHood import SoraKeystore import SSFUtils import SSFModels +import SSFAccountManagmentStorage struct YourValidatorListViewFactory { static func createView( diff --git a/fearless/Modules/Staking/StakingAmount/Flow/Relaychain/StakingAmountRelaychainViewModelFactory.swift b/fearless/Modules/Staking/StakingAmount/Flow/Relaychain/StakingAmountRelaychainViewModelFactory.swift index 7659c29d7f..30e9916182 100644 --- a/fearless/Modules/Staking/StakingAmount/Flow/Relaychain/StakingAmountRelaychainViewModelFactory.swift +++ b/fearless/Modules/Staking/StakingAmount/Flow/Relaychain/StakingAmountRelaychainViewModelFactory.swift @@ -1,6 +1,6 @@ import Foundation import SoraFoundation -import CommonWallet + import SSFModels final class StakingAmountRelaychainViewModelFactory: StakingAmountViewModelFactoryProtocol { diff --git a/fearless/Modules/Staking/StakingAmount/Flow/Relaychain/StakingAmountRelaychainViewModelState.swift b/fearless/Modules/Staking/StakingAmount/Flow/Relaychain/StakingAmountRelaychainViewModelState.swift index 92ea8cc184..83d50c9d90 100644 --- a/fearless/Modules/Staking/StakingAmount/Flow/Relaychain/StakingAmountRelaychainViewModelState.swift +++ b/fearless/Modules/Staking/StakingAmount/Flow/Relaychain/StakingAmountRelaychainViewModelState.swift @@ -1,6 +1,6 @@ import Foundation import BigInt -import CommonWallet + import SoraFoundation import SSFModels diff --git a/fearless/Modules/Staking/StakingAmount/Flow/StakingAmountFlow.swift b/fearless/Modules/Staking/StakingAmount/Flow/StakingAmountFlow.swift index f99163fbab..d12112860f 100644 --- a/fearless/Modules/Staking/StakingAmount/Flow/StakingAmountFlow.swift +++ b/fearless/Modules/Staking/StakingAmount/Flow/StakingAmountFlow.swift @@ -1,4 +1,4 @@ -import CommonWallet + import Foundation import RobinHood import SoraFoundation diff --git a/fearless/Modules/Staking/StakingAmount/StakingAmountPresenter.swift b/fearless/Modules/Staking/StakingAmount/StakingAmountPresenter.swift index 66257c29a8..794b84fc58 100644 --- a/fearless/Modules/Staking/StakingAmount/StakingAmountPresenter.swift +++ b/fearless/Modules/Staking/StakingAmount/StakingAmountPresenter.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import BigInt import SSFModels diff --git a/fearless/Modules/Staking/StakingAmount/StakingAmountProtocols.swift b/fearless/Modules/Staking/StakingAmount/StakingAmountProtocols.swift index 44213e3776..ec2b1346a6 100644 --- a/fearless/Modules/Staking/StakingAmount/StakingAmountProtocols.swift +++ b/fearless/Modules/Staking/StakingAmount/StakingAmountProtocols.swift @@ -1,7 +1,7 @@ import Foundation import SoraFoundation import BigInt -import CommonWallet + import SSFModels protocol StakingAmountViewProtocol: ControllerBackedProtocol, Localizable, LoadableViewProtocol { diff --git a/fearless/Modules/Staking/StakingAmount/StakingAmountViewController.swift b/fearless/Modules/Staking/StakingAmount/StakingAmountViewController.swift index 676824d11c..3ff7a476dc 100644 --- a/fearless/Modules/Staking/StakingAmount/StakingAmountViewController.swift +++ b/fearless/Modules/Staking/StakingAmount/StakingAmountViewController.swift @@ -2,7 +2,6 @@ import UIKit import SoraFoundation import SoraUI import SSFUtils -import CommonWallet final class StakingAmountViewController: UIViewController, AdaptiveDesignable, LoadableViewProtocol { var presenter: StakingAmountPresenterProtocol? diff --git a/fearless/Modules/Staking/StakingAmount/StakingAmountViewFactory.swift b/fearless/Modules/Staking/StakingAmount/StakingAmountViewFactory.swift index 26a2b85f82..d19b211ebc 100644 --- a/fearless/Modules/Staking/StakingAmount/StakingAmountViewFactory.swift +++ b/fearless/Modules/Staking/StakingAmount/StakingAmountViewFactory.swift @@ -4,6 +4,7 @@ import RobinHood import SoraFoundation import SSFUtils import SSFModels +import SSFAccountManagmentStorage final class StakingAmountViewFactory: StakingAmountViewFactoryProtocol { static func createView( diff --git a/fearless/Modules/Staking/StakingAmount/ViewModel/RewardDestinationViewModelFactory.swift b/fearless/Modules/Staking/StakingAmount/ViewModel/RewardDestinationViewModelFactory.swift index 42e7eec69e..9afb393a13 100644 --- a/fearless/Modules/Staking/StakingAmount/ViewModel/RewardDestinationViewModelFactory.swift +++ b/fearless/Modules/Staking/StakingAmount/ViewModel/RewardDestinationViewModelFactory.swift @@ -1,7 +1,7 @@ import Foundation import SoraFoundation import SSFUtils -import CommonWallet + import SSFModels protocol RewardDestinationViewModelFactoryProtocol { diff --git a/fearless/Modules/Staking/StakingAmount/ViewModel/StakingAmountViewModel.swift b/fearless/Modules/Staking/StakingAmount/ViewModel/StakingAmountViewModel.swift index a2a27ff89c..5cb1056740 100644 --- a/fearless/Modules/Staking/StakingAmount/ViewModel/StakingAmountViewModel.swift +++ b/fearless/Modules/Staking/StakingAmount/ViewModel/StakingAmountViewModel.swift @@ -1,6 +1,5 @@ import Foundation import SoraFoundation -import CommonWallet struct StakingAmountMainViewModel { let assetViewModel: LocalizableResource? diff --git a/fearless/Modules/Staking/StakingBalance/StakingBalanceViewFactory.swift b/fearless/Modules/Staking/StakingBalance/StakingBalanceViewFactory.swift index 085fc4b0c9..791fae0948 100644 --- a/fearless/Modules/Staking/StakingBalance/StakingBalanceViewFactory.swift +++ b/fearless/Modules/Staking/StakingBalance/StakingBalanceViewFactory.swift @@ -3,6 +3,7 @@ import SoraKeystore import RobinHood import SSFUtils import SSFModels +import SSFAccountManagmentStorage struct StakingBalanceViewFactory { static func createView( diff --git a/fearless/Modules/Staking/StakingBondMore/StakingBondMorePresenter.swift b/fearless/Modules/Staking/StakingBondMore/StakingBondMorePresenter.swift index 16a2c13be5..c4e7703447 100644 --- a/fearless/Modules/Staking/StakingBondMore/StakingBondMorePresenter.swift +++ b/fearless/Modules/Staking/StakingBondMore/StakingBondMorePresenter.swift @@ -1,5 +1,5 @@ import SoraFoundation -import CommonWallet + import BigInt import SSFModels diff --git a/fearless/Modules/Staking/StakingBondMore/StakingBondMoreProtocols.swift b/fearless/Modules/Staking/StakingBondMore/StakingBondMoreProtocols.swift index 03f31d27d9..61da0ed024 100644 --- a/fearless/Modules/Staking/StakingBondMore/StakingBondMoreProtocols.swift +++ b/fearless/Modules/Staking/StakingBondMore/StakingBondMoreProtocols.swift @@ -1,5 +1,5 @@ import SoraFoundation -import CommonWallet + import BigInt import SSFModels diff --git a/fearless/Modules/Staking/StakingBondMore/StakingBondMoreViewController.swift b/fearless/Modules/Staking/StakingBondMore/StakingBondMoreViewController.swift index d4d3a83e4b..4844fdad5c 100644 --- a/fearless/Modules/Staking/StakingBondMore/StakingBondMoreViewController.swift +++ b/fearless/Modules/Staking/StakingBondMore/StakingBondMoreViewController.swift @@ -1,6 +1,5 @@ import UIKit import SoraFoundation -import CommonWallet final class StakingBondMoreViewController: UIViewController, ViewHolder, HiddableBarWhenPushed { typealias RootViewType = StakingBondMoreViewLayout diff --git a/fearless/Modules/Staking/StakingBondMore/StakingBondMoreViewFactory.swift b/fearless/Modules/Staking/StakingBondMore/StakingBondMoreViewFactory.swift index 95726db647..6b58aa13d5 100644 --- a/fearless/Modules/Staking/StakingBondMore/StakingBondMoreViewFactory.swift +++ b/fearless/Modules/Staking/StakingBondMore/StakingBondMoreViewFactory.swift @@ -2,8 +2,8 @@ import SoraFoundation import SoraKeystore import RobinHood import SSFUtils -import CommonWallet import SSFModels +import SSFAccountManagmentStorage struct StakingBondMoreViewFactory { static func createView( diff --git a/fearless/Modules/Staking/StakingBondMoreConfirmation/Flow/Parachain/StakingBondMoreConfirmParachainViewModelFactory.swift b/fearless/Modules/Staking/StakingBondMoreConfirmation/Flow/Parachain/StakingBondMoreConfirmParachainViewModelFactory.swift index 57a202ae53..b07d597fa1 100644 --- a/fearless/Modules/Staking/StakingBondMoreConfirmation/Flow/Parachain/StakingBondMoreConfirmParachainViewModelFactory.swift +++ b/fearless/Modules/Staking/StakingBondMoreConfirmation/Flow/Parachain/StakingBondMoreConfirmParachainViewModelFactory.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SoraFoundation import SSFUtils import SSFModels diff --git a/fearless/Modules/Staking/StakingBondMoreConfirmation/Flow/Pool/StakingBondMoreConfirmationPoolViewModelFactory.swift b/fearless/Modules/Staking/StakingBondMoreConfirmation/Flow/Pool/StakingBondMoreConfirmationPoolViewModelFactory.swift index 0fb010e44b..400c9cd993 100644 --- a/fearless/Modules/Staking/StakingBondMoreConfirmation/Flow/Pool/StakingBondMoreConfirmationPoolViewModelFactory.swift +++ b/fearless/Modules/Staking/StakingBondMoreConfirmation/Flow/Pool/StakingBondMoreConfirmationPoolViewModelFactory.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SoraFoundation import SSFUtils import SSFModels diff --git a/fearless/Modules/Staking/StakingBondMoreConfirmation/Flow/Relaychain/StakingBondMoreConfirmRelaychainViewModelFactory.swift b/fearless/Modules/Staking/StakingBondMoreConfirmation/Flow/Relaychain/StakingBondMoreConfirmRelaychainViewModelFactory.swift index b230c890e0..3f514cf459 100644 --- a/fearless/Modules/Staking/StakingBondMoreConfirmation/Flow/Relaychain/StakingBondMoreConfirmRelaychainViewModelFactory.swift +++ b/fearless/Modules/Staking/StakingBondMoreConfirmation/Flow/Relaychain/StakingBondMoreConfirmRelaychainViewModelFactory.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SoraFoundation import SSFUtils import SSFModels diff --git a/fearless/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationProtocols.swift b/fearless/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationProtocols.swift index b393b4b90c..e2fde95682 100644 --- a/fearless/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationProtocols.swift +++ b/fearless/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationProtocols.swift @@ -1,5 +1,5 @@ import SoraFoundation -import CommonWallet + import BigInt import SSFModels diff --git a/fearless/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationViewFactory.swift b/fearless/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationViewFactory.swift index c1166ea407..6b16936423 100644 --- a/fearless/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationViewFactory.swift +++ b/fearless/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationViewFactory.swift @@ -4,6 +4,7 @@ import SoraKeystore import RobinHood import SSFModels import SSFUtils +import SSFAccountManagmentStorage struct StakingBondMoreConfirmViewFactory { static func createView( diff --git a/fearless/Modules/Staking/StakingMain/StakingMainInteractor+Subscription.swift b/fearless/Modules/Staking/StakingMain/StakingMainInteractor+Subscription.swift index 213b9c2bee..86278f1d0a 100644 --- a/fearless/Modules/Staking/StakingMain/StakingMainInteractor+Subscription.swift +++ b/fearless/Modules/Staking/StakingMain/StakingMainInteractor+Subscription.swift @@ -1,7 +1,7 @@ import Foundation import RobinHood import BigInt -import CommonWallet + import SSFUtils import SSFModels @@ -155,7 +155,7 @@ extension StakingMainInteractor { if let analyticsURL = selectedChainAsset?.chain.externalApi?.staking?.url, selectedChainAsset?.stakingType?.isParachain == true, let chainAsset = selectedChainAsset { - rewardAnalyticsProvider = subscribeWeaklyRewardAnalytics(chainAsset: chainAsset, for: address, url: analyticsURL) + rewardAnalyticsProvider = try? subscribeWeaklyRewardAnalytics(chainAsset: chainAsset, for: address, url: analyticsURL) } else { presenter?.didReceieve( subqueryRewards: .success(nil), diff --git a/fearless/Modules/Staking/StakingMain/StakingMainPresenter.swift b/fearless/Modules/Staking/StakingMain/StakingMainPresenter.swift index 3964f9c4fd..26e0fd9f26 100644 --- a/fearless/Modules/Staking/StakingMain/StakingMainPresenter.swift +++ b/fearless/Modules/Staking/StakingMain/StakingMainPresenter.swift @@ -1,6 +1,6 @@ // swiftlint:disable file_length import Foundation -import CommonWallet + import BigInt import SwiftUI import SoraFoundation diff --git a/fearless/Modules/Staking/StakingMain/StakingMainProtocols.swift b/fearless/Modules/Staking/StakingMain/StakingMainProtocols.swift index e3a00bb110..267ca8ba80 100644 --- a/fearless/Modules/Staking/StakingMain/StakingMainProtocols.swift +++ b/fearless/Modules/Staking/StakingMain/StakingMainProtocols.swift @@ -1,6 +1,6 @@ import Foundation import SoraFoundation -import CommonWallet + import BigInt import SSFModels diff --git a/fearless/Modules/Staking/StakingMain/StakingMainViewController.swift b/fearless/Modules/Staking/StakingMain/StakingMainViewController.swift index 5ff10c123c..9587c4051b 100644 --- a/fearless/Modules/Staking/StakingMain/StakingMainViewController.swift +++ b/fearless/Modules/Staking/StakingMain/StakingMainViewController.swift @@ -2,7 +2,7 @@ import UIKit import SSFUtils import SoraFoundation import SoraUI -import CommonWallet + import SnapKit final class StakingMainViewController: UIViewController, AdaptiveDesignable { diff --git a/fearless/Modules/Staking/StakingMain/View/RewardEstimationView.swift b/fearless/Modules/Staking/StakingMain/View/RewardEstimationView.swift index c2cf3c9554..b76aaa5982 100644 --- a/fearless/Modules/Staking/StakingMain/View/RewardEstimationView.swift +++ b/fearless/Modules/Staking/StakingMain/View/RewardEstimationView.swift @@ -1,5 +1,5 @@ import UIKit -import CommonWallet + import SoraFoundation import SoraUI diff --git a/fearless/Modules/Staking/StakingMain/ViewModel/StakingEstimationViewModel.swift b/fearless/Modules/Staking/StakingMain/ViewModel/StakingEstimationViewModel.swift index 74f26e6f58..3cf35d8d70 100644 --- a/fearless/Modules/Staking/StakingMain/ViewModel/StakingEstimationViewModel.swift +++ b/fearless/Modules/Staking/StakingMain/ViewModel/StakingEstimationViewModel.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SoraFoundation import SSFModels diff --git a/fearless/Modules/Staking/StakingMain/ViewModel/StakingStateViewModelFactory.swift b/fearless/Modules/Staking/StakingMain/ViewModel/StakingStateViewModelFactory.swift index 6cafdab057..309eaa4e52 100644 --- a/fearless/Modules/Staking/StakingMain/ViewModel/StakingStateViewModelFactory.swift +++ b/fearless/Modules/Staking/StakingMain/ViewModel/StakingStateViewModelFactory.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SoraFoundation import BigInt import IrohaCrypto diff --git a/fearless/Modules/Staking/StakingPayoutConfirmation/StakingPayoutConfirmationInteractor.swift b/fearless/Modules/Staking/StakingPayoutConfirmation/StakingPayoutConfirmationInteractor.swift index 3b4aa9f934..ffbf25cb7f 100644 --- a/fearless/Modules/Staking/StakingPayoutConfirmation/StakingPayoutConfirmationInteractor.swift +++ b/fearless/Modules/Staking/StakingPayoutConfirmation/StakingPayoutConfirmationInteractor.swift @@ -1,6 +1,6 @@ import Foundation import SoraKeystore -import CommonWallet + import RobinHood import IrohaCrypto import BigInt diff --git a/fearless/Modules/Staking/StakingPayoutConfirmation/StakingPayoutConfirmationPresenter.swift b/fearless/Modules/Staking/StakingPayoutConfirmation/StakingPayoutConfirmationPresenter.swift index ea7048a6a5..40a433e3e1 100644 --- a/fearless/Modules/Staking/StakingPayoutConfirmation/StakingPayoutConfirmationPresenter.swift +++ b/fearless/Modules/Staking/StakingPayoutConfirmation/StakingPayoutConfirmationPresenter.swift @@ -1,6 +1,6 @@ import Foundation import BigInt -import CommonWallet + import SSFModels final class StakingPayoutConfirmationPresenter { diff --git a/fearless/Modules/Staking/StakingPayoutConfirmation/StakingPayoutConfirmationViewFactory.swift b/fearless/Modules/Staking/StakingPayoutConfirmation/StakingPayoutConfirmationViewFactory.swift index 7d9ce7f858..cea61aea0e 100644 --- a/fearless/Modules/Staking/StakingPayoutConfirmation/StakingPayoutConfirmationViewFactory.swift +++ b/fearless/Modules/Staking/StakingPayoutConfirmation/StakingPayoutConfirmationViewFactory.swift @@ -4,6 +4,7 @@ import SoraKeystore import SSFUtils import RobinHood import SSFModels +import SSFAccountManagmentStorage final class StakingPayoutConfirmationViewFactory: StakingPayoutConfirmationViewFactoryProtocol { static func createView( diff --git a/fearless/Modules/Staking/StakingRebondConfirmation/StakingRebondConfirmationViewFactory.swift b/fearless/Modules/Staking/StakingRebondConfirmation/StakingRebondConfirmationViewFactory.swift index b3af091ba5..7c913f3b4d 100644 --- a/fearless/Modules/Staking/StakingRebondConfirmation/StakingRebondConfirmationViewFactory.swift +++ b/fearless/Modules/Staking/StakingRebondConfirmation/StakingRebondConfirmationViewFactory.swift @@ -4,6 +4,7 @@ import SoraKeystore import RobinHood import SSFUtils import SSFModels +import SSFAccountManagmentStorage struct StakingRebondConfirmationViewFactory { static func createView( diff --git a/fearless/Modules/Staking/StakingRebondSetup/StakingRebondSetupProtocols.swift b/fearless/Modules/Staking/StakingRebondSetup/StakingRebondSetupProtocols.swift index 3f4851e4db..93ce03220d 100644 --- a/fearless/Modules/Staking/StakingRebondSetup/StakingRebondSetupProtocols.swift +++ b/fearless/Modules/Staking/StakingRebondSetup/StakingRebondSetupProtocols.swift @@ -1,6 +1,6 @@ import Foundation import SoraFoundation -import CommonWallet + import SSFModels protocol StakingRebondSetupViewProtocol: ControllerBackedProtocol, Localizable { diff --git a/fearless/Modules/Staking/StakingRebondSetup/StakingRebondSetupViewController.swift b/fearless/Modules/Staking/StakingRebondSetup/StakingRebondSetupViewController.swift index de8cd451ed..c56a0e5789 100644 --- a/fearless/Modules/Staking/StakingRebondSetup/StakingRebondSetupViewController.swift +++ b/fearless/Modules/Staking/StakingRebondSetup/StakingRebondSetupViewController.swift @@ -1,6 +1,5 @@ import UIKit import SoraFoundation -import CommonWallet final class StakingRebondSetupViewController: UIViewController, ViewHolder { typealias RootViewType = StakingRebondSetupLayout diff --git a/fearless/Modules/Staking/StakingRebondSetup/StakingRebondSetupViewFactory.swift b/fearless/Modules/Staking/StakingRebondSetup/StakingRebondSetupViewFactory.swift index c04743776f..35b56dc563 100644 --- a/fearless/Modules/Staking/StakingRebondSetup/StakingRebondSetupViewFactory.swift +++ b/fearless/Modules/Staking/StakingRebondSetup/StakingRebondSetupViewFactory.swift @@ -3,6 +3,7 @@ import SoraFoundation import SoraKeystore import RobinHood import SSFModels +import SSFAccountManagmentStorage final class StakingRebondSetupViewFactory: StakingRebondSetupViewFactoryProtocol { static func createView( diff --git a/fearless/Modules/Staking/StakingRedeem/StakingRedeemViewFactory.swift b/fearless/Modules/Staking/StakingRedeem/StakingRedeemViewFactory.swift index 021aac00df..5f0d9a6136 100644 --- a/fearless/Modules/Staking/StakingRedeem/StakingRedeemViewFactory.swift +++ b/fearless/Modules/Staking/StakingRedeem/StakingRedeemViewFactory.swift @@ -4,6 +4,7 @@ import SoraKeystore import RobinHood import SSFUtils import SSFModels +import SSFAccountManagmentStorage final class StakingRedeemViewFactory: StakingRedeemViewFactoryProtocol { static func createView( diff --git a/fearless/Modules/Staking/StakingRedeemConfirmation/StakingRedeemConfirmationViewFactory.swift b/fearless/Modules/Staking/StakingRedeemConfirmation/StakingRedeemConfirmationViewFactory.swift index 35d38448cc..4f1189cafb 100644 --- a/fearless/Modules/Staking/StakingRedeemConfirmation/StakingRedeemConfirmationViewFactory.swift +++ b/fearless/Modules/Staking/StakingRedeemConfirmation/StakingRedeemConfirmationViewFactory.swift @@ -4,6 +4,7 @@ import SoraKeystore import RobinHood import SSFUtils import SSFModels +import SSFAccountManagmentStorage final class StakingRedeemConfirmationViewFactory: StakingRedeemConfirmationViewFactoryProtocol { static func createView( diff --git a/fearless/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmViewFactory.swift b/fearless/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmViewFactory.swift index c416840974..e9408160f6 100644 --- a/fearless/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmViewFactory.swift +++ b/fearless/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmViewFactory.swift @@ -4,6 +4,7 @@ import RobinHood import SoraFoundation import SSFModels import SSFUtils +import SSFAccountManagmentStorage struct StakingRewardDestConfirmViewFactory { static func createView( diff --git a/fearless/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupViewController.swift b/fearless/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupViewController.swift index 78dc02ad6b..50a0b09780 100644 --- a/fearless/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupViewController.swift +++ b/fearless/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupViewController.swift @@ -1,5 +1,5 @@ import UIKit -import CommonWallet + import SSFUtils import SoraFoundation diff --git a/fearless/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupViewFactory.swift b/fearless/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupViewFactory.swift index bd5a55d8b5..74f6849ce3 100644 --- a/fearless/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupViewFactory.swift +++ b/fearless/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupViewFactory.swift @@ -3,6 +3,7 @@ import SoraKeystore import RobinHood import SSFUtils import SSFModels +import SSFAccountManagmentStorage struct StakingRewardDestSetupViewFactory { static func createView( diff --git a/fearless/Modules/Staking/StakingUnbondConfirm/StakingUnbondConfirmViewFactory.swift b/fearless/Modules/Staking/StakingUnbondConfirm/StakingUnbondConfirmViewFactory.swift index 1209ebe1f1..f7f187d587 100644 --- a/fearless/Modules/Staking/StakingUnbondConfirm/StakingUnbondConfirmViewFactory.swift +++ b/fearless/Modules/Staking/StakingUnbondConfirm/StakingUnbondConfirmViewFactory.swift @@ -4,6 +4,7 @@ import SoraKeystore import RobinHood import SSFModels import SSFUtils +import SSFAccountManagmentStorage struct StakingUnbondConfirmViewFactory: StakingUnbondConfirmViewFactoryProtocol { static func createView( diff --git a/fearless/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupProtocols.swift b/fearless/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupProtocols.swift index 2302c95c1f..354f1d2804 100644 --- a/fearless/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupProtocols.swift +++ b/fearless/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupProtocols.swift @@ -1,6 +1,6 @@ import Foundation import SoraFoundation -import CommonWallet + import BigInt import SSFModels diff --git a/fearless/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupViewController.swift b/fearless/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupViewController.swift index 1b6fab0673..b2d66d98cb 100644 --- a/fearless/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupViewController.swift +++ b/fearless/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupViewController.swift @@ -1,6 +1,5 @@ import UIKit import SoraFoundation -import CommonWallet final class StakingUnbondSetupViewController: UIViewController, ViewHolder, HiddableBarWhenPushed { typealias RootViewType = StakingUnbondSetupLayout diff --git a/fearless/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupViewFactory.swift b/fearless/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupViewFactory.swift index e33e2ff118..f1875ee6db 100644 --- a/fearless/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupViewFactory.swift +++ b/fearless/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupViewFactory.swift @@ -4,6 +4,7 @@ import SoraKeystore import RobinHood import SSFUtils import SSFModels +import SSFAccountManagmentStorage struct StakingUnbondSetupViewFactory: StakingUnbondSetupViewFactoryProtocol { static func createView( diff --git a/fearless/Modules/Staking/View/StakingRewardCalculatorView.swift b/fearless/Modules/Staking/View/StakingRewardCalculatorView.swift index 87a30b2aef..ff0773493b 100644 --- a/fearless/Modules/Staking/View/StakingRewardCalculatorView.swift +++ b/fearless/Modules/Staking/View/StakingRewardCalculatorView.swift @@ -1,6 +1,5 @@ import UIKit import SoraUI -import CommonWallet protocol RewardCalculatorViewDelegate: AnyObject { func rewardCalculatorView(_ view: StakingRewardCalculatorView, didChange amount: Decimal?) diff --git a/fearless/Modules/StakingPool/Join/StakingPoolJoinConfig/StakingPoolJoinConfigPresenter.swift b/fearless/Modules/StakingPool/Join/StakingPoolJoinConfig/StakingPoolJoinConfigPresenter.swift index ea2f7e34b1..6adfa14611 100644 --- a/fearless/Modules/StakingPool/Join/StakingPoolJoinConfig/StakingPoolJoinConfigPresenter.swift +++ b/fearless/Modules/StakingPool/Join/StakingPoolJoinConfig/StakingPoolJoinConfigPresenter.swift @@ -1,6 +1,6 @@ import Foundation import SoraFoundation -import CommonWallet + import BigInt import SSFModels diff --git a/fearless/Modules/StakingPool/Join/StakingPoolJoinConfig/StakingPoolJoinConfigProtocols.swift b/fearless/Modules/StakingPool/Join/StakingPoolJoinConfig/StakingPoolJoinConfigProtocols.swift index 666c0127ca..cdbf9e7c32 100644 --- a/fearless/Modules/StakingPool/Join/StakingPoolJoinConfig/StakingPoolJoinConfigProtocols.swift +++ b/fearless/Modules/StakingPool/Join/StakingPoolJoinConfig/StakingPoolJoinConfigProtocols.swift @@ -1,4 +1,4 @@ -import CommonWallet +import Foundation import BigInt import SSFModels diff --git a/fearless/Modules/StakingPool/Join/StakingPoolJoinConfig/StakingPoolJoinConfigViewController.swift b/fearless/Modules/StakingPool/Join/StakingPoolJoinConfig/StakingPoolJoinConfigViewController.swift index 755f6a5102..39a0b923c2 100644 --- a/fearless/Modules/StakingPool/Join/StakingPoolJoinConfig/StakingPoolJoinConfigViewController.swift +++ b/fearless/Modules/StakingPool/Join/StakingPoolJoinConfig/StakingPoolJoinConfigViewController.swift @@ -1,6 +1,5 @@ import UIKit import SoraFoundation -import CommonWallet final class StakingPoolJoinConfigViewController: UIViewController, ViewHolder, HiddableBarWhenPushed { typealias RootViewType = StakingPoolJoinConfigViewLayout diff --git a/fearless/Modules/StakingPool/PoolRolesConfirm/PoolRolesConfirmAssembly.swift b/fearless/Modules/StakingPool/PoolRolesConfirm/PoolRolesConfirmAssembly.swift index fb4276133b..707c095049 100644 --- a/fearless/Modules/StakingPool/PoolRolesConfirm/PoolRolesConfirmAssembly.swift +++ b/fearless/Modules/StakingPool/PoolRolesConfirm/PoolRolesConfirmAssembly.swift @@ -3,6 +3,7 @@ import SoraFoundation import SoraKeystore import RobinHood import SSFModels +import SSFAccountManagmentStorage final class PoolRolesConfirmAssembly { static func configureModule( diff --git a/fearless/Modules/StakingPool/StakingPoolCreate/StakingPoolCreateProtocols.swift b/fearless/Modules/StakingPool/StakingPoolCreate/StakingPoolCreateProtocols.swift index 78ce6b5004..a87de78109 100644 --- a/fearless/Modules/StakingPool/StakingPoolCreate/StakingPoolCreateProtocols.swift +++ b/fearless/Modules/StakingPool/StakingPoolCreate/StakingPoolCreateProtocols.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import BigInt import SoraFoundation import SSFModels diff --git a/fearless/Modules/StakingPool/StakingPoolCreate/StakingPoolCreateViewController.swift b/fearless/Modules/StakingPool/StakingPoolCreate/StakingPoolCreateViewController.swift index eb176fa082..71ccf09e54 100644 --- a/fearless/Modules/StakingPool/StakingPoolCreate/StakingPoolCreateViewController.swift +++ b/fearless/Modules/StakingPool/StakingPoolCreate/StakingPoolCreateViewController.swift @@ -1,6 +1,6 @@ import UIKit import SoraFoundation -import CommonWallet + import SoraUI import SnapKit diff --git a/fearless/Modules/SwapTransactionDetail/SwapTransactionDetailAssembly.swift b/fearless/Modules/SwapTransactionDetail/SwapTransactionDetailAssembly.swift index 983bf6aa8d..468efa7032 100644 --- a/fearless/Modules/SwapTransactionDetail/SwapTransactionDetailAssembly.swift +++ b/fearless/Modules/SwapTransactionDetail/SwapTransactionDetailAssembly.swift @@ -1,6 +1,6 @@ import UIKit import SoraFoundation -import CommonWallet + import SSFModels final class SwapTransactionDetailAssembly { diff --git a/fearless/Modules/SwapTransactionDetail/SwapTransactionDetailPresenter.swift b/fearless/Modules/SwapTransactionDetail/SwapTransactionDetailPresenter.swift index 647f8fc245..83dfb6ac3f 100644 --- a/fearless/Modules/SwapTransactionDetail/SwapTransactionDetailPresenter.swift +++ b/fearless/Modules/SwapTransactionDetail/SwapTransactionDetailPresenter.swift @@ -1,6 +1,6 @@ import Foundation import SoraFoundation -import CommonWallet + import SSFModels final class SwapTransactionDetailPresenter { diff --git a/fearless/Modules/SwapTransactionDetail/viewModel/SwapTransactionViewModelFactory.swift b/fearless/Modules/SwapTransactionDetail/viewModel/SwapTransactionViewModelFactory.swift index 641649a0fa..2859f95fad 100644 --- a/fearless/Modules/SwapTransactionDetail/viewModel/SwapTransactionViewModelFactory.swift +++ b/fearless/Modules/SwapTransactionDetail/viewModel/SwapTransactionViewModelFactory.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet +import UIKit import SSFModels protocol SwapTransactionViewModelFactoryProtocol { diff --git a/fearless/Modules/SwitchAccount/SwitchAccount+AccountCreateWireframe.swift b/fearless/Modules/SwitchAccount/SwitchAccount+AccountCreateWireframe.swift index 18119f5ceb..efce735d73 100644 --- a/fearless/Modules/SwitchAccount/SwitchAccount+AccountCreateWireframe.swift +++ b/fearless/Modules/SwitchAccount/SwitchAccount+AccountCreateWireframe.swift @@ -1,4 +1,5 @@ import Foundation +import SSFModels extension SwitchAccount { final class AccountCreateWireframe: AccountCreateWireframeProtocol { diff --git a/fearless/Modules/SwitchAccount/SwitchAccount+AccountImportWireframe.swift b/fearless/Modules/SwitchAccount/SwitchAccount+AccountImportWireframe.swift index 7259e6cec9..007bd33198 100644 --- a/fearless/Modules/SwitchAccount/SwitchAccount+AccountImportWireframe.swift +++ b/fearless/Modules/SwitchAccount/SwitchAccount+AccountImportWireframe.swift @@ -1,4 +1,5 @@ import Foundation +import SSFModels extension SwitchAccount { final class AccountImportWireframe: AccountImportWireframeProtocol { diff --git a/fearless/Modules/Wallet/AccountList/TransferMetadataContext.swift b/fearless/Modules/Wallet/AccountList/TransferMetadataContext.swift deleted file mode 100644 index caa94ad88b..0000000000 --- a/fearless/Modules/Wallet/AccountList/TransferMetadataContext.swift +++ /dev/null @@ -1,38 +0,0 @@ -import Foundation - -struct TransferMetadataContext { - static let receiverBalanceKey = "transfer.metadata.receiver.balance.key" - static let priceKey = "transfer.metadata.price.key" - - let receiverBalance: Decimal - let price: Decimal -} - -extension TransferMetadataContext { - private static func decimalFromContext(_ context: [String: String], key: String) -> Decimal { - guard let stringValue = context[key] else { return .zero } - return Decimal(string: stringValue) ?? .zero - } - - init(data: AccountData, precision: Int16, price: Decimal) { - let free = Decimal - .fromSubstrateAmount(data.free, precision: precision) ?? .zero - let reserved = Decimal - .fromSubstrateAmount(data.reserved, precision: precision) ?? .zero - - receiverBalance = free + reserved - self.price = price - } - - init(context: [String: String]) { - receiverBalance = Self.decimalFromContext(context, key: Self.receiverBalanceKey) - price = Self.decimalFromContext(context, key: Self.priceKey) - } - - func toContext() -> [String: String] { - [ - Self.receiverBalanceKey: receiverBalance.stringWithPointSeparator, - Self.priceKey: price.stringWithPointSeparator - ] - } -} diff --git a/fearless/Modules/Wallet/AccountList/View/AssetStyleFactory.swift b/fearless/Modules/Wallet/AccountList/View/AssetStyleFactory.swift deleted file mode 100644 index d13788e7b0..0000000000 --- a/fearless/Modules/Wallet/AccountList/View/AssetStyleFactory.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Foundation -import CommonWallet - -struct AssetStyleFactory: AssetCellStyleFactoryProtocol { - func createCellStyle(for _: WalletAsset) -> AssetCellStyle { - let shadow = WalletShadowStyle( - offset: CGSize(width: 0.0, height: 0.0), - color: UIColor.black, - opacity: 0.0, - blurRadius: 0.0 - ) - - let textColor = R.color.colorWhite()! - let subtitleColor = UIColor(white: 136.0 / 255.0, alpha: 1.0) - let headerFont = R.font.soraRc0040417SemiBold(size: 18)! - let regularFont = R.font.soraRc0040417Regular(size: 14)! - - let cardStyle = CardAssetStyle( - backgroundColor: UIColor.black.withAlphaComponent(0.7), - leftFillColor: UIColor.black.withAlphaComponent(0.0), - symbol: WalletTextStyle(font: headerFont, color: R.color.colorWhite()!), - title: WalletTextStyle(font: headerFont, color: textColor), - subtitle: WalletTextStyle(font: regularFont, color: subtitleColor), - accessory: WalletTextStyle(font: regularFont, color: textColor), - shadow: shadow, - cornerRadius: 10.0 - ) - - return .card(cardStyle) - } -} diff --git a/fearless/Modules/Wallet/AccountList/View/WalletActionsCell.swift b/fearless/Modules/Wallet/AccountList/View/WalletActionsCell.swift deleted file mode 100644 index e9881689b6..0000000000 --- a/fearless/Modules/Wallet/AccountList/View/WalletActionsCell.swift +++ /dev/null @@ -1,59 +0,0 @@ -import Foundation -import SoraUI -import CommonWallet - -final class WalletActionsCell: UICollectionViewCell { - @IBOutlet private var sendButton: RoundedButton! - @IBOutlet private var receiveButton: RoundedButton! - @IBOutlet private var buyButton: RoundedButton! - - private(set) var actionsViewModel: WalletActionsViewModelProtocol? - - override func prepareForReuse() { - super.prepareForReuse() - - actionsViewModel = nil - } - - @IBAction private func actionSend() { - if let actionsViewModel = actionsViewModel { - try? actionsViewModel.send.command.execute() - } - } - - @IBAction private func actionReceive() { - if let actionsViewModel = actionsViewModel { - try? actionsViewModel.receive.command.execute() - } - } - - @IBAction private func actionBuy() { - if let command = actionsViewModel?.buy.command { - try? command.execute() - } - } -} - -extension WalletActionsCell: WalletViewProtocol { - var viewModel: WalletViewModelProtocol? { - actionsViewModel - } - - func bind(viewModel: WalletViewModelProtocol) { - if let actionsViewModel = viewModel as? WalletActionsViewModelProtocol { - self.actionsViewModel = actionsViewModel - - sendButton.imageWithTitleView?.title = actionsViewModel.send.title - receiveButton.imageWithTitleView?.title = actionsViewModel.receive.title - buyButton.imageWithTitleView?.title = actionsViewModel.buy.title - - let enabled = (actionsViewModel.buy.command != nil) - buyButton.isUserInteractionEnabled = enabled - buyButton.alpha = enabled ? 1.0 : 0.2 - - sendButton.invalidateLayout() - receiveButton.invalidateLayout() - buyButton.invalidateLayout() - } - } -} diff --git a/fearless/Modules/Wallet/AccountList/View/WalletActionsCell.xib b/fearless/Modules/Wallet/AccountList/View/WalletActionsCell.xib deleted file mode 100644 index 4ee955f18f..0000000000 --- a/fearless/Modules/Wallet/AccountList/View/WalletActionsCell.xib +++ /dev/null @@ -1,254 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/fearless/Modules/Wallet/AccountList/View/WalletAssetCell.swift b/fearless/Modules/Wallet/AccountList/View/WalletAssetCell.swift deleted file mode 100644 index 903671e826..0000000000 --- a/fearless/Modules/Wallet/AccountList/View/WalletAssetCell.swift +++ /dev/null @@ -1,53 +0,0 @@ -import UIKit -import CommonWallet - -final class WalletAssetCell: UICollectionViewCell { - private enum Constants { - static let contentInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 8.0, right: 16.0) - } - - @IBOutlet private var iconView: UIImageView! - @IBOutlet private var platformLabel: UILabel! - @IBOutlet private var symbolLabel: UILabel! - @IBOutlet private var balanceLabel: UILabel! - @IBOutlet private var priceLabel: UILabel! - @IBOutlet private var changeLabel: UILabel! - @IBOutlet private var totalPriceLabel: UILabel! - - var viewModel: WalletViewModelProtocol? - - override func prepareForReuse() { - super.prepareForReuse() - - (viewModel as? WalletAssetViewModel)?.imageViewModel?.cancel() - } -} - -extension WalletAssetCell: WalletViewProtocol { - func bind(viewModel: WalletViewModelProtocol) { - if let assetViewModel = viewModel as? WalletAssetViewModel { - self.viewModel = viewModel - - platformLabel.text = assetViewModel.platform - symbolLabel.text = assetViewModel.symbol - balanceLabel.text = assetViewModel.amount - priceLabel.text = assetViewModel.details - totalPriceLabel.text = assetViewModel.accessoryDetails - - switch assetViewModel.priceChangeViewModel { - case let .goingUp(displayString): - changeLabel.text = displayString - changeLabel.textColor = R.color.colorGreen() - case let .goingDown(displayString): - changeLabel.text = displayString - changeLabel.textColor = R.color.colorRed() - } - - iconView.image = nil - - assetViewModel.imageViewModel?.loadImage { [weak self] image, _ in - self?.iconView.image = image - } - } - } -} diff --git a/fearless/Modules/Wallet/AccountList/View/WalletAssetCell.xib b/fearless/Modules/Wallet/AccountList/View/WalletAssetCell.xib deleted file mode 100644 index 242b6001b7..0000000000 --- a/fearless/Modules/Wallet/AccountList/View/WalletAssetCell.xib +++ /dev/null @@ -1,159 +0,0 @@ - - - - - - - - - - - - - - sora-rc004-0417-Bold - - - sora-rc004-0417-Regular - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/fearless/Modules/Wallet/AccountList/View/WalletTotalPriceCell.swift b/fearless/Modules/Wallet/AccountList/View/WalletTotalPriceCell.swift deleted file mode 100644 index 18d1569881..0000000000 --- a/fearless/Modules/Wallet/AccountList/View/WalletTotalPriceCell.swift +++ /dev/null @@ -1,38 +0,0 @@ -import UIKit -import SoraUI -import CommonWallet - -final class WalletTotalPriceCell: UICollectionViewCell { - @IBOutlet private var titleLabel: UILabel! - @IBOutlet private var priceLabel: UILabel! - @IBOutlet private var accountButton: RoundedButton! - - var viewModel: WalletViewModelProtocol? - - override func prepareForReuse() { - super.prepareForReuse() - - (viewModel as? WalletTotalPriceViewModel)?.imageViewModel?.cancel() - } - - @IBAction private func actionAccount() { - try? (viewModel as? WalletTotalPriceViewModel)?.accountCommand?.execute() - } -} - -extension WalletTotalPriceCell: WalletViewProtocol { - func bind(viewModel: WalletViewModelProtocol) { - if let totalPriceViewModel = viewModel as? WalletTotalPriceViewModel { - self.viewModel = totalPriceViewModel - - titleLabel.text = totalPriceViewModel.details - priceLabel.text = totalPriceViewModel.amount - - accountButton.imageWithTitleView?.iconImage = nil - - totalPriceViewModel.imageViewModel?.loadImage { [weak self] image, _ in - self?.accountButton.imageWithTitleView?.iconImage = image - } - } - } -} diff --git a/fearless/Modules/Wallet/AccountList/View/WalletTotalPriceCell.xib b/fearless/Modules/Wallet/AccountList/View/WalletTotalPriceCell.xib deleted file mode 100644 index 213b105121..0000000000 --- a/fearless/Modules/Wallet/AccountList/View/WalletTotalPriceCell.xib +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - - - - - - - - - sora-rc004-0417-Bold - - - sora-rc004-0417-Regular - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/fearless/Modules/Wallet/AccountList/ViewModel/WalletActionsViewModel.swift b/fearless/Modules/Wallet/AccountList/ViewModel/WalletActionsViewModel.swift deleted file mode 100644 index ee66c8630e..0000000000 --- a/fearless/Modules/Wallet/AccountList/ViewModel/WalletActionsViewModel.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Foundation -import CommonWallet - -protocol WalletDisablingActionProtocol { - var title: String { get } - var command: WalletCommandProtocol? { get } -} - -protocol WalletActionsViewModelProtocol: ActionsViewModelProtocol { - var buy: WalletDisablingActionProtocol { get } -} - -final class WalletActionViewModel: ActionViewModelProtocol { - var title: String - var command: WalletCommandProtocol - var style: WalletTextStyleProtocol { WalletTextStyle(font: .capsTitle, color: .black) } - - init(title: String, command: WalletCommandProtocol) { - self.title = title - self.command = command - } -} - -final class WalletDisablingAction: WalletDisablingActionProtocol { - let title: String - let command: WalletCommandProtocol? - - init(title: String, command: WalletCommandProtocol?) { - self.title = title - self.command = command - } -} - -final class WalletActionsViewModel: WalletActionsViewModelProtocol { - var cellReuseIdentifier: String { WalletAccountListConstants.actionsCellId } - var itemHeight: CGFloat { WalletAccountListConstants.actionsCellHeight } - var command: WalletCommandProtocol? { nil } - - let send: ActionViewModelProtocol - let receive: ActionViewModelProtocol - let buy: WalletDisablingActionProtocol - - init( - send: ActionViewModelProtocol, - receive: ActionViewModelProtocol, - buy: WalletDisablingActionProtocol - ) { - self.send = send - self.receive = receive - self.buy = buy - } -} diff --git a/fearless/Modules/Wallet/AccountList/ViewModel/WalletAssetViewModel.swift b/fearless/Modules/Wallet/AccountList/ViewModel/WalletAssetViewModel.swift deleted file mode 100644 index 6cf38f3891..0000000000 --- a/fearless/Modules/Wallet/AccountList/ViewModel/WalletAssetViewModel.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Foundation -import CommonWallet - -enum WalletPriceChangeViewModel { - case goingUp(displayValue: String) - case goingDown(displayValue: String) -} - -final class WalletAssetViewModel: AssetViewModelProtocol { - var itemHeight: CGFloat { WalletAccountListConstants.assetCellHeight } - var cellReuseIdentifier: String { WalletAccountListConstants.assetCellId } - let assetId: String - let amount: String - let symbol: String? - let details: String - let accessoryDetails: String? - let imageViewModel: WalletImageViewModelProtocol? - let style: AssetCellStyle - let command: WalletCommandProtocol? - let priceChangeViewModel: WalletPriceChangeViewModel - - let platform: String - - init( - assetId: String, - amount: String, - symbol: String?, - accessoryDetails: String?, - imageViewModel: WalletImageViewModelProtocol?, - style: AssetCellStyle, - platform: String, - details: String, - priceChangeViewModel: WalletPriceChangeViewModel, - command: WalletCommandProtocol? - ) { - self.assetId = assetId - self.amount = amount - self.symbol = symbol - self.accessoryDetails = accessoryDetails - self.imageViewModel = imageViewModel - self.style = style - self.platform = platform - self.details = details - self.priceChangeViewModel = priceChangeViewModel - self.command = command - } -} diff --git a/fearless/Modules/Wallet/AccountList/ViewModel/WalletTotalPriceViewModel.swift b/fearless/Modules/Wallet/AccountList/ViewModel/WalletTotalPriceViewModel.swift deleted file mode 100644 index 72872cacb0..0000000000 --- a/fearless/Modules/Wallet/AccountList/ViewModel/WalletTotalPriceViewModel.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Foundation -import CommonWallet - -final class WalletTotalPriceViewModel: AssetViewModelProtocol { - var cellReuseIdentifier: String { WalletAccountListConstants.totalPriceCellId } - var itemHeight: CGFloat { WalletAccountListConstants.totalPriceHeight } - - let assetId: String - let details: String - let amount: String - let symbol: String? = nil - let accessoryDetails: String? = nil - let imageViewModel: WalletImageViewModelProtocol? - - let style: AssetCellStyle - - let command: WalletCommandProtocol? - let accountCommand: WalletCommandProtocol? - - init( - assetId: String, - details: String, - amount: String, - imageViewModel: WalletImageViewModelProtocol?, - style: AssetCellStyle, - command: WalletCommandProtocol?, - accountCommand: WalletCommandProtocol? - ) { - self.assetId = assetId - self.details = details - self.amount = amount - self.command = command - self.imageViewModel = imageViewModel - self.accountCommand = accountCommand - self.style = style - } -} diff --git a/fearless/Modules/Wallet/AccountList/WalletAccountListConstants.swift b/fearless/Modules/Wallet/AccountList/WalletAccountListConstants.swift deleted file mode 100644 index 54a149ad49..0000000000 --- a/fearless/Modules/Wallet/AccountList/WalletAccountListConstants.swift +++ /dev/null @@ -1,12 +0,0 @@ -import UIKit - -struct WalletAccountListConstants { - static let totalPriceCellId: String = "fearlessTotalPriceCellId" - static let totalPriceHeight: CGFloat = 66.0 - - static let assetCellId: String = "fearlessAssetCellId" - static let assetCellHeight: CGFloat = 96.0 - - static let actionsCellId: String = "fearlessActionsCellId" - static let actionsCellHeight: CGFloat = 90 -} diff --git a/fearless/Modules/Wallet/Commands/ExistentialDepositInfoCommand.swift b/fearless/Modules/Wallet/Commands/ExistentialDepositInfoCommand.swift index 4b0cb4108f..164a9db680 100644 --- a/fearless/Modules/Wallet/Commands/ExistentialDepositInfoCommand.swift +++ b/fearless/Modules/Wallet/Commands/ExistentialDepositInfoCommand.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SoraFoundation struct TransferExistentialState { diff --git a/fearless/Modules/Wallet/Commands/TransferConfirmCommand.swift b/fearless/Modules/Wallet/Commands/TransferConfirmCommand.swift index 544c55c46c..e3dd4a6a5b 100644 --- a/fearless/Modules/Wallet/Commands/TransferConfirmCommand.swift +++ b/fearless/Modules/Wallet/Commands/TransferConfirmCommand.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SoraFoundation protocol WalletCommandDecoratorDelegateProtocol { diff --git a/fearless/Modules/Wallet/Commands/TransferConfirmCommandProxy.swift b/fearless/Modules/Wallet/Commands/TransferConfirmCommandProxy.swift index d7be154323..385ab792eb 100644 --- a/fearless/Modules/Wallet/Commands/TransferConfirmCommandProxy.swift +++ b/fearless/Modules/Wallet/Commands/TransferConfirmCommandProxy.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SoraFoundation import RobinHood import CoreData diff --git a/fearless/Modules/Wallet/Commands/WalletBuyCommand.swift b/fearless/Modules/Wallet/Commands/WalletBuyCommand.swift index a98fc40cd6..c515165014 100644 --- a/fearless/Modules/Wallet/Commands/WalletBuyCommand.swift +++ b/fearless/Modules/Wallet/Commands/WalletBuyCommand.swift @@ -1,5 +1,4 @@ import Foundation -import CommonWallet final class WalletBuyCommand: WalletCommandProtocol { let action: PurchaseAction diff --git a/fearless/Modules/Wallet/Commands/WalletCopyCommand.swift b/fearless/Modules/Wallet/Commands/WalletCopyCommand.swift index 3740fc626c..29a318a481 100644 --- a/fearless/Modules/Wallet/Commands/WalletCopyCommand.swift +++ b/fearless/Modules/Wallet/Commands/WalletCopyCommand.swift @@ -1,5 +1,4 @@ import Foundation -import CommonWallet final class WalletCopyCommand: WalletCommandProtocol { weak var commandFactory: WalletCommandFactoryProtocol? diff --git a/fearless/Modules/Wallet/Commands/WalletSelectAccountCommand.swift b/fearless/Modules/Wallet/Commands/WalletSelectAccountCommand.swift index d456946a83..1168eb2fa1 100644 --- a/fearless/Modules/Wallet/Commands/WalletSelectAccountCommand.swift +++ b/fearless/Modules/Wallet/Commands/WalletSelectAccountCommand.swift @@ -1,5 +1,4 @@ import Foundation -import CommonWallet final class WalletSelectAccountCommand: WalletCommandProtocol { weak var commandFactory: WalletCommandFactoryProtocol? diff --git a/fearless/Modules/Wallet/Commands/WalletSelectAccountCommandFactory.swift b/fearless/Modules/Wallet/Commands/WalletSelectAccountCommandFactory.swift index b6075ea4a9..37f23921d1 100644 --- a/fearless/Modules/Wallet/Commands/WalletSelectAccountCommandFactory.swift +++ b/fearless/Modules/Wallet/Commands/WalletSelectAccountCommandFactory.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import SoraFoundation import SoraKeystore diff --git a/fearless/Modules/Wallet/Commands/WalletSelectPurchaseProviderCommand.swift b/fearless/Modules/Wallet/Commands/WalletSelectPurchaseProviderCommand.swift index 3cf070fc2a..cab2d5b0b4 100644 --- a/fearless/Modules/Wallet/Commands/WalletSelectPurchaseProviderCommand.swift +++ b/fearless/Modules/Wallet/Commands/WalletSelectPurchaseProviderCommand.swift @@ -1,5 +1,4 @@ import Foundation -import CommonWallet final class WalletSelectPurchaseProviderCommand: WalletCommandProtocol { let actions: [PurchaseAction] diff --git a/fearless/Modules/Wallet/Contacts/ContactViewModel.swift b/fearless/Modules/Wallet/Contacts/ContactViewModel.swift deleted file mode 100644 index 8dd7be23b8..0000000000 --- a/fearless/Modules/Wallet/Contacts/ContactViewModel.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Foundation -import CommonWallet -import RobinHood -import SoraFoundation - -final class ContactViewModel: ContactsLocalSearchResultProtocol { - var command: WalletCommandProtocol? - - var cellReuseIdentifier: String { ContactsConstants.contactCellIdentifier } - var itemHeight: CGFloat { ContactsConstants.contactCellHeight } - - let firstName: String - let lastName: String - let accountId: String - let image: UIImage? - let name: String - - init( - firstName: String, - lastName: String, - accountId: String, - image: UIImage?, - name: String - ) { - self.firstName = firstName - self.lastName = lastName - self.accountId = accountId - self.image = image - self.name = name - } -} diff --git a/fearless/Modules/Wallet/Contacts/ContactsConfigurator.swift b/fearless/Modules/Wallet/Contacts/ContactsConfigurator.swift deleted file mode 100644 index 3956fcfe07..0000000000 --- a/fearless/Modules/Wallet/Contacts/ContactsConfigurator.swift +++ /dev/null @@ -1,102 +0,0 @@ -import Foundation -import IrohaCrypto -import CommonWallet -import SoraFoundation -import SSFUtils - -final class ContactsConfigurator { - private var localSearchEngine: ContactsLocalSearchEngine - - private lazy var contactsViewStyle: ContactsViewStyleProtocol = { - let searchTextStyle = WalletTextStyle( - font: UIFont.p1Paragraph, - color: R.color.colorWhite()! - ) - let searchPlaceholderStyle = WalletTextStyle( - font: UIFont.p1Paragraph, - color: R.color.colorLightGray()! - ) - - let searchStroke = WalletStrokeStyle( - color: R.color.colorGray()!, - lineWidth: 1.0 - ) - let searchFieldStyle = WalletRoundedViewStyle( - fill: .clear, - cornerRadius: 8.0, - stroke: searchStroke - ) - - return ContactsViewStyle( - backgroundColor: R.color.colorBlack()!, - searchHeaderBackgroundColor: R.color.colorBlack()!, - searchTextStyle: searchTextStyle, - searchPlaceholderStyle: searchPlaceholderStyle, - searchFieldStyle: searchFieldStyle, - searchIndicatorStyle: R.color.colorGray()!, - searchIcon: R.image.iconSearch(), - searchSeparatorColor: .clear, - tableSeparatorColor: R.color.colorDarkGray()!, - actionsSeparator: WalletStrokeStyle(color: .clear, lineWidth: 0.0) - ) - }() - - private lazy var contactCellStyle: ContactCellStyleProtocol = { - let iconStyle = WalletNameIconStyle( - background: R.color.colorWhite()!, - title: WalletTextStyle(font: UIFont.p1Paragraph, color: .black), - radius: 12.0 - ) - return ContactCellStyle( - title: WalletTextStyle(font: UIFont.p1Paragraph, color: R.color.colorWhite()!), - nameIcon: iconStyle, - accessoryIcon: R.image.iconSmallArrow(), - lineBreakMode: .byTruncatingMiddle, - selectionColor: R.color.colorPink()!.withAlphaComponent(0.3) - ) - }() - - private lazy var sectionHeaderStyle: ContactsSectionStyleProtocol = { - let title = WalletTextStyle( - font: UIFont.capsTitle, - color: R.color.colorLightGray()! - ) - return ContactsSectionStyle( - title: title, - uppercased: true, - height: 30.0, - displaysSeparatorForLastCell: false - ) - }() - - init(networkType: SNAddressType) { - let viewModelFactory = ContactsViewModelFactory(dataStorageFacade: SubstrateDataStorageFacade.shared, iconGenerator: UniversalIconGenerator()) - localSearchEngine = ContactsLocalSearchEngine( - addressPrefix: UInt16(networkType.rawValue), - contactViewModelFactory: viewModelFactory - ) - } - - func configure(builder: ContactsModuleBuilderProtocol) { - let listViewModelFactory = ContactsListViewModelFactory() - - let searchPlaceholder = LocalizableResource { locale in - R.string.localizable - .walletContactsSearchPlaceholder_v110(preferredLanguages: locale.rLanguages) - } - - builder - .with(cellClass: ContactTableViewCell.self, for: ContactsConstants.contactCellIdentifier) - .with(localSearchEngine: localSearchEngine) - .with(listViewModelFactory: listViewModelFactory) - .with(canFindItself: false) - .with(supportsLiveSearch: true) - .with(searchEmptyStateDataSource: WalletEmptyStateDataSource.search) - .with(contactsEmptyStateDataSource: WalletEmptyStateDataSource.contacts) - .with(viewStyle: contactsViewStyle) - .with(contactCellStyle: contactCellStyle) - .with(sectionHeaderStyle: sectionHeaderStyle) - .with(searchPlaceholder: searchPlaceholder) - .with(viewModelFactoryWrapper: localSearchEngine.contactViewModelFactory) - } -} diff --git a/fearless/Modules/Wallet/Contacts/ContactsListViewModelFactory.swift b/fearless/Modules/Wallet/Contacts/ContactsListViewModelFactory.swift deleted file mode 100644 index 0c65667d08..0000000000 --- a/fearless/Modules/Wallet/Contacts/ContactsListViewModelFactory.swift +++ /dev/null @@ -1,101 +0,0 @@ -import Foundation -import CommonWallet -import SSFUtils - -final class ContactsListViewModelFactory: ContactsListViewModelFactoryProtocol { - private var itemViewModelFactory = - ContactsViewModelFactory(dataStorageFacade: SubstrateDataStorageFacade.shared, iconGenerator: UniversalIconGenerator()) - - func createContactViewModelListFromItems( - _ items: [SearchData], - parameters: ContactModuleParameters, - locale: Locale, - delegate: ContactViewModelDelegate?, - commandFactory: WalletCommandFactoryProtocol - ) -> [ContactSectionViewModelProtocol] { - let (localItems, remoteItems) = items.reduce(([SearchData](), [SearchData]())) { result, item in - let context = ContactContext(context: item.context ?? [:]) - - switch context.destination { - case .local: - return (result.0 + [item], result.1) - case .remote: - return (result.0, result.1 + [item]) - } - } - - var sections = [ContactSectionViewModelProtocol]() - - if !localItems.isEmpty { - let viewModels = createSearchViewModelListFromItems( - localItems, - parameters: parameters, - locale: locale, - delegate: delegate, - commandFactory: commandFactory - ) - - let sectionTitle = R.string.localizable - .walletSearchAccounts(preferredLanguages: locale.rLanguages) - let section = ContactSectionViewModel( - title: sectionTitle, - items: viewModels - ) - sections.append(section) - } - - if !remoteItems.isEmpty { - let viewModels = createSearchViewModelListFromItems( - remoteItems, - parameters: parameters, - locale: locale, - delegate: delegate, - commandFactory: commandFactory - ) - let sectionTitle = R.string.localizable - .walletSearchContacts(preferredLanguages: locale.rLanguages) - let section = ContactSectionViewModel( - title: sectionTitle, - items: viewModels - ) - sections.append(section) - } - - return sections - } - - func createSearchViewModelListFromItems( - _ items: [SearchData], - parameters: ContactModuleParameters, - locale: Locale, - delegate: ContactViewModelDelegate?, - commandFactory: WalletCommandFactoryProtocol - ) -> [WalletViewModelProtocol] { - items.compactMap { - itemViewModelFactory.createContactViewModelFromContact( - $0, - parameters: parameters, - locale: locale, - delegate: delegate, - commandFactory: commandFactory - ) - } - } - - func createBarActionForAccountId( - _: ContactModuleParameters, - locale _: Locale, - commandFactory: WalletCommandFactoryProtocol - ) -> WalletBarActionViewModelProtocol? { - guard let icon = R.image.iconScanQr() else { - return nil - } - - let command = commandFactory.prepareScanReceiverCommand() - let viewModel = WalletBarActionViewModel( - displayType: .icon(icon), - command: command - ) - return viewModel - } -} diff --git a/fearless/Modules/Wallet/Contacts/ContactsLocalSearchEngine.swift b/fearless/Modules/Wallet/Contacts/ContactsLocalSearchEngine.swift deleted file mode 100644 index 61fe15b971..0000000000 --- a/fearless/Modules/Wallet/Contacts/ContactsLocalSearchEngine.swift +++ /dev/null @@ -1,60 +0,0 @@ -import Foundation -import CommonWallet -import IrohaCrypto -import SSFUtils -import RobinHood -import SoraFoundation - -final class ContactsLocalSearchEngine: ContactsLocalSearchEngineProtocol { - let contactViewModelFactory: ContactsFactoryWrapperProtocol - let addressPrefix: UInt16 - - private lazy var addressFactory = SS58AddressFactory() - - init(addressPrefix: UInt16, contactViewModelFactory: ContactsFactoryWrapperProtocol) { - self.contactViewModelFactory = contactViewModelFactory - self.addressPrefix = addressPrefix - } - - func search( - query: String, - parameters: ContactModuleParameters, - locale: Locale, - delegate: ContactViewModelDelegate?, - commandFactory: WalletCommandFactoryProtocol - ) -> [ContactViewModelProtocol]? { - do { - let peerId = try addressFactory.accountId( - fromAddress: query, - addressPrefix: addressPrefix - ) - let accountIdData = try Data(hexStringSSF: parameters.accountId) - - guard peerId != accountIdData else { - return [] - } - - let searchData = SearchData( - accountId: peerId.toHex(), - firstName: query, - lastName: "" - ) - - guard let viewModel = contactViewModelFactory - .createContactViewModelFromContact( - searchData, - parameters: parameters, - locale: locale, - delegate: delegate, - commandFactory: commandFactory - ) - else { - return nil - } - - return [viewModel] - } catch { - return nil - } - } -} diff --git a/fearless/Modules/Wallet/Contacts/ContactsViewModelFactory.swift b/fearless/Modules/Wallet/Contacts/ContactsViewModelFactory.swift deleted file mode 100644 index 111701110a..0000000000 --- a/fearless/Modules/Wallet/Contacts/ContactsViewModelFactory.swift +++ /dev/null @@ -1,75 +0,0 @@ -import Foundation -import CommonWallet -import IrohaCrypto -import SSFUtils -import RobinHood - -final class ContactsViewModelFactory: ContactsFactoryWrapperProtocol { - private var iconGenerator: IconGenerating - - var dataStorageFacade: StorageFacadeProtocol - - init( - dataStorageFacade: StorageFacadeProtocol, - iconGenerator: IconGenerating - ) { - self.dataStorageFacade = dataStorageFacade - self.iconGenerator = iconGenerator - } - - func createContactViewModelFromContact( - _ contact: SearchData, - parameters: ContactModuleParameters, - locale: Locale, - delegate: ContactViewModelDelegate?, - commandFactory: WalletCommandFactoryProtocol - ) -> ContactViewModelProtocol? { - do { - guard parameters.accountId != contact.accountId else { - return nil - } - - let icon = try iconGenerator.generateFromAddress(contact.firstName) - .imageWithFillColor( - R.color.colorWhite()!, - size: CGSize(width: 24.0, height: 24.0), - contentScale: UIScreen.main.scale - ) - - let storage: CoreDataRepository = - dataStorageFacade.createRepository() - - let viewModel = ContactViewModel( - firstName: contact.firstName, - lastName: contact.lastName, - accountId: contact.accountId, - image: icon, - name: contact.firstName - ) - - let nextAction = { [weak delegate] in - delegate?.didSelect(contact: viewModel) - return - } - - let cancelAction = { - let hideCommand = commandFactory.prepareHideCommand(with: .pop) - try? hideCommand.execute() - } - - viewModel.command = PhishingCheckExecutor( - commandFactory: commandFactory, - storage: AnyDataProviderRepository(storage), - nextAction: nextAction, - cancelAction: cancelAction, - locale: locale, - publicKey: contact.accountId, - walletAddress: contact.firstName - ) - - return viewModel - } catch { - return nil - } - } -} diff --git a/fearless/Modules/Wallet/Contacts/View/ContactTableViewCell.swift b/fearless/Modules/Wallet/Contacts/View/ContactTableViewCell.swift deleted file mode 100644 index 572e8fcc2f..0000000000 --- a/fearless/Modules/Wallet/Contacts/View/ContactTableViewCell.swift +++ /dev/null @@ -1,134 +0,0 @@ -import UIKit -import CommonWallet - -final class ContactTableViewCell: UITableViewCell { - enum Constants { - static let iconRadius: CGFloat = 12.0 - static let horizontalSpacing: CGFloat = 12.0 - static let contentInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 8.0, right: 16.0) - } - - private var titleLabel = UILabel() - private var subtitleLabel: UILabel? - private var iconImageView = UIImageView() - - var viewModel: WalletViewModelProtocol? - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - backgroundColor = .clear - - let selectedBackgroundView = UIView() - selectedBackgroundView.backgroundColor = R.color.colorPink()!.withAlphaComponent(0.3) - self.selectedBackgroundView = selectedBackgroundView - - setupTitleLabel() - setupImageView() - - accessoryView = UIImageView(image: R.image.iconSmallArrow()) - } - - override func layoutSubviews() { - super.layoutSubviews() - - let bounds = contentView.bounds - let insets = Constants.contentInsets - let iconRadius = Constants.iconRadius - - iconImageView.frame = CGRect( - x: bounds.minX + insets.left, - y: bounds.midY - iconRadius, - width: 2.0 * iconRadius, - height: 2.0 * iconRadius - ) - - let position = iconImageView.frame.maxX + Constants.horizontalSpacing - let width = bounds.width - position - insets.right - - let titleHeight = titleLabel.intrinsicContentSize.height - - if let subtitleLabel = subtitleLabel { - titleLabel.frame = CGRect( - x: position, - y: bounds.minY + insets.top, - width: width, - height: titleHeight - ) - - let subtitleHeight = subtitleLabel.intrinsicContentSize.height - subtitleLabel.frame = CGRect( - x: position, - y: bounds.maxY - insets.bottom - subtitleHeight, - width: width, - height: subtitleHeight - ) - } else { - titleLabel.frame = CGRect( - x: position, - y: bounds.midY - titleHeight / 2.0, - width: width, - height: titleHeight - ) - } - } - - private func setupTitleLabel() { - addSubview(titleLabel) - - titleLabel.textColor = R.color.colorWhite() - titleLabel.font = UIFont.p1Paragraph - titleLabel.lineBreakMode = .byTruncatingMiddle - } - - private func setupSubtitleLabel() { - guard subtitleLabel == nil else { - return - } - - let label = UILabel() - label.textColor = R.color.colorLightGray() - label.font = UIFont.p2Paragraph - label.lineBreakMode = .byTruncatingMiddle - contentView.addSubview(label) - - subtitleLabel = label - } - - private func setupImageView() { - iconImageView.contentMode = .scaleAspectFill - contentView.addSubview(iconImageView) - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -extension ContactTableViewCell: WalletViewProtocol { - func bind(viewModel: WalletViewModelProtocol) { - if let contactsViewModel = viewModel as? ContactViewModelProtocol { - self.viewModel = contactsViewModel - - let title = contactsViewModel.lastName.isEmpty ? - contactsViewModel.firstName : contactsViewModel.lastName - let optionalSubtitle = !contactsViewModel.lastName.isEmpty ? contactsViewModel.firstName : nil - - titleLabel.text = title - - if let subtitle = optionalSubtitle { - setupSubtitleLabel() - - subtitleLabel?.text = subtitle - } else { - subtitleLabel?.removeFromSuperview() - subtitleLabel = nil - } - - imageView?.image = contactsViewModel.image - - setNeedsLayout() - } - } -} diff --git a/fearless/Modules/Wallet/Contacts/View/ContactsConstants.swift b/fearless/Modules/Wallet/Contacts/View/ContactsConstants.swift deleted file mode 100644 index 1b8e6e1154..0000000000 --- a/fearless/Modules/Wallet/Contacts/View/ContactsConstants.swift +++ /dev/null @@ -1,6 +0,0 @@ -import UIKit - -struct ContactsConstants { - static let contactCellIdentifier = "fearlessContactCellId" - static let contactCellHeight: CGFloat = 48.0 -} diff --git a/fearless/Modules/Wallet/History/View/WalletHistoryBackgroundView.swift b/fearless/Modules/Wallet/History/View/WalletHistoryBackgroundView.swift deleted file mode 100644 index 49dddeeecf..0000000000 --- a/fearless/Modules/Wallet/History/View/WalletHistoryBackgroundView.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation -import CommonWallet - -final class WalletHistoryBackgroundView: TriangularedBlurView { - let minimizedSideLength: CGFloat = 10.0 -} - -extension WalletHistoryBackgroundView: HistoryBackgroundViewProtocol { - func apply(style _: HistoryViewStyleProtocol) {} - - func applyFullscreen(progress: CGFloat) { - sideLength = minimizedSideLength * progress - } -} diff --git a/fearless/Modules/Wallet/History/WalletHistoryFilterEditor.swift b/fearless/Modules/Wallet/History/WalletHistoryFilterEditor.swift deleted file mode 100644 index b380826e63..0000000000 --- a/fearless/Modules/Wallet/History/WalletHistoryFilterEditor.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation -import CommonWallet - -final class WalletHistoryFilterEditor: HistoryFilterEditing { - func startEditing( - filter: WalletHistoryRequest, - with _: [WalletAsset], - commandFactory: WalletCommandFactoryProtocol, - notifying delegate: HistoryFilterEditingDelegate? - ) { - guard let view = WalletHistoryFilterViewFactory - .createView(request: filter, commandFactory: commandFactory, delegate: delegate) else { - return - } - - let command = commandFactory.preparePresentationCommand(for: view.controller) - command.presentationStyle = .modal(inNavigation: true) - try? command.execute() - } -} diff --git a/fearless/Modules/Wallet/History/WalletHistoryStyle.swift b/fearless/Modules/Wallet/History/WalletHistoryStyle.swift deleted file mode 100644 index ad2e2d7ebb..0000000000 --- a/fearless/Modules/Wallet/History/WalletHistoryStyle.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation -import CommonWallet - -extension HistoryViewStyle { - static var fearless: HistoryViewStyleProtocol { - let borderStyle = WalletStrokeStyle(color: .clear, lineWidth: 0.0) - let cornerRadius: CGFloat = 10.0 - let titleStyle = WalletTextStyle( - font: .p0Paragraph, - color: R.color.colorWhite()! - ) - - return HistoryViewStyle( - fillColor: UIColor.black.withAlphaComponent(0.7), - borderStyle: borderStyle, - cornerRadius: cornerRadius, - titleStyle: titleStyle, - filterIcon: R.image.iconFilter(), - closeIcon: R.image.iconClose(), - panIndicatorStyle: R.color.colorWhite()!, - shouldInsertFullscreenShadow: false, - shadow: nil, - separatorStyle: nil, - pageLoadingIndicatorColor: R.color.colorTransparentText() - ) - } -} diff --git a/fearless/Modules/Wallet/History/WalletHistoryViewFactoryOverriding.swift b/fearless/Modules/Wallet/History/WalletHistoryViewFactoryOverriding.swift deleted file mode 100644 index cd19110242..0000000000 --- a/fearless/Modules/Wallet/History/WalletHistoryViewFactoryOverriding.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation -import CommonWallet - -final class WalletHistoryViewFactoryOverriding: HistoryViewFactoryOverriding { - func createBackgroundView() -> BaseHistoryBackgroundView? { - let backgroundView = WalletHistoryBackgroundView() - backgroundView.cornerCut = [.topLeft] - backgroundView.blurStyle = .dark - return backgroundView - } -} diff --git a/fearless/Modules/Wallet/HistoryFilter/ViewModel/WalletHistoryFilterViewModel.swift b/fearless/Modules/Wallet/HistoryFilter/ViewModel/WalletHistoryFilterViewModel.swift deleted file mode 100644 index 6df45bf7f7..0000000000 --- a/fearless/Modules/Wallet/HistoryFilter/ViewModel/WalletHistoryFilterViewModel.swift +++ /dev/null @@ -1,48 +0,0 @@ -import Foundation -import SoraFoundation - -enum WalletHistoryFilterRow: Int, CaseIterable { - case transfers - case rewardsAndSlashes - case extrinsics - - var title: LocalizableResource { - switch self { - case .transfers: - return LocalizableResource { locale in - R.string.localizable.walletFiltersTransfers(preferredLanguages: locale.rLanguages) - } - case .rewardsAndSlashes: - return LocalizableResource { locale in - R.string.localizable - .walletFiltersRewardsAndSlashes(preferredLanguages: locale.rLanguages) - } - case .extrinsics: - return LocalizableResource { locale in - R.string.localizable.walletFiltersExtrinsics(preferredLanguages: locale.rLanguages) - } - } - } - - var filter: WalletHistoryFilter { - switch self { - case .transfers: - return .transfers - case .rewardsAndSlashes: - return .rewardsAndSlashes - case .extrinsics: - return .extrinsics - } - } -} - -struct WalletHistoryFilterItemViewModel { - let title: LocalizableResource - let isOn: Bool -} - -struct WalletHistoryFilterViewModel { - let items: [WalletHistoryFilterItemViewModel] - let canApply: Bool - let canReset: Bool -} diff --git a/fearless/Modules/Wallet/HistoryFilter/WalletHistoryFilterPresenter.swift b/fearless/Modules/Wallet/HistoryFilter/WalletHistoryFilterPresenter.swift deleted file mode 100644 index 5a18b01fde..0000000000 --- a/fearless/Modules/Wallet/HistoryFilter/WalletHistoryFilterPresenter.swift +++ /dev/null @@ -1,59 +0,0 @@ -import Foundation -import CommonWallet - -final class WalletHistoryFilterPresenter { - weak var view: WalletHistoryFilterViewProtocol? - var wireframe: WalletHistoryFilterWireframeProtocol! - - let initialFilter: WalletHistoryFilter - - private(set) var currentFilter: WalletHistoryFilter - - init(filter: WalletHistoryFilter) { - initialFilter = filter - currentFilter = filter - } - - private func createViewModel() -> WalletHistoryFilterViewModel { - let items = WalletHistoryFilterRow.allCases.map { row in - WalletHistoryFilterItemViewModel(title: row.title, isOn: currentFilter.contains(row.filter)) - } - - return WalletHistoryFilterViewModel( - items: items, - canApply: currentFilter != initialFilter, - canReset: currentFilter != .all - ) - } -} - -extension WalletHistoryFilterPresenter: WalletHistoryFilterPresenterProtocol { - func setup() { - view?.didReceive(viewModel: createViewModel()) - } - - func toggleFilterItem(at index: Int) { - guard let filter = WalletHistoryFilterRow(rawValue: index)?.filter else { - return - } - - let newFilter = currentFilter.symmetricDifference(filter) - - if newFilter != [] { - currentFilter = newFilter - - view?.didConfirm(viewModel: createViewModel()) - } else { - view?.didReceive(viewModel: createViewModel()) - } - } - - func apply() { - wireframe.proceed(from: view, applying: currentFilter) - } - - func reset() { - currentFilter = .all - view?.didReceive(viewModel: createViewModel()) - } -} diff --git a/fearless/Modules/Wallet/HistoryFilter/WalletHistoryFilterProtocols.swift b/fearless/Modules/Wallet/HistoryFilter/WalletHistoryFilterProtocols.swift deleted file mode 100644 index 1e3d59533e..0000000000 --- a/fearless/Modules/Wallet/HistoryFilter/WalletHistoryFilterProtocols.swift +++ /dev/null @@ -1,25 +0,0 @@ -import CommonWallet - -protocol WalletHistoryFilterViewProtocol: ControllerBackedProtocol { - func didReceive(viewModel: WalletHistoryFilterViewModel) - func didConfirm(viewModel: WalletHistoryFilterViewModel) -} - -protocol WalletHistoryFilterPresenterProtocol: AnyObject { - func setup() - func toggleFilterItem(at index: Int) - func apply() - func reset() -} - -protocol WalletHistoryFilterWireframeProtocol: AnyObject { - func proceed(from view: WalletHistoryFilterViewProtocol?, applying filter: WalletHistoryFilter) -} - -protocol WalletHistoryFilterViewFactoryProtocol: AnyObject { - static func createView( - request: WalletHistoryRequest, - commandFactory: WalletCommandFactoryProtocol, - delegate: HistoryFilterEditingDelegate? - ) -> WalletHistoryFilterViewProtocol? -} diff --git a/fearless/Modules/Wallet/HistoryFilter/WalletHistoryFilterViewController.swift b/fearless/Modules/Wallet/HistoryFilter/WalletHistoryFilterViewController.swift deleted file mode 100644 index c2c20aa055..0000000000 --- a/fearless/Modules/Wallet/HistoryFilter/WalletHistoryFilterViewController.swift +++ /dev/null @@ -1,167 +0,0 @@ -import UIKit -import SoraFoundation - -final class WalletHistoryFilterViewController: UIViewController, ViewHolder { - typealias RootViewType = WalletHistoryFilterViewLayout - - let presenter: WalletHistoryFilterPresenterProtocol - - private var viewModel: WalletHistoryFilterViewModel? - - init( - presenter: WalletHistoryFilterPresenterProtocol, - localizationManager: LocalizationManagerProtocol - ) { - self.presenter = presenter - - super.init(nibName: nil, bundle: nil) - - self.localizationManager = localizationManager - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func loadView() { - view = WalletHistoryFilterViewLayout() - } - - override func viewDidLoad() { - super.viewDidLoad() - - setupNavigationItem() - setupTableView() - setupApplyButton() - setupLocalization() - - presenter.setup() - } - - private func setupApplyButton() { - rootView.applyButton.addTarget(self, action: #selector(actionApply), for: .touchUpInside) - } - - private func setupNavigationItem() { - let resetItem = UIBarButtonItem( - title: "", - style: .plain, - target: self, - action: #selector(actionReset) - ) - - let normalAttributes: [NSAttributedString.Key: Any] = [ - .foregroundColor: R.color.colorWhite()!, - .font: UIFont.p0Paragraph - ] - - let highlightedAttributes: [NSAttributedString.Key: Any] = [ - .foregroundColor: R.color.colorWhite()!.withAlphaComponent(0.5), - .font: UIFont.p0Paragraph - ] - - resetItem.setTitleTextAttributes(normalAttributes, for: .normal) - resetItem.setTitleTextAttributes(highlightedAttributes, for: .highlighted) - resetItem.setTitleTextAttributes(highlightedAttributes, for: .disabled) - - navigationItem.rightBarButtonItem = resetItem - } - - private func setupTableView() { - rootView.tableView.registerClassForCell(SwitchTableViewCell.self) - rootView.tableView.dataSource = self - rootView.tableView.rowHeight = 48.0 - rootView.tableView.separatorInset = UIEdgeInsets( - top: 0.0, - left: UIConstants.horizontalInset, - bottom: 0.0, - right: UIConstants.horizontalInset - ) - } - - private func setupLocalization() { - let languages = localizationManager?.selectedLocale.rLanguages - - title = R.string.localizable.walletFiltersTitle(preferredLanguages: languages) - navigationItem.rightBarButtonItem?.title = R.string.localizable - .commonReset(preferredLanguages: languages) - - rootView.applyButton.imageWithTitleView?.title = R.string.localizable - .commonApply(preferredLanguages: languages) - rootView.applyButton.invalidateLayout() - - let title = R.string.localizable.commonShow(preferredLanguages: languages) - rootView.headerView.bind(title: title, icon: nil) - } - - private func updateActionsState() { - let isEnabled = viewModel?.canApply ?? false - rootView.applyButton.set(enabled: isEnabled) - navigationItem.rightBarButtonItem?.isEnabled = viewModel?.canReset ?? false - } - - @objc private func actionReset() { - presenter.reset() - } - - @objc private func actionApply() { - presenter.apply() - } -} - -extension WalletHistoryFilterViewController: UITableViewDataSource { - func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { - viewModel?.items.count ?? 0 - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let items = viewModel?.items ?? [] - - let cell = tableView.dequeueReusableCellWithType(SwitchTableViewCell.self)! - cell.delegate = self - - let locale = localizationManager?.selectedLocale ?? Locale.current - let title = items[indexPath.row].title.value(for: locale) - let isOn = items[indexPath.row].isOn - - cell.bind(title: title, isOn: isOn) - - return cell - } -} - -extension WalletHistoryFilterViewController: SwitchTableViewCellDelegate { - func didToggle(cell: SwitchTableViewCell) { - guard let indexPath = rootView.tableView.indexPath(for: cell) else { - return - } - - presenter.toggleFilterItem(at: indexPath.row) - } -} - -extension WalletHistoryFilterViewController: WalletHistoryFilterViewProtocol { - func didReceive(viewModel: WalletHistoryFilterViewModel) { - self.viewModel = viewModel - - rootView.tableView.reloadData() - - updateActionsState() - } - - func didConfirm(viewModel: WalletHistoryFilterViewModel) { - self.viewModel = viewModel - - updateActionsState() - } -} - -extension WalletHistoryFilterViewController: Localizable { - func applyLocalization() { - if isViewLoaded { - setupLocalization() - rootView.tableView.reloadData() - } - } -} diff --git a/fearless/Modules/Wallet/HistoryFilter/WalletHistoryFilterViewFactory.swift b/fearless/Modules/Wallet/HistoryFilter/WalletHistoryFilterViewFactory.swift deleted file mode 100644 index 896d12298e..0000000000 --- a/fearless/Modules/Wallet/HistoryFilter/WalletHistoryFilterViewFactory.swift +++ /dev/null @@ -1,30 +0,0 @@ -import Foundation -import SoraFoundation -import CommonWallet - -final class WalletHistoryFilterViewFactory: WalletHistoryFilterViewFactoryProtocol { - static func createView( - request: WalletHistoryRequest, - commandFactory: WalletCommandFactoryProtocol, - delegate: HistoryFilterEditingDelegate? - ) -> WalletHistoryFilterViewProtocol? { - let filter = WalletHistoryFilter(string: request.filter) - - let presenter = WalletHistoryFilterPresenter(filter: filter) - let view = WalletHistoryFilterViewController( - presenter: presenter, - localizationManager: LocalizationManager.shared - ) - - let wireframe = WalletHistoryFilterWireframe( - originalRequest: request, - commandFactory: commandFactory, - delegate: delegate - ) - - presenter.view = view - presenter.wireframe = wireframe - - return view - } -} diff --git a/fearless/Modules/Wallet/HistoryFilter/WalletHistoryFilterViewLayout.swift b/fearless/Modules/Wallet/HistoryFilter/WalletHistoryFilterViewLayout.swift deleted file mode 100644 index a35d651275..0000000000 --- a/fearless/Modules/Wallet/HistoryFilter/WalletHistoryFilterViewLayout.swift +++ /dev/null @@ -1,64 +0,0 @@ -import UIKit -import SnapKit - -final class WalletHistoryFilterViewLayout: UIView { - let tableView: UITableView = { - let tableView = UITableView() - tableView.tableFooterView = UIView() - tableView.backgroundColor = .clear - tableView.separatorColor = R.color.colorDarkGray() - return tableView - }() - - let headerView: IconTitleHeaderView = { - let view = R.nib.iconTitleHeaderView(owner: nil)! - view.titleView.titleColor = R.color.colorWhite() - view.titleView.titleFont = .h4Title - view.titleView.spacingBetweenLabelAndIcon = 0 - return view - }() - - let applyButton: TriangularedButton = { - let button = TriangularedButton() - button.applyEnabledStyle() - return button - }() - - override init(frame: CGRect) { - super.init(frame: frame) - configure() - setupLayout() - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func configure() { - backgroundColor = R.color.colorBlack() - } - - private func setupLayout() { - addSubview(tableView) - - tableView.snp.makeConstraints { make in - make.bottom.leading.trailing.equalToSuperview() - make.top.equalTo(safeAreaLayoutGuide) - } - - headerView.snp.makeConstraints { make in - make.height.equalTo(UIConstants.tableHeaderHeight) - } - - tableView.tableHeaderView = headerView - - addSubview(applyButton) - - applyButton.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview().inset(UIConstants.horizontalInset) - make.bottom.equalTo(safeAreaLayoutGuide).inset(UIConstants.actionBottomInset) - make.height.equalTo(UIConstants.actionHeight) - } - } -} diff --git a/fearless/Modules/Wallet/HistoryFilter/WalletHistoryFilterWireframe.swift b/fearless/Modules/Wallet/HistoryFilter/WalletHistoryFilterWireframe.swift deleted file mode 100644 index 682ff126c9..0000000000 --- a/fearless/Modules/Wallet/HistoryFilter/WalletHistoryFilterWireframe.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation -import CommonWallet - -final class WalletHistoryFilterWireframe: WalletHistoryFilterWireframeProtocol { - let commandFactory: WalletCommandFactoryProtocol - weak var delegate: HistoryFilterEditingDelegate? - let originalRequest: WalletHistoryRequest - - init( - originalRequest: WalletHistoryRequest, - commandFactory: WalletCommandFactoryProtocol, - delegate: HistoryFilterEditingDelegate? - ) { - self.originalRequest = originalRequest - self.commandFactory = commandFactory - self.delegate = delegate - } - - func proceed(from _: WalletHistoryFilterViewProtocol?, applying filter: WalletHistoryFilter) { - var newRequest = WalletHistoryRequest(assets: originalRequest.assets ?? []) - newRequest.filter = (filter != .all) ? String(filter.rawValue) : nil - - delegate?.historyFilterDidEdit(request: newRequest) - - let command = commandFactory.prepareHideCommand(with: .dismiss) - try? command.execute() - } -} diff --git a/fearless/Modules/Wallet/InvoiceScan/InvoiceScanConfigurator.swift b/fearless/Modules/Wallet/InvoiceScan/InvoiceScanConfigurator.swift deleted file mode 100644 index 1418015a2d..0000000000 --- a/fearless/Modules/Wallet/InvoiceScan/InvoiceScanConfigurator.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Foundation -import CommonWallet -import IrohaCrypto - -final class InvoiceScanConfigurator { - let searchEngine: InvoiceLocalSearchEngineProtocol - - init(networkType: SNAddressType) { - searchEngine = InvoiceScanLocalSearchEngine(chainFormat: .substrate(UInt16(networkType.rawValue))) - } - - let style: InvoiceScanViewStyleProtocol = { - let title = WalletTextStyle(font: UIFont.h3Title, color: R.color.colorWhite()!) - let message = WalletTextStyle(font: UIFont.h3Title, color: R.color.colorWhite()!) - - let uploadTitle = WalletTextStyle(font: UIFont.h5Title, color: R.color.colorWhite()!) - let upload = WalletRoundedButtonStyle(background: R.color.colorPink()!, title: uploadTitle) - - return InvoiceScanViewStyle( - background: R.color.colorBlack()!, - title: title, - message: message, - maskBackground: R.color.colorBlack()!.withAlphaComponent(0.8), - upload: upload - ) - }() - - func configure(builder: InvoiceScanModuleBuilderProtocol) { - builder - .with(viewStyle: style) - .with(localSearchEngine: searchEngine) - } -} diff --git a/fearless/Modules/Wallet/InvoiceScan/InvoiceScanLocalSearchEngine.swift b/fearless/Modules/Wallet/InvoiceScan/InvoiceScanLocalSearchEngine.swift deleted file mode 100644 index 2691f3eddc..0000000000 --- a/fearless/Modules/Wallet/InvoiceScan/InvoiceScanLocalSearchEngine.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation -import CommonWallet -import IrohaCrypto - -final class InvoiceScanLocalSearchEngine: InvoiceLocalSearchEngineProtocol { - let chainFormat: ChainFormat - - init(chainFormat: ChainFormat) { - self.chainFormat = chainFormat - } - - func searchByAccountId(_ accountIdHex: String) -> SearchData? { - guard let accountId = AccountId.matchHex(accountIdHex) else { - return nil - } - - guard let address = try? AddressFactory.address( - for: accountId, - chainFormat: chainFormat - ) else { - return nil - } - - let context = ContactContext(destination: .local) - return SearchData( - accountId: accountIdHex, - firstName: address, - lastName: "", - context: context.toContext() - ) - } -} diff --git a/fearless/Modules/Wallet/Model/TransferConstants.swift b/fearless/Modules/Wallet/Model/TransferConstants.swift deleted file mode 100644 index 1d5aef8803..0000000000 --- a/fearless/Modules/Wallet/Model/TransferConstants.swift +++ /dev/null @@ -1,5 +0,0 @@ -import Foundation - -struct TransferConstants { - static let maxAmount: Decimal = 1e+7 -} diff --git a/fearless/Modules/Wallet/Style/WalletCommonStyleConfigurator.swift b/fearless/Modules/Wallet/Style/WalletCommonStyleConfigurator.swift deleted file mode 100644 index 6ab8fcf649..0000000000 --- a/fearless/Modules/Wallet/Style/WalletCommonStyleConfigurator.swift +++ /dev/null @@ -1,63 +0,0 @@ -import Foundation -import CommonWallet - -struct WalletCommonStyleConfigurator { - let navigationBarStyle: WalletNavigationBarStyleProtocol = { - var navigationBarStyle = WalletNavigationBarStyle( - barColor: .clear, - shadowColor: .clear, - itemTintColor: R.color.colorWhite()!, - titleColor: R.color.colorWhite()!, - titleFont: UIFont.h3Title, - backButtonImage: R.image.iconBack() - ) - return navigationBarStyle - }() - - let accessoryStyle: WalletAccessoryStyleProtocol = { - let title = WalletTextStyle( - font: UIFont.p1Paragraph, - color: R.color.colorWhite()! - ) - - let buttonTitle = WalletTextStyle( - font: UIFont.h5Title, - color: R.color.colorWhite()! - ) - - let buttonStyle = WalletRoundedButtonStyle( - background: R.color.colorPink()!, - title: buttonTitle - ) - - let separator = WalletStrokeStyle(color: .clear, lineWidth: 0.0) - - return WalletAccessoryStyle( - title: title, - action: buttonStyle, - separator: separator, - background: R.color.colorBlack()! - ) - }() -} - -extension WalletCommonStyleConfigurator { - func configure(builder: WalletStyleBuilderProtocol) { - builder - .with(background: R.color.colorBlack()!) - .with(navigationBarStyle: navigationBarStyle) - .with(header1: UIFont.h1Title) - .with(header2: UIFont.h2Title) - .with(header3: UIFont.h3Title) - .with(header4: UIFont.h4Title) - .with(bodyBold: UIFont.h5Title) - .with(bodyRegular: UIFont.p1Paragraph) - .with(small: UIFont.p2Paragraph) - .with(keyboardIcon: R.image.iconKeyboardOff()!) - .with(caretColor: R.color.colorWhite()!) - .with(closeIcon: R.image.iconClose()) - .with(shareIcon: R.image.iconShare()) - .with(accessoryStyle: accessoryStyle) - .with(formCellStyle: WalletFormCellStyle.fearless) - } -} diff --git a/fearless/Modules/Wallet/Style/WalletFormCellStyle+Internal.swift b/fearless/Modules/Wallet/Style/WalletFormCellStyle+Internal.swift deleted file mode 100644 index ff7ab2d350..0000000000 --- a/fearless/Modules/Wallet/Style/WalletFormCellStyle+Internal.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation -import CommonWallet - -extension WalletFormCellStyle { - static var fearless: WalletFormCellStyle { - let title = WalletTextStyle( - font: UIFont.p1Paragraph, - color: R.color.colorLightGray()! - ) - let details = WalletTextStyle( - font: UIFont.p1Paragraph, - color: R.color.colorWhite()! - ) - - let link = WalletLinkStyle( - normal: R.color.colorWhite()!, - highlighted: R.color.colorPink()! - ) - - return WalletFormCellStyle( - title: title, - details: details, - link: link, - separator: R.color.colorDarkGray()! - ) - } -} diff --git a/fearless/Modules/Wallet/View/DummySelectedAssetView.swift b/fearless/Modules/Wallet/View/DummySelectedAssetView.swift deleted file mode 100644 index 789c6abf56..0000000000 --- a/fearless/Modules/Wallet/View/DummySelectedAssetView.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation -import CommonWallet -import SoraUI - -final class DummySelectedAssetView: UIControl { - weak var delegate: SelectedAssetViewDelegate? - var activated: Bool = false - var borderType: BorderType = .none - - init() { - super.init(frame: .zero) - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -extension DummySelectedAssetView: SelectedAssetViewProtocol { - func bind(viewModel _: AssetSelectionViewModelProtocol) {} -} - -extension DummySelectedAssetView: WalletFormBordering {} diff --git a/fearless/Modules/Wallet/View/ViewModel/FeePriceViewModel.swift b/fearless/Modules/Wallet/View/ViewModel/FeePriceViewModel.swift deleted file mode 100644 index 640c152661..0000000000 --- a/fearless/Modules/Wallet/View/ViewModel/FeePriceViewModel.swift +++ /dev/null @@ -1,11 +0,0 @@ -import CommonWallet - -struct FeePriceViewModel: BalanceViewModelProtocol, FeeViewModelProtocol { - let amount: String - let price: String? - - let title: String - let details: String - let isLoading: Bool - let allowsEditing: Bool -} diff --git a/fearless/Modules/Wallet/View/ViewModel/RichAmountDisplayViewModel.swift b/fearless/Modules/Wallet/View/ViewModel/RichAmountDisplayViewModel.swift deleted file mode 100644 index 2eddf2f92c..0000000000 --- a/fearless/Modules/Wallet/View/ViewModel/RichAmountDisplayViewModel.swift +++ /dev/null @@ -1,29 +0,0 @@ -import SoraFoundation -import CommonWallet - -protocol RichAmountDisplayViewModelProtocol: WalletFormViewBindingProtocol, - AssetBalanceViewModelProtocol { - var title: String { get } - var amount: String { get } -} - -struct RichAmountDisplayViewModel: RichAmountDisplayViewModelProtocol { - var iconViewModel: ImageViewModelProtocol? - - let title: String - let amount: String - let icon: UIImage? - let symbol: String - let balance: String? - let fiatBalance: String? - let price: String? - let selectable: Bool - - func accept(definition: WalletFormDefining) -> WalletFormItemView? { - if let definition = definition as? WalletFearlessFormDefining { - return definition.defineViewForAmountDisplay(self) - } else { - return nil - } - } -} diff --git a/fearless/Modules/Wallet/View/ViewModel/RichAmountInputViewModel.swift b/fearless/Modules/Wallet/View/ViewModel/RichAmountInputViewModel.swift deleted file mode 100644 index 59e5640dc3..0000000000 --- a/fearless/Modules/Wallet/View/ViewModel/RichAmountInputViewModel.swift +++ /dev/null @@ -1,108 +0,0 @@ -import SoraFoundation -import CommonWallet -import SSFModels - -protocol RichAmountInputViewModelProtocol: IAmountInputViewModel { - var balanceViewModelFactory: BalanceViewModelFactoryProtocol { get } - var priceData: PriceData? { get } - var displayPrice: LocalizableResource { get } - var displayBalance: LocalizableResource { get } - var decimalBalance: Decimal? { get } - var fee: Decimal? { get } - var symbol: String { get } - var icon: UIImage? { get } - var balance: String? { get } - - func didSelectPercentage(_ percentage: Float) -} - -final class RichAmountInputViewModel: RichAmountInputViewModelProtocol { - let amountInputViewModel: IAmountInputViewModel - let balanceViewModelFactory: BalanceViewModelFactoryProtocol - - let symbol: String - let icon: UIImage? - let balance: String? - let priceData: PriceData? - let decimalBalance: Decimal? - let fee: Decimal? - let limit: Decimal - - var displayAmount: String { - amountInputViewModel.displayAmount - } - - var decimalAmount: Decimal? { - amountInputViewModel.decimalAmount - } - - var isValid: Bool { - amountInputViewModel.isValid - } - - var observable: WalletViewModelObserverContainer { - amountInputViewModel.observable - } - - var displayPrice: LocalizableResource { - LocalizableResource { [self] locale in - guard let amount = decimalAmount, - let priceData = priceData - else { return "" } - - return balanceViewModelFactory.balanceFromPrice( - amount, - priceData: priceData, - usageCase: .inputFiat - ).value(for: locale).price ?? "" - } - } - - var displayBalance: LocalizableResource { - LocalizableResource { locale in - R.string.localizable - .commonAvailableFormat(self.balance ?? "0", preferredLanguages: locale.rLanguages) - } - } - - init( - amountInputViewModel: IAmountInputViewModel, - balanceViewModelFactory: BalanceViewModelFactoryProtocol, - symbol: String, - icon: UIImage?, - balance: String?, - priceData: PriceData?, - decimalBalance: Decimal?, - fee: Decimal?, - limit: Decimal - ) { - self.amountInputViewModel = amountInputViewModel - self.balanceViewModelFactory = balanceViewModelFactory - self.symbol = symbol - self.icon = icon - self.balance = balance - self.priceData = priceData - self.decimalBalance = decimalBalance - self.fee = fee - self.limit = limit - } - - func didReceiveReplacement(_ string: String, for range: NSRange) -> Bool { - amountInputViewModel.didReceiveReplacement(string, for: range) - } - - func didUpdateAmount(to newAmount: Decimal) { - amountInputViewModel.didUpdateAmount(to: newAmount) - } - - func didSelectPercentage(_ percentage: Float) { - if let balance = decimalBalance, - let fee = fee { - var newAmount = max(balance - fee, 0.0) - newAmount = min(newAmount, limit) - newAmount *= Decimal(Double(percentage)) - - didUpdateAmount(to: newAmount) - } - } -} diff --git a/fearless/Modules/Wallet/View/ViewModel/WalletCompoundDetailsViewModel.swift b/fearless/Modules/Wallet/View/ViewModel/WalletCompoundDetailsViewModel.swift deleted file mode 100644 index b36b141c13..0000000000 --- a/fearless/Modules/Wallet/View/ViewModel/WalletCompoundDetailsViewModel.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation -import CommonWallet - -struct WalletCompoundDetailsViewModel: WalletFormViewBindingProtocol { - let title: String - let details: String - let mainIcon: UIImage? - let actionIcon: UIImage? - let command: WalletCommandProtocol - let enabled: Bool - - func accept(definition: WalletFormDefining) -> WalletFormItemView? { - if let definition = definition as? WalletFearlessFormDefining { - return definition.defineViewForCompoundDetails(self) - } else { - return nil - } - } -} - -extension WalletCompoundDetailsViewModel: MultilineTitleIconViewModelProtocol { - var text: String { details } - - var icon: UIImage? { mainIcon } -} diff --git a/fearless/Modules/Wallet/View/ViewModel/WalletTokenViewModel.swift b/fearless/Modules/Wallet/View/ViewModel/WalletTokenViewModel.swift deleted file mode 100644 index 898ac12586..0000000000 --- a/fearless/Modules/Wallet/View/ViewModel/WalletTokenViewModel.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation -import CommonWallet - -struct WalletTokenViewModel: AssetSelectionViewModelProtocol { - let header: String - - let title: String - - let subtitle: String - - let details: String - - let icon: UIImage? - - let state: SelectedAssetState - - let detailsCommand: WalletCommandProtocol? -} - -extension WalletTokenViewModel: WalletFormViewBindingProtocol { - func accept(definition: WalletFormDefining) -> WalletFormItemView? { - if let definition = definition as? WalletFearlessFormDefining { - return definition.defineViewForFearlessTokenViewModel(self) - } else { - return nil - } - } -} diff --git a/fearless/Modules/Wallet/View/WalletAmountView.xib b/fearless/Modules/Wallet/View/WalletAmountView.xib deleted file mode 100644 index 34de67a9b5..0000000000 --- a/fearless/Modules/Wallet/View/WalletAmountView.xib +++ /dev/null @@ -1,163 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/fearless/Modules/Wallet/View/WalletBaseAmountView.swift b/fearless/Modules/Wallet/View/WalletBaseAmountView.swift deleted file mode 100644 index f994882b89..0000000000 --- a/fearless/Modules/Wallet/View/WalletBaseAmountView.swift +++ /dev/null @@ -1,73 +0,0 @@ -import Foundation -import CommonWallet -import SoraUI - -class WalletBaseAmountView: UIView { - @IBOutlet private(set) var borderView: BorderedContainerView! - @IBOutlet private(set) var fieldBackgroundView: TriangularedView! - @IBOutlet private(set) var amountInputView: AmountInputView! - @IBOutlet private(set) var contentView: UIView! - - override init(frame: CGRect) { - super.init(frame: frame) - - setupSubviews() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - - setupSubviews() - } - - func setupSubviews() { - _ = R.nib.walletAmountView(owner: self) - addSubview(contentView) - - contentView.translatesAutoresizingMaskIntoConstraints = false - - contentView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - contentView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - contentView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true - contentView.topAnchor.constraint(equalTo: topAnchor).isActive = true - - fieldBackgroundView.strokeColor = .clear - fieldBackgroundView.highlightedStrokeColor = .clear - - amountInputView.triangularedBackgroundView?.strokeColor = R.color.colorStrokeGray()! - amountInputView.triangularedBackgroundView?.highlightedStrokeColor = R.color.colorStrokeGray()! - amountInputView.triangularedBackgroundView?.strokeWidth = 1.0 - amountInputView.triangularedBackgroundView?.fillColor = .clear - amountInputView.triangularedBackgroundView?.highlightedFillColor = .clear - - amountInputView.titleLabel.textColor = R.color.colorLightGray() - amountInputView.titleLabel.font = .p2Paragraph - amountInputView.priceLabel.textColor = R.color.colorLightGray() - amountInputView.priceLabel.font = .p2Paragraph - amountInputView.symbolLabel.textColor = R.color.colorWhite() - amountInputView.symbolLabel.font = .h4Title - amountInputView.balanceLabel.textColor = R.color.colorLightGray() - amountInputView.balanceLabel.font = .p2Paragraph - amountInputView.textField.font = .h4Title - amountInputView.textField.textColor = R.color.colorWhite() - amountInputView.textField.tintColor = R.color.colorWhite() - amountInputView.verticalSpacing = 2.0 - amountInputView.iconRadius = 12.0 - amountInputView.contentInsets = UIEdgeInsets( - top: 8.0, - left: UIConstants.horizontalInset, - bottom: 8.0, - right: UIConstants.horizontalInset - ) - - amountInputView.textField.attributedPlaceholder = NSAttributedString( - string: "", - attributes: [ - .foregroundColor: R.color.colorWhite()!.withAlphaComponent(0.5), - .font: UIFont.h4Title - ] - ) - - amountInputView.textField.keyboardType = .decimalPad - } -} diff --git a/fearless/Modules/Wallet/View/WalletBaseTokenView.swift b/fearless/Modules/Wallet/View/WalletBaseTokenView.swift deleted file mode 100644 index 36d73816d6..0000000000 --- a/fearless/Modules/Wallet/View/WalletBaseTokenView.swift +++ /dev/null @@ -1,50 +0,0 @@ -import UIKit -import CommonWallet -import SoraUI - -class WalletBaseTokenView: UIControl { - var borderType: BorderType { - get { - borderedView.borderType - } - set(newValue) { - borderedView.borderType = newValue - } - } - - @IBOutlet private(set) var borderedView: BorderedContainerView! - @IBOutlet private(set) var borderedActionControl: BorderedSubtitleActionView! - @IBOutlet private(set) var balanceTitle: UILabel! - @IBOutlet private(set) var balanceDetails: UILabel! - @IBOutlet private(set) var contentView: UIView! - - override var intrinsicContentSize: CGSize { - CGSize(width: UIView.noIntrinsicMetric, height: 102.0) - } - - override init(frame: CGRect) { - super.init(frame: frame) - - setupSubviews() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - - setupSubviews() - } - - func setupSubviews() { - _ = R.nib.walletTokenView(owner: self) - addSubview(contentView) - - contentView.translatesAutoresizingMaskIntoConstraints = false - - contentView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - contentView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - contentView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true - contentView.topAnchor.constraint(equalTo: topAnchor).isActive = true - } - - @IBAction func actionBalance() {} -} diff --git a/fearless/Modules/Wallet/View/WalletCompoundDetailsView.swift b/fearless/Modules/Wallet/View/WalletCompoundDetailsView.swift deleted file mode 100644 index 0215c45105..0000000000 --- a/fearless/Modules/Wallet/View/WalletCompoundDetailsView.swift +++ /dev/null @@ -1,105 +0,0 @@ -import Foundation -import CommonWallet -import SoraUI - -final class WalletCompoundDetailsView: WalletFormItemView { - var contentInsets = UIEdgeInsets(top: 16.0, left: 0.0, bottom: 0.0, right: 0.0) { - didSet { - bottomConstraint.constant = contentInsets.bottom - invalidateIntrinsicContentSize() - setNeedsLayout() - } - } - - @IBOutlet private var borderedView: BorderedContainerView! - @IBOutlet private var contentView: DetailsTriangularedView! - - @IBOutlet private var bottomConstraint: NSLayoutConstraint! - - private var viewModel: WalletCompoundDetailsViewModel? - - override var intrinsicContentSize: CGSize { - CGSize( - width: UIView.noIntrinsicMetric, - height: 52.0 + contentInsets.top + contentInsets.bottom - ) - } - - override func awakeFromNib() { - super.awakeFromNib() - - contentView.addTarget(self, action: #selector(actionDetails), for: .touchUpInside) - - contentView.subtitleLabel?.lineBreakMode = .byTruncatingMiddle - - bottomConstraint.constant = contentInsets.bottom - } - - private func setupContentInsets() { - if borderType.contains(.bottom) { - var contentInsets = self.contentInsets - contentInsets.bottom = 16.0 - self.contentInsets = contentInsets - } - } - - func bind(viewModel: WalletCompoundDetailsViewModel) { - self.viewModel = viewModel - - contentView.title = viewModel.title - contentView.subtitle = viewModel.details - - if let mainIcon = viewModel.mainIcon { - contentView.iconRadius = max(mainIcon.size.width, mainIcon.size.height) / 2.0 - contentView.horizontalSpacing = 4.0 - contentView.iconImage = mainIcon - } else { - contentView.iconRadius = 0.0 - contentView.horizontalSpacing = 0.0 - contentView.iconImage = nil - } - - contentView.actionImage = viewModel.actionIcon - - if viewModel.enabled { - contentView.fillColor = .clear - contentView.strokeColor = R.color.colorGray()! - contentView.highlightedStrokeColor = R.color.colorGray()! - contentView.highlightedFillColor = R.color.colorHighlightedPink()! - } else { - contentView.fillColor = R.color.colorDarkGray()! - contentView.strokeColor = R.color.colorDarkGray()! - contentView.highlightedStrokeColor = R.color.colorDarkGray()! - contentView.highlightedFillColor = R.color.colorHighlightedPink()! - } - } - - @objc private func actionDetails() { - try? viewModel?.command.execute() - } -} - -extension WalletCompoundDetailsView { - var borderType: BorderType { - get { - borderedView.borderType - } - - set { - borderedView.borderType = newValue - setupContentInsets() - invalidateIntrinsicContentSize() - setNeedsLayout() - } - } -} - -extension WalletCompoundDetailsView: ReceiverViewProtocol { - func bind(viewModel: MultilineTitleIconViewModelProtocol) { - guard let viewModel = viewModel as? WalletCompoundDetailsViewModel else { - return - } - - bind(viewModel: viewModel) - } -} diff --git a/fearless/Modules/Wallet/View/WalletCompoundDetailsView.xib b/fearless/Modules/Wallet/View/WalletCompoundDetailsView.xib deleted file mode 100644 index 81409a6e1b..0000000000 --- a/fearless/Modules/Wallet/View/WalletCompoundDetailsView.xib +++ /dev/null @@ -1,143 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/fearless/Modules/Wallet/View/WalletDisplayAmountView.swift b/fearless/Modules/Wallet/View/WalletDisplayAmountView.swift deleted file mode 100644 index c4b83c74fa..0000000000 --- a/fearless/Modules/Wallet/View/WalletDisplayAmountView.swift +++ /dev/null @@ -1,39 +0,0 @@ -import UIKit -import SoraUI -import CommonWallet -import SoraFoundation - -final class WalletDisplayAmountView: WalletBaseAmountView { - override var intrinsicContentSize: CGSize { - CGSize(width: UIView.noIntrinsicMetric, height: 72.0) - } - - var viewModel: RichAmountDisplayViewModelProtocol? -} - -extension WalletDisplayAmountView: WalletFormBordering { - var borderType: BorderType { - get { - borderView.borderType - } - set(newValue) { - borderView.borderType = newValue - } - } - - func bind(viewModel: RichAmountDisplayViewModel) { - self.viewModel = viewModel - - amountInputView.title = viewModel.title - amountInputView.textField.text = viewModel.amount - amountInputView.isUserInteractionEnabled = false - - amountInputView.triangularedBackgroundView?.applyDisabledStyle() - - amountInputView.assetIcon = viewModel.icon - amountInputView.symbol = viewModel.symbol - - amountInputView.priceText = viewModel.price - amountInputView.balanceText = viewModel.balance - } -} diff --git a/fearless/Modules/Wallet/View/WalletDisplayTokenView.swift b/fearless/Modules/Wallet/View/WalletDisplayTokenView.swift deleted file mode 100644 index 6d757c1eef..0000000000 --- a/fearless/Modules/Wallet/View/WalletDisplayTokenView.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation -import CommonWallet -import SoraUI - -final class WalletDisplayTokenView: WalletBaseTokenView { - var style: WalletFormTokenViewStyle? - - private(set) var viewModel: WalletTokenViewModel? - - override func actionBalance() { - try? viewModel?.detailsCommand?.execute() - } -} - -extension WalletDisplayTokenView { - func bind(viewModel: WalletTokenViewModel) { - self.viewModel = viewModel - - borderedActionControl.actionControl.contentView.titleLabel.text = viewModel.header - borderedActionControl.actionControl.contentView.subtitleImageView.image = viewModel.icon - borderedActionControl.actionControl.contentView.subtitleLabelView.text = viewModel.title - - balanceTitle.text = viewModel.subtitle.uppercased() + ":" - balanceDetails.text = viewModel.details - - borderedActionControl.isUserInteractionEnabled = false - - borderedActionControl.applyDisabledStyle() - } -} - -extension WalletDisplayTokenView: WalletFormBordering {} diff --git a/fearless/Modules/Wallet/View/WalletFearlessDefinition.swift b/fearless/Modules/Wallet/View/WalletFearlessDefinition.swift deleted file mode 100644 index f5534f8f2d..0000000000 --- a/fearless/Modules/Wallet/View/WalletFearlessDefinition.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Foundation -import CommonWallet -import SoraFoundation - -final class WalletFearlessDefinition: WalletFearlessFormDefining { - let binder: WalletFormViewModelBinderProtocol - let itemViewFactory: WalletFormItemViewFactoryProtocol - - init( - binder: WalletFormViewModelBinderProtocol, - itemViewFactory: WalletFormItemViewFactoryProtocol - ) { - self.binder = binder - self.itemViewFactory = itemViewFactory - } - - func defineViewForFearlessTokenViewModel(_: WalletTokenViewModel) -> WalletFormItemView? { - nil - } - - func defineViewForCompoundDetails(_ viewModel: WalletCompoundDetailsViewModel) -> WalletFormItemView? { - let detailsView = R.nib.walletCompoundDetailsView(owner: nil)! - detailsView.bind(viewModel: viewModel) - detailsView.contentInsets = UIEdgeInsets(top: 4.0, left: 0.0, bottom: 12.0, right: 0.0) - return detailsView - } - - func defineViewForAmountDisplay( - _ viewModel: RichAmountDisplayViewModel) -> WalletFormItemView? { - let amountDisplayView = WalletDisplayAmountView() - amountDisplayView.bind(viewModel: viewModel) - return amountDisplayView - } -} diff --git a/fearless/Modules/Wallet/View/WalletFearlessDefinitionFactory.swift b/fearless/Modules/Wallet/View/WalletFearlessDefinitionFactory.swift deleted file mode 100644 index 96e6124a63..0000000000 --- a/fearless/Modules/Wallet/View/WalletFearlessDefinitionFactory.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation -import CommonWallet -import SoraFoundation - -struct WalletFearlessDefinitionFactory: WalletFormDefinitionFactoryProtocol { - func createDefinitionWithBinder( - _ binder: WalletFormViewModelBinderProtocol, - itemFactory: WalletFormItemViewFactoryProtocol - ) -> WalletFormDefining { - WalletFearlessDefinition( - binder: binder, - itemViewFactory: itemFactory - ) - } -} diff --git a/fearless/Modules/Wallet/View/WalletFearlessFormDefining.swift b/fearless/Modules/Wallet/View/WalletFearlessFormDefining.swift deleted file mode 100644 index e6df0b81a8..0000000000 --- a/fearless/Modules/Wallet/View/WalletFearlessFormDefining.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation -import CommonWallet - -protocol WalletFearlessFormDefining: WalletFormDefining { - func defineViewForFearlessTokenViewModel(_ model: WalletTokenViewModel) -> WalletFormItemView? - func defineViewForCompoundDetails(_ viewModel: WalletCompoundDetailsViewModel) -> WalletFormItemView? - func defineViewForAmountDisplay(_ viewModel: RichAmountDisplayViewModel) -> WalletFormItemView? -} diff --git a/fearless/Modules/Wallet/View/WalletSingleActionAccessoryFactory.swift b/fearless/Modules/Wallet/View/WalletSingleActionAccessoryFactory.swift deleted file mode 100644 index a0b8e6adda..0000000000 --- a/fearless/Modules/Wallet/View/WalletSingleActionAccessoryFactory.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation -import CommonWallet - -struct WalletSingleActionAccessoryFactory: CommonWallet.AccessoryViewFactoryProtocol { - static func createAccessoryView( - from _: WalletAccessoryViewType, - style _: WalletAccessoryStyleProtocol?, - target: Any?, - completionSelector: Selector? - ) -> CommonWallet.AccessoryViewProtocol { - let view = R.nib.walletSingleActionAccessoryView(owner: nil)! - - if let target = target, let selector = completionSelector { - view.actionButton.addTarget(target, action: selector, for: .touchUpInside) - } - - return view - } -} diff --git a/fearless/Modules/Wallet/View/WalletSingleActionAccessoryView.swift b/fearless/Modules/Wallet/View/WalletSingleActionAccessoryView.swift deleted file mode 100644 index fc1e97117c..0000000000 --- a/fearless/Modules/Wallet/View/WalletSingleActionAccessoryView.swift +++ /dev/null @@ -1,29 +0,0 @@ -import UIKit -import CommonWallet -import SoraUI - -final class WalletSingleActionAccessoryView: UIView { - @IBOutlet private(set) var actionButton: TriangularedButton! -} - -extension WalletSingleActionAccessoryView: CommonWallet.AccessoryViewProtocol { - var contentView: UIView { - self - } - - var isActionEnabled: Bool { - get { - actionButton.isEnabled - } - set(newValue) { - actionButton.set(enabled: newValue) - } - } - - var extendsUnderSafeArea: Bool { true } - - func bind(viewModel: AccessoryViewModelProtocol) { - actionButton.imageWithTitleView?.title = viewModel.action - actionButton.invalidateLayout() - } -} diff --git a/fearless/Modules/Wallet/View/WalletSingleActionAccessoryView.xib b/fearless/Modules/Wallet/View/WalletSingleActionAccessoryView.xib deleted file mode 100644 index 0e27f60e61..0000000000 --- a/fearless/Modules/Wallet/View/WalletSingleActionAccessoryView.xib +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/fearless/Modules/Wallet/View/WalletTokenView.xib b/fearless/Modules/Wallet/View/WalletTokenView.xib deleted file mode 100644 index 0ca6ebd55c..0000000000 --- a/fearless/Modules/Wallet/View/WalletTokenView.xib +++ /dev/null @@ -1,189 +0,0 @@ - - - - - - - - - - - - - - sora-rc004-0417-Bold - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/fearless/Modules/Wallet/View/WalletTransferTokenView.swift b/fearless/Modules/Wallet/View/WalletTransferTokenView.swift deleted file mode 100644 index fc9f890992..0000000000 --- a/fearless/Modules/Wallet/View/WalletTransferTokenView.swift +++ /dev/null @@ -1,38 +0,0 @@ -import UIKit -import CommonWallet -import SoraUI - -final class WalletTransferTokenView: WalletBaseTokenView { - weak var delegate: SelectedAssetViewDelegate? - - private(set) var viewModel: WalletTokenViewModel? - - var activated: Bool { - borderedActionControl.actionControl.isActivated - } - - override func actionBalance() { - try? viewModel?.detailsCommand?.execute() - } -} - -extension WalletTransferTokenView: SelectedAssetViewProtocol { - func bind(viewModel: AssetSelectionViewModelProtocol) { - guard let viewModel = viewModel as? WalletTokenViewModel else { - return - } - - self.viewModel = viewModel - - borderedActionControl.actionControl.contentView.titleLabel.text = viewModel.header - borderedActionControl.actionControl.contentView.subtitleImageView.image = viewModel.icon - borderedActionControl.actionControl.contentView.subtitleLabelView.text = viewModel.title - - balanceTitle.text = viewModel.subtitle.uppercased() + ":" - balanceDetails.text = viewModel.details - - borderedActionControl.isUserInteractionEnabled = false - - borderedActionControl.applyDisabledStyle() - } -} diff --git a/fearless/Modules/Wallet/WalletCommandDecorator.swift b/fearless/Modules/Wallet/WalletCommandDecorator.swift deleted file mode 100644 index 33de37df37..0000000000 --- a/fearless/Modules/Wallet/WalletCommandDecorator.swift +++ /dev/null @@ -1,38 +0,0 @@ -import Foundation -import CommonWallet -import SoraFoundation -import RobinHood - -final class StubCommandDecorator: WalletCommandDecoratorProtocol { - var undelyingCommand: WalletCommandProtocol? - - func execute() throws {} -} - -final class WalletCommandDecoratorFactory: WalletCommandDecoratorFactoryProtocol { - let localizationManager: LocalizationManagerProtocol - let dataStorageFacade: StorageFacadeProtocol - - init( - localizationManager: LocalizationManagerProtocol, - dataStorageFacade: StorageFacadeProtocol - ) { - self.localizationManager = localizationManager - self.dataStorageFacade = dataStorageFacade - } - - func createTransferConfirmationDecorator( - with commandFactory: WalletCommandFactoryProtocol, - payload: ConfirmationPayload - ) -> WalletCommandDecoratorProtocol? { - let storage: CoreDataRepository = - dataStorageFacade.createRepository() - - return TransferConfirmCommandProxy( - payload: payload, - localizationManager: localizationManager, - commandFactory: commandFactory, - storage: AnyDataProviderRepository(storage) - ) - } -} diff --git a/fearless/Modules/Wallet/WalletContextFactory.swift b/fearless/Modules/Wallet/WalletContextFactory.swift deleted file mode 100644 index 8966572c0e..0000000000 --- a/fearless/Modules/Wallet/WalletContextFactory.swift +++ /dev/null @@ -1,86 +0,0 @@ -import Foundation -import CommonWallet -import SoraKeystore -import RobinHood -import IrohaCrypto -import SoraFoundation -import SSFUtils - -enum WalletContextFactoryError: Error { - case missingAccount -} - -protocol WalletContextFactoryProtocol { - func createContext() throws -> CommonWalletContextProtocol -} - -final class WalletContextFactory { - let logger: LoggerProtocol - - init(logger: LoggerProtocol = Logger.shared) { - self.logger = logger - } -} - -extension WalletContextFactory: WalletContextFactoryProtocol { - func createContext() throws -> CommonWalletContextProtocol { - guard let selectedAccount = SelectedWalletSettings.shared.value else { - throw WalletContextFactoryError.missingAccount - } - - logger.info("Start wallet for: \(selectedAccount.metaId)") - - return try createDummyContext(for: selectedAccount) - } - - private func createDummyContext(for account: MetaAccountModel) throws -> CommonWalletContextProtocol { - let accountSettings = WalletAccountSettings( - accountId: account.metaId, - assets: [] - ) - - let context = try CommonWalletBuilder.builder( - with: accountSettings, - networkOperationFactory: DummyWalletNetworkOperationFactory() - ).build() - - return context - } -} - -final class DummyWalletNetworkOperationFactory: WalletNetworkOperationFactoryProtocol { - func fetchBalanceOperation(_: [String]) -> CompoundOperationWrapper<[BalanceData]?> { - CompoundOperationWrapper.createWithResult(nil) - } - - func fetchTransactionHistoryOperation( - _: WalletHistoryRequest, - pagination _: Pagination - ) -> CompoundOperationWrapper { - CompoundOperationWrapper.createWithResult(nil) - } - - func transferMetadataOperation(_: TransferMetadataInfo) -> CompoundOperationWrapper { - CompoundOperationWrapper.createWithResult(nil) - } - - func transferOperation(_: TransferInfo) -> CompoundOperationWrapper { - CompoundOperationWrapper.createWithResult(Data(repeating: 0, count: 32)) - } - - func searchOperation(_: String) -> CompoundOperationWrapper<[SearchData]?> { - CompoundOperationWrapper.createWithResult(nil) - } - - func contactsOperation() -> CompoundOperationWrapper<[SearchData]?> { - CompoundOperationWrapper.createWithResult(nil) - } - - func withdrawalMetadataOperation(_: WithdrawMetadataInfo) -> CompoundOperationWrapper { - CompoundOperationWrapper.createWithResult(nil) - } - - func withdrawOperation(_: WithdrawInfo) -> CompoundOperationWrapper { - CompoundOperationWrapper.createWithResult(Data(repeating: 0, count: 32)) - } -} diff --git a/fearless/Modules/Wallet/WalletSingleProviderIdFactory.swift b/fearless/Modules/Wallet/WalletSingleProviderIdFactory.swift deleted file mode 100644 index 201bcf1d3b..0000000000 --- a/fearless/Modules/Wallet/WalletSingleProviderIdFactory.swift +++ /dev/null @@ -1,39 +0,0 @@ -import Foundation -import CommonWallet -import IrohaCrypto - -final class WalletSingleProviderIdFactory: SingleProviderIdentifierFactoryProtocol { - let addressType: SNAddressType - - init(addressType: SNAddressType) { - self.addressType = addressType - } - - func balanceIdentifierForAccountId(_ accountId: String) -> String { - "wallet.cache.\(accountId).\(addressType.rawValue).balance" - } - - func historyIdentifierForAccountId(_ accountId: String, assets _: [String]) -> String { - "wallet.cache.\(accountId).\(addressType.rawValue).history" - } - - func contactsIdentifierForAccountId(_ accountId: String) -> String { - "wallet.cache.\(accountId).\(addressType.rawValue).contacts" - } - - func withdrawMetadataIdentifierForAccountId( - _ accountId: String, - assetId _: String, - optionId _: String - ) -> String { - "wallet.cache.\(accountId).\(addressType.rawValue).withdraw.metadata" - } - - func transferMetadataIdentifierForAccountId( - _ accountId: String, - assetId _: String, - receiverId _: String - ) -> String { - "wallet.cache.\(accountId).\(addressType.rawValue).transfer.metadata" - } -} diff --git a/fearless/Modules/Wallet/WalletStaticImageViewModel.swift b/fearless/Modules/Wallet/WalletStaticImageViewModel.swift index dc78cd51dc..cb06ae76e4 100644 --- a/fearless/Modules/Wallet/WalletStaticImageViewModel.swift +++ b/fearless/Modules/Wallet/WalletStaticImageViewModel.swift @@ -1,5 +1,5 @@ import Foundation -import CommonWallet + import Kingfisher final class WalletStaticImageViewModel: WalletImageViewModelProtocol { diff --git a/fearless/Modules/WalletDetails/WalletDetailsInteractor.swift b/fearless/Modules/WalletDetails/WalletDetailsInteractor.swift index 5f098b3c57..6dc82d17e5 100644 --- a/fearless/Modules/WalletDetails/WalletDetailsInteractor.swift +++ b/fearless/Modules/WalletDetails/WalletDetailsInteractor.swift @@ -1,5 +1,6 @@ import RobinHood import SSFModels +import Foundation final class WalletDetailsInteractor { weak var presenter: WalletDetailsInteractorOutputProtocol! diff --git a/fearless/Resources/chains.json b/fearless/Resources/chains.json index aabf9f6345..be7cc45b9a 100644 --- a/fearless/Resources/chains.json +++ b/fearless/Resources/chains.json @@ -1,16 +1,16 @@ [{ "disabled": false, "chainId": "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3", - "rank": 3, + "rank": 8, "name": "Polkadot", "externalApi": { "staking": { "type": "subsquid", - "url": "https://squid.subsquid.io/fearless-x-polkadot/v/v1/graphql" + "url": "https://squid.subsquid.io/fearless-polkadot/v/v3/graphql" }, "history": { "type": "subsquid", - "url": "https://squid.subsquid.io/fearless-x-polkadot/v/v1/graphql" + "url": "https://squid.subsquid.io/fearless-polkadot/v/v3/graphql" }, "crowdloans": { "type": "github", @@ -100,6 +100,17 @@ "symbol": "DOT" } ] + }, + { + "chainId": "7e4e32d0feafd4f9c9414b0be86373f9a1efa904809b683453a9af6856d38ad5", + "assets": [ + { + "id": "99e66d4f-00cd-4d73-bd1b-3adadcacffb2", + "symbol": "DOT" + } + + ], + "bridgeParachainId": "e92d165ad41e41e215d09713788173aecfdbe34d3bed29409d33a2ef03980738" } ] }, @@ -126,16 +137,16 @@ { "disabled": false, "chainId": "b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe", - "rank": 4, + "rank": 9, "name": "Kusama", "externalApi": { "staking": { "type": "subsquid", - "url": "https://squid.subsquid.io/fearless-x-kusama/v/v1/graphql" + "url": "https://squid.subsquid.io/fearless-kusama-2/v/v3/graphql" }, "history": { "type": "subsquid", - "url": "https://squid.subsquid.io/fearless-x-kusama/v/v1/graphql" + "url": "https://squid.subsquid.io/fearless-kusama-2/v/v3/graphql" }, "crowdloans": { "type": "github", @@ -247,16 +258,9 @@ { "disabled": false, "chainId": "e143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e", + "rank": 108, "name": "Westend", "externalApi": { - "staking": { - "type": "subquery", - "url": "https://api.subquery.network/sq/soramitsu/fearless-wallet-westend" - }, - "history": { - "type": "subquery", - "url": "https://api.subquery.network/sq/soramitsu/fearless-wallet-westend" - }, "crowdloans": { "type": "github", "url": "https://raw.githubusercontent.com/soramitsu/fearless-utils/master/crowdloan/westend.json" @@ -447,7 +451,10 @@ } ], "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/Assethub.svg", - "addressPrefix": 2 + "addressPrefix": 2, + "options": [ + "utilityFeePayment" + ] }, { "disabled": false, @@ -494,6 +501,26 @@ "priceId": "tether", "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/USDT.svg", "color": "26A17B" + }, + { + "id": "8f79aa5a-9f31-442c-ac96-01ff80b105e0", + "type": "assets", + "name": "dot is $ded", + "symbol": "ded", + "precision": 10, + "currencyId": "30", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/DED.svg", + "color": "FE0186" + }, + { + "id": "44587704-7da8-45c3-9541-be7b81de76ee", + "type": "assets", + "name": "pink", + "symbol": "pink", + "precision": 10, + "currencyId": "23", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/PINK.svg", + "color": "FF0066" } ], "xcm": { @@ -563,7 +590,10 @@ } ], "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/Assethub.svg", - "addressPrefix": 0 + "addressPrefix": 0, + "options": [ + "utilityFeePayment" + ] }, { "disabled": false, @@ -815,7 +845,10 @@ } ], "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/acala.svg", - "addressPrefix": 10 + "addressPrefix": 10, + "options": [ + "utilityFeePayment" + ] }, { "disabled": false, @@ -1296,12 +1329,16 @@ } ], "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/Karura.svg", - "addressPrefix": 8 + "addressPrefix": 8, + "options": [ + "utilityFeePayment" + ] }, { "disabled": false, "chainId": "401a1f9dca3da46f5c4091016c8a2f26dcea05865116b286f60f668207d1474b", "parentId": "b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe", + "rank": 11, "name": "Moonriver", "paraId": "2023", "externalApi": { @@ -2684,10 +2721,7 @@ ] }, - "nodes": [{ - "url": "wss://rpc.parallel.fi", - "name": "Parallel node" - }, + "nodes": [ { "url": "wss://parallel-rpc.dwellir.com", "name": "Dwellir node" @@ -2760,6 +2794,7 @@ "disabled": false, "chainId": "fe58ea77779b7abda7da4ec526d14db9b1e9cd40a217c34892af80a9b332b76d", "parentId": "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3", + "rank": 12, "paraId": "2004", "name": "Moonbeam", "externalApi": { @@ -2888,6 +2923,16 @@ "priceId": "pha", "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/PHA.svg", "color": "DAFE6F" + }, + { + "id": "6fed1ff5-3aa5-4d94-b07a-22dfc0770fc0", + "type": "assets", + "name": "pink", + "symbol": "xcpink", + "precision": 10, + "currencyId": "64174511183114006009298114091987195453", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/PINK.svg", + "color": "FF0066" } ], "xcm": { @@ -2986,6 +3031,7 @@ { "disabled": false, "chainId": "91bc6e169807aaa54802737e1c504b2577d4fafedd5a02c10293b1cd60e39527", + "rank": 112, "name": "Moonbase Alpha", "externalApi": { "staking": { @@ -4728,6 +4774,7 @@ { "disabled": false, "chainId": "3266816be9fa51b32cfea58d3e33ca77246bc9618595a4300e44c8856a8d8a17", + "rank": 100, "name": "SORA test", "externalApi": { "history": { @@ -4736,7 +4783,7 @@ }, "staking": { "type": "sora", - "url": "https://squid.subsquid.io/sora-stage/v/v4/graphql" + "url": "https://squid.subsquid.io/sora-stage/v/v5/graphql" } }, "xcm": { @@ -4770,7 +4817,11 @@ "color": "EE2233", "isUtility": true, "type": "soraAsset", - "staking": "relaychain" + "staking": "relaychain", + "priceProvider": { + "type": "sorasubquery", + "id": "0x0200000000000000000000000000000000000000000000000000000000000000" + } }, { "id": "0ecacd48-ffd4-4a2e-87e3-c5f72f9a9877", @@ -4921,25 +4972,15 @@ "type": "soraAsset" } ], - "nodes": [{ - "url": "wss://ws.framenode-1.s1.stg1.sora2.soramitsu.co.jp", - "name": "Sora Stage Node #1" - }, - { - "url": "wss://ws.framenode-2.s1.stg1.sora2.soramitsu.co.jp", - "name": "Sora Stage Node #2" - }, - { - "url": "wss://ws.framenode-3.s2.stg1.sora2.soramitsu.co.jp", - "name": "Sora Stage Node #3" - }, + "nodes": [ + { - "url": "wss://ws.framenode-4.s2.stg1.sora2.soramitsu.co.jp", - "name": "Sora Stage Node #4" + "url": "wss://ws.framenode-7.s4.stg1.sora2.soramitsu.co.jp", + "name": "Sora Stage #7" }, { - "url": "wss://ws.framenode-5.s3.stg1.sora2.soramitsu.co.jp", - "name": "Sora Stage Node #5" + "url": "wss://ws.framenode-8.s5.stg1.sora2.soramitsu.co.jp", + "name": "Sora Stage #8" }, { "url": "wss://ws.framenode-1.r0.dev.sora2.soramitsu.co.jp", @@ -4969,11 +5010,15 @@ "externalApi": { "history": { "type": "sora", - "url": "https://squid.subsquid.io/sora/v/v4/graphql" + "url": "https://squid.subsquid.io/sora/v/v5/graphql" }, "staking": { "type" : "sora", - "url": "https://squid.subsquid.io/sora/v/v4/graphql" + "url": "https://squid.subsquid.io/sora/v/v5/graphql" + }, + "pricing": { + "type": "sora", + "url": "https://api.subquery.network/sq/sora-xor/sora-prod" }, "explorers": [{ "type": "subscan", @@ -4995,7 +5040,11 @@ "color": "EE2233", "isUtility": true, "type": "soraAsset", - "staking": "relaychain" + "staking": "relaychain", + "priceProvider": { + "type": "sorasubquery", + "id": "0x0200000000000000000000000000000000000000000000000000000000000000" + } }, { "id": "24d0809e-0a4c-42ea-bdd8-dc7a518f389c", @@ -5007,7 +5056,11 @@ "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/VAL.svg", "color": "F3B966", "type": "soraAsset", - "isNative": true + "isNative": true, + "priceProvider": { + "type": "sorasubquery", + "id": "0x0200040000000000000000000000000000000000000000000000000000000000" + } }, { "id": "37a999a2-5e90-4448-8b0e-98d06ac8f9d4", @@ -5019,7 +5072,11 @@ "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/PSWAP.svg", "color": "FF0066", "type": "soraAsset", - "isNative": true + "isNative": true, + "priceProvider": { + "type": "sorasubquery", + "id": "0x0200050000000000000000000000000000000000000000000000000000000000" + } }, { "id": "75a0bc9b-a1fb-446e-8781-621036bfd979", @@ -5031,7 +5088,11 @@ "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/XST.svg", "color": "EE2233", "type": "soraAsset", - "isNative": true + "isNative": true, + "priceProvider": { + "type": "sorasubquery", + "id": "0x0200090000000000000000000000000000000000000000000000000000000000" + } }, { "id": "217925d8-c529-4480-98e5-b8bf651129ef", @@ -5043,7 +5104,11 @@ "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/XSTUSD.svg", "color": "EE2233", "type": "soraAsset", - "isNative": true + "isNative": true, + "priceProvider": { + "type": "sorasubquery", + "id": "0x0200080000000000000000000000000000000000000000000000000000000000" + } }, { "id": "5e3de486-789e-4e47-8f49-870852cfebb6", @@ -5055,7 +5120,11 @@ "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/DEO.svg", "color": "54B198", "type": "soraAsset", - "isNative": true + "isNative": true, + "priceProvider": { + "type": "sorasubquery", + "id": "0x00f2f4fda40a4bf1fc3769d156fa695532eec31e265d75068524462c0b80f674" + } }, { "id": "8fe0cbf4-7ece-45f6-968b-5c1b77accff0", @@ -5067,7 +5136,11 @@ "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/CERES.svg", "color": "243579", "type": "soraAsset", - "isNative": true + "isNative": true, + "priceProvider": { + "type": "sorasubquery", + "id": "0x008bcfd2387d3fc453333557eecb0efe59fcba128769b2feefdd306e98e66440" + } }, { "id": "079f2a74-1440-440c-b826-6d85a7dd3a91", @@ -5079,7 +5152,11 @@ "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/NOIR.svg", "color": "A0A7FF", "type": "soraAsset", - "isNative": true + "isNative": true, + "priceProvider": { + "type": "sorasubquery", + "id": "0x0044aee0776cfb826434af8ef0f8e2c7e9e6644cfda0ae0f02c471b1eebc2483" + } }, { "id": "2de2b668-33cd-4e85-a501-4921481e618f", @@ -5090,7 +5167,11 @@ "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/UMI.svg", "color": "3C7EB2", "type": "soraAsset", - "isNative": true + "isNative": true, + "priceProvider": { + "type": "sorasubquery", + "id": "0x003252667a82d2dd70fa046eea663eaec1f2e37c20879f113b880b04c5ebd805" + } }, { "id": "4c7b8da9-b297-4093-b8bc-28d477e7b5ad", @@ -5155,7 +5236,11 @@ "precision": 18, "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/SHIB.svg", "color": "FFA409", - "type": "soraAsset" + "type": "soraAsset", + "priceProvider": { + "type": "sorasubquery", + "id": "0x005aa73d7a4a3fdbe830c7d0ee26c09ba7f1db119da86d5b4dcb6609dac5ceb5" + } }, { "id": "68a46965-94e8-4a03-af65-6237f83d482f", @@ -5166,7 +5251,11 @@ "icon": "https://raw.githubusercontent.com/soramitsu/fearless-utils/master/icons/tokens/coloured/TBCD.svg", "color":"6D8954", "type": "soraAsset", - "isNative": true + "isNative": true, + "priceProvider": { + "type": "sorasubquery", + "id": "0x02000a0000000000000000000000000000000000000000000000000000000000" + } }, { "id": "61f864b5-9fe0-4ba5-b5b6-c338ceaeee91", @@ -5177,7 +5266,11 @@ "icon": "https://raw.githubusercontent.com/soramitsu/fearless-utils/master/icons/tokens/coloured/HMX.svg", "color":"FFFFFF", "type": "soraAsset", - "isNative": true + "isNative": true, + "priceProvider": { + "type": "sorasubquery", + "id": "0x002d4e9e03f192cc33b128319a049f353db98fbf4d98f717fd0b7f66a0462142" + } }, { "id": "a13169a1-32fa-4b44-aecb-c404c5f3cdbc", @@ -5187,7 +5280,11 @@ "precision": 18, "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/BCSD.svg", "color": "FFFFFF", - "type": "soraAsset" + "type": "soraAsset", + "priceProvider": { + "type": "sorasubquery", + "id": "0x00eacaea6599a04358fda986388ef0bb0c17a553ec819d5de2900c0af0862502" + } }, { "id": "5416b261-a759-4ba6-bc83-ea79a83c5101", @@ -5199,6 +5296,17 @@ "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/KSM.svg", "color": "FFFFFF", "type": "soraAsset" + }, + { + "id": "cd092a5a-4eb6-4318-9f11-4bf8454d67a2", + "name": "polkadot", + "symbol": "dot", + "currencyId": "0x0003b1dbee890acfb1b3bc12d1bb3b4295f52755423f84d1751b2545cebf000b", + "precision": 18, + "priceId": "polkadot", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/DOT.svg", + "color": "FF0066", + "type": "soraAsset" } ], "xcm": { @@ -5207,6 +5315,10 @@ { "id": "5416b261-a759-4ba6-bc83-ea79a83c5101", "symbol": "KSM" + }, + { + "id": "cd092a5a-4eb6-4318-9f11-4bf8454d67a2", + "symbol": "DOT" } ], "availableDestinations": [ @@ -5218,6 +5330,15 @@ "symbol": "KSM" } ] + }, + { + "chainId": "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3", + "assets": [ + { + "id": "cd092a5a-4eb6-4318-9f11-4bf8454d67a2", + "symbol": "DOT" + } + ] } ] }, @@ -5519,6 +5640,7 @@ "disabled": false, "chainId": "5d3c298622d5634ed019bf61ea4b71655030015bde9beb0d6a24743714462c86", "parentId": "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3", + "rank": 10, "paraId": "2094", "name": "Pendulum", "externalApi": { @@ -5609,7 +5731,11 @@ "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/XOR.svg", "color": "EE2233", "isUtility": true, - "type": "normal" + "type": "normal", + "priceProvider": { + "type": "sorasubquery", + "id": "0x0200000000000000000000000000000000000000000000000000000000000000" + } }], "nodes": [{ "url": "wss://ws.parachain-collator-1.c1.sora2.soramitsu.co.jp", @@ -5618,6 +5744,34 @@ "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/SORA.svg", "addressPrefix": 420 }, + { + "disabled": false, + "chainId": "e92d165ad41e41e215d09713788173aecfdbe34d3bed29409d33a2ef03980738", + "parentId": "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3", + "paraId": "2025", + "name": "SORA Polkadot parachain", + "assets": [{ + "id": "a7c696d7-42ce-4ed6-a036-4f0f76386c49", + "name": "sora", + "symbol": "xor", + "precision": 18, + "priceId": "sora", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/XOR.svg", + "color": "EE2233", + "isUtility": true, + "type": "normal", + "priceProvider": { + "type": "sorasubquery", + "id": "0x0200000000000000000000000000000000000000000000000000000000000000" + } + }], + "nodes": [{ + "url": "wss://ws.parachain-collator-1.pc1.sora2.soramitsu.co.jp", + "name": "Soramitsu node" + }], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/SORA.svg", + "addressPrefix": 81 + }, { "disabled": false, "chainId": "1", @@ -5786,7 +5940,11 @@ "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/XOR.svg", "color": "EE2233", "type": "normal", - "ethereumType": "erc20" + "ethereumType": "erc20", + "priceProvider": { + "type": "sorasubquery", + "id": "0x0200000000000000000000000000000000000000000000000000000000000000" + } }, { "isUtility": false, @@ -5846,6 +6004,7 @@ { "disabled": false, "chainId": "5", + "rank": 101, "name": "Ethereum Goerli", "externalApi": { "explorers": [{ @@ -5913,6 +6072,7 @@ "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/ETH.svg", "addressPrefix": 0, "options": [ + "testnet", "ethereum", "utilityFeePayment" ] @@ -6105,6 +6265,7 @@ { "disabled": false, "chainId": "97", + "rank": 102, "name": "BNB Smart Chain Testnet", "externalApi": { "history": { @@ -6161,8 +6322,9 @@ "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/bnbchain.svg", "addressPrefix": 0, "options": [ - "ethereum", - "utilityFeePayment" + "testnet", + "ethereum", + "utilityFeePayment" ] }, { @@ -6171,8 +6333,8 @@ "name": "Sepolia", "externalApi": { "history": { - "type": "alchemy", - "url": "https://eth-sepolia.g.alchemy.com/v2/16bz4S4bROJsWH-eDpiJ21cx_c-KAAaP" + "type": "etherscan", + "url": "https://api-sepolia.etherscan.io/api" }, "explorers": [{ "type": "etherscan", @@ -6223,13 +6385,15 @@ "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/ETH.svg", "addressPrefix": 0, "options": [ - "ethereum", - "utilityFeePayment" + "testnet", + "ethereum", + "utilityFeePayment" ] }, { "disabled": false, "chainId": "137", + "rank": 3, "name": "Polygon", "externalApi": { "explorers": [ @@ -6254,7 +6418,7 @@ "name": "polygon", "symbol": "matic", "precision": 18, - "priceId": "matic", + "priceId": "matic-network", "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/MATIC.svg", "color": "8247E5", "type": "normal", @@ -6395,6 +6559,7 @@ { "disabled": false, "chainId": "80001", + "rank": 103, "name": "Polygon mumbai testnet", "externalApi": { "explorers": [ @@ -6448,27 +6613,16 @@ { "disabled": false, "chainId": "6408de7737c59c238890533af25896a2c20608d8b380bb01029acb392781063e", + "rank": 109, "name": "Rococo", "externalApi": { - "staking": { - "type": "subquery", - "url": "https://api.subquery.network/sq/soramitsu/fearless-wallet-westend" - }, - "history": { - "type": "subquery", - "url": "https://api.subquery.network/sq/soramitsu/fearless-wallet-westend" - }, - "crowdloans": { - "type": "github", - "url": "https://raw.githubusercontent.com/soramitsu/fearless-utils/master/crowdloan/westend.json" - }, "explorers": [{ "type": "subscan", "types": [ "extrinsic", "account" ], - "url": "https://westend.subscan.io/{type}/{value}" + "url": "https://rococo.subscan.io/{type}/{value}" }] }, "xcm": { @@ -6620,12 +6774,15 @@ ], "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/Enjin.svg", - "addressPrefix": 9030 + "addressPrefix": 9030, + "options": [ + "testnet" + ] }, { "disabled": false, "chainId": "42161", - "rank": 13, + "rank": 4, "name": "Arbitrum One", "externalApi": { "history": { @@ -6690,7 +6847,7 @@ { "disabled": false, "chainId": "10", - "rank": 14, + "rank": 5, "name": "OP Mainnet", "externalApi": { "history": { @@ -6762,7 +6919,7 @@ { "disabled": false, "chainId": "43114", - "rank": 16, + "rank": 6, "name": "Avalanche C-Chain", "externalApi": { "history": { @@ -6814,7 +6971,7 @@ { "disabled": false, "chainId": "195", - "rank": 18, + "rank": 13, "name": "X1 testnet", "externalApi": { "history": { @@ -6865,7 +7022,7 @@ { "disabled": false, "chainId": "1101", - "rank": 20, + "rank": 7, "name": "Polygon zkEVM", "externalApi": { "history": { @@ -6941,7 +7098,7 @@ { "disabled": false, "chainId": "7001", - "rank": 17, + "rank": 14, "name": "ZetaChain Testnet", "assets": [ { @@ -6954,26 +7111,14 @@ "color": "235643", "type": "normal", "ethereumType": "normal" - }, - { - "isUtility": false, - "id": "0x48f80608B672DC30DC7e3dbBd0343c5F02C738Eb", - "name": "MATIC-mumbai_testnet", - "symbol": "matic", - "precision": 18, - "priceId": "tMATIC", - "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/MATIC.svg", - "color": "8247E5", - "type": "normal", - "ethereumType": "erc20" } ], "externalApi": { "history": { - "type": "zeta", - "url": "https://zetachain-athens-3.blockscout.com/api/v2/addresses/" + "type": "zeta", + "url": "https://zetachain-athens-3.blockscout.com/api/v2/addresses/" } - }, + }, "nodes": [ { "url": "https://rpc.ankr.com/zetachain_evm_athens_testnet", @@ -7023,6 +7168,7 @@ "explorers": [{ "type": "reef", "types": [ + "transfer", "extrinsic", "account" ], @@ -7035,7 +7181,742 @@ } ], - "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/white/reefchain.svg", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/reefchain.svg", "addressPrefix": 42 - } + }, + { + "disabled": false, + "chainId": "f3c7ad88f6a80f366c4be216691411ef0622e8b809b1046ea297ef106058d4eb", + "name": "Manta Parachain", + "assets": [{ + "id": "0c41f3da-3c42-413a-aca3-bfb19b717df7", + "name": "manta", + "symbol": "manta", + "precision": 18, + "priceId": "manta-network", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/MANTA.svg", + "color": "29CCB9", + "isUtility": true, + "type": "normal" + }], + "externalApi": { + "explorers": [{ + "type": "subscan", + "types": [ + "extrinsic", + "account" + ], + "url": "https://manta.subscan.io/{type}/{value}" + }] + }, + "nodes": [{ + "url": "wss://ws.manta.systems", + "name": "Manta Community node" + } + + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/mantachain.svg", + "addressPrefix": 77 + }, + { + "disabled": false, + "chainId": "169", + "name": "Manta Pacific Mainnet", + "assets": [ + { + "isUtility": true, + "id": "af44954d-2be1-4021-ae0b-f05d8180400a", + "name": "ethereum", + "symbol": "eth", + "precision": 18, + "priceId": "ethereum", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/ETH.svg", + "color":"627EEA", + "type": "normal", + "ethereumType": "normal" + }, + { + "isUtility": false, + "id": "0x95CeF13441Be50d20cA4558CC0a27B601aC544E5", + "name": "manta", + "symbol": "manta", + "precision": 18, + "priceId": "manta-network", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/MANTA.svg", + "color": "8247E5", + "type": "normal", + "ethereumType": "erc20" + } + ], + "externalApi": { + "history": { + "type": "zeta", + "url": "https://pacific-explorer.manta.network/api/v2/addresses/" + } + }, + "nodes": [ + { + "url": "https://pacific-rpc.manta.network/http", + "name": "Manta https node" + }, + { + "url": "https://1rpc.io/manta", + "name": "1RPC https node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/mantachain.svg", + "addressPrefix": 0, + "options": [ + "ethereum", + "utilityFeePayment" + ] + }, + { + "disabled": true, + "chainId": "d8761d3c88f26dc12875c00d3165f7d67243d56fc85b4cf19937601a7916e5a9", + "name": "Enjin Relaychain", + "externalApi": { + "explorers": [{ + "type": "subscan", + "types": [ + "extrinsic", + "account" + ], + "url": "https://enjin.subscan.io/{type}/{value}" + }] + }, + "assets": [{ + "id": "43748d94-90ba-41b2-8732-326cd943a501", + "name": "enjin coin", + "symbol": "enj", + "precision": 18, + "priceId": "enjincoin", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/ENJ.svg", + "color": "5A27ED", + "isUtility": true, + "type": "normal" + }], + "nodes": [{ + "url": "wss://rpc.relay.blockchain.enjin.io", + "name": "Enjin Foundation node" + } + + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/Enjin.svg", + "addressPrefix": 2135 + }, + { + "disabled": false, + "chainId": "6bd89e052d67a45bb60a9a23e8581053d5e0d619f15cb9865946937e690c42d6", + "name": "Liberland", + "assets": [ + { + "id": "a6b83d39-a488-4b34-8352-280705a792ea", + "name": "liberland lld", + "symbol": "lld", + "precision": 12, + "priceId": "liberland-lld", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/LLD.svg", + "color": "00437F", + "isUtility": true, + "type": "normal" + } + ], + "nodes": [ + { + "url": "wss://mainnet.liberland.org", + "name": "Liberland node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/liberland.svg", + "addressPrefix": 42 + }, + { + "disabled": false, + "chainId": "248", + "rank": 15, + "name": "Oasys Mainnet", + "externalApi": { + "history": { + "type": "etherscan", + "url": "https://explorer.oasys.games/api" + }, + "explorers": [ + { + "type": "etherscan", + "types": [ + "tx", + "address" + ], + "url": "https://explorer.oasys.games/{type}/{value}" + } + ] + }, + "assets": [ + { + "isUtility": true, + "id": "11d2ece1-ecae-4596-aae4-ee9db83a5e2a", + "name": "oasys", + "symbol": "oas", + "precision": 18, + "priceId": "oasys", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/OAS.svg", + "color": "00A84F", + "type": "normal", + "ethereumType": "normal" + } + ], + "nodes": [ + { + "url": "https://oasys.blockpi.network/v1/rpc/public/", + "name": "Blockpi https node" + }, + { + "url": "https://rpc.mainnet.oasys.games/", + "name": "Oasys https node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/oasys.svg", + "addressPrefix": 0, + "options": [ + "ethereum", + "utilityFeePayment" + ] + }, + { + "disabled": false, + "chainId": "29548", + "rank": 150, + "name": "MCH Verse Mainnet", + "externalApi": { + "history": { + "type": "etherscan", + "url": "https://explorer.oasys.mycryptoheroes.net/api" + }, + "explorers": [ + { + "type": "etherscan", + "types": [ + "tx", + "address" + ], + "url": "https://explorer.oasys.mycryptoheroes.net/{type}/{value}" + } + ] + }, + "assets": [ + { + "isUtility": true, + "id": "dad38228-7e66-46cb-b8f8-bee5a738a18a", + "name": "oasys", + "symbol": "oas", + "precision": 18, + "priceId": "oasys", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/OAS.svg", + "color": "00A84F", + "type": "normal", + "ethereumType": "normal" + } + ], + "nodes": [ + { + "url": "https://rpc.oasys.mycryptoheroes.net/", + "name": "MCH https node" + }, + { + "url": "wss://ws.oasys.mycryptoheroes.net/", + "name": "MCH wss node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/mchverse.svg", + "addressPrefix": 0, + "options": [ + "ethereum", + "utilityFeePayment" + ] + }, + { + "disabled": false, + "chainId": "2400", + "rank": 151, + "name": "TCG Verse Mainnet", + "externalApi": { + "history": { + "type": "etherscan", + "url": "https://explorer.tcgverse.xyz/api" + }, + "explorers": [ + { + "type": "etherscan", + "types": [ + "tx", + "address" + ], + "url": "https://explorer.tcgverse.xyz/{type}/{value}" + } + ] + }, + "assets": [ + { + "isUtility": true, + "id": "e2661f7b-3916-464d-8ea2-5650b6c7de94", + "name": "oasys", + "symbol": "oas", + "precision": 18, + "priceId": "oasys", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/OAS.svg", + "color": "00A84F", + "type": "normal", + "ethereumType": "normal" + } + ], + "nodes": [ + { + "url": "https://rpc.tcgverse.xyz/", + "name": "TCG https node" + }, + { + "url": "wss://ws-rpc.tcgverse.xyz/", + "name": "TCG wss node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/tcgverse.svg", + "addressPrefix": 0, + "options": [ + "ethereum", + "utilityFeePayment" + ] + }, + { + "disabled": false, + "chainId": "19011", + "rank": 152, + "name": "HOME Verse Mainnet", + "externalApi": { + "history": { + "type": "etherscan", + "url": "https://explorer.oasys.homeverse.games/api" + }, + "explorers": [ + { + "type": "etherscan", + "types": [ + "tx", + "address" + ], + "url": "https://explorer.oasys.homeverse.games/{type}/{value}" + } + ] + }, + "assets": [ + { + "isUtility": true, + "id": "943d4fe9-76e5-48bd-9220-4fcda4e3b8e4", + "name": "oasys", + "symbol": "oas", + "precision": 18, + "priceId": "oasys", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/OAS.svg", + "color": "00A84F", + "type": "normal", + "ethereumType": "normal" + } + ], + "nodes": [ + { + "url": "https://rpc.mainnet.oasys.homeverse.games/", + "name": "HOME https node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/homeverse.svg", + "addressPrefix": 0, + "options": [ + "ethereum", + "utilityFeePayment" + ] + }, + { + "disabled": false, + "chainId": "5555", + "rank": 153, + "name": "Chain Verse Mainnet", + "externalApi": { + "history": { + "type": "etherscan", + "url": "https://explorer.chainverse.info/api" + }, + "explorers": [ + { + "type": "etherscan", + "types": [ + "tx", + "address" + ], + "url": "https://explorer.chainverse.info/{type}/{value}" + } + ] + }, + "assets": [ + { + "isUtility": true, + "id": "f1eeb4f0-d87d-41ef-8e23-af5a535b793c", + "name": "oasys", + "symbol": "oas", + "precision": 18, + "priceId": "oasys", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/OAS.svg", + "color": "00A84F", + "type": "normal", + "ethereumType": "normal" + } + ], + "nodes": [ + { + "url": "https://rpc.chainverse.info/", + "name": "Chain https node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/chainverse.svg", + "addressPrefix": 0, + "options": [ + "ethereum", + "utilityFeePayment" + ] + }, + { + "disabled": false, + "chainId": "7225878", + "rank": 154, + "name": "Saakuru Verse Mainnet", + "externalApi": { + "history": { + "type": "etherscan", + "url": "https://explorer.saakuru.network/api" + }, + "explorers": [ + { + "type": "etherscan", + "types": [ + "tx", + "address" + ], + "url": "https://explorer.saakuru.network/{type}/{value}" + } + ] + }, + "assets": [ + { + "isUtility": true, + "id": "5e15fa7a-b398-49a6-8459-da4b4f660f32", + "name": "oasys", + "symbol": "oas", + "precision": 18, + "priceId": "oasys", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/OAS.svg", + "color": "00A84F", + "type": "normal", + "ethereumType": "normal" + } + ], + "nodes": [ + { + "url": "https://rpc.saakuru.network/", + "name": "Saakuru https node" + }, + { + "url": "wss://ws.saakuru.network/", + "name": "Saakuru wss node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/saakuruverse.svg", + "addressPrefix": 0, + "options": [ + "ethereum", + "utilityFeePayment" + ] + }, + { + "disabled": false, + "chainId": "50005", + "rank": 155, + "name": "Yooldo Verse Mainnet", + "externalApi": { + "history": { + "type": "etherscan", + "url": "https://explorer.yooldo-verse.xyz/api" + }, + "explorers": [ + { + "type": "etherscan", + "types": [ + "tx", + "address" + ], + "url": "https://explorer.yooldo-verse.xyz/{type}/{value}" + } + ] + }, + "assets": [ + { + "isUtility": true, + "id": "648d069c-c32f-4fbc-af23-14f77a9158aa", + "name": "oasys", + "symbol": "oas", + "precision": 18, + "priceId": "oasys", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/OAS.svg", + "color": "00A84F", + "type": "normal", + "ethereumType": "normal" + } + ], + "nodes": [ + { + "url": "https://rpc.yooldo-verse.xyz/", + "name": "Yooldo https node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/yooldoverse.svg", + "addressPrefix": 0, + "options": [ + "ethereum", + "utilityFeePayment" + ] + }, + { + "disabled": false, + "chainId": "30", + "rank": 16, + "name": "Rootstock Mainnet", + "externalApi": { + "history": { + "type": "zeta", + "url": "https://rootstock.blockscout.com//api/v2/addresses/" + } + }, + "assets": [ + { + "isUtility": true, + "id": "defb1fb8-8cf1-49b1-8dd3-b8dac33394a4", + "name": "rootstock rsk rbtc", + "symbol": "rbtc", + "precision": 18, + "priceId": "rootstock", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/RBTC.svg", + "color": "", + "type": "normal", + "ethereumType": "normal" + } + ], + "nodes": [ + { + "url": "https://public-node.rsk.co/", + "name": "Public https node" + }, + { + "url": "https://mycrypto.rsk.co/", + "name": "Mycrypto https node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/rootstock.svg", + "addressPrefix": 0, + "options": [ + "ethereum", + "utilityFeePayment" + ] + }, + { + "disabled": false, + "chainId": "50dd5d206917bf10502c68fb4d18a59fc8aa31586f4e8856b493e43544aa82aa", + "name": "XX network", + "externalApi": { + "history": { + "type": "subquery", + "url": "https://api.subquery.network/sq/nova-wallet/nova-wallet-xx-network" + } + }, + "assets": [{ + "id": "526dca29-63ff-4683-88d6-852d1455b17b", + "name": "xx network", + "symbol": "xx", + "precision": 9, + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/XX.svg", + "priceId": "xxcoin", + "color": "0AC1C7", + "isUtility": true, + "type": "normal" + }], + "nodes": [ + { + "url": "wss://xxnetwork-rpc.dwellir.com", + "name": "Dwellir node" + }, + { + "url": "wss://rpc.xx.network", + "name": "xx Foundation node #1" + }, + { + "url": "wss://rpc-hetzner.xx.network", + "name": "xx Foundation node #2" + }, + { + "url": "wss://rpc-do.xx.network", + "name": "xx Foundation node #3" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/xxnetwork.svg", + "addressPrefix": 55 + }, + { + "disabled": false, + "chainId": "6660", + "name": "Latest Testnet", + "externalApi": { + "history": { + "type": "zeta", + "url": "https://testscan.latestchain.io/api/v2/addresses/" + } + }, + "assets": [ + { + "isUtility": true, + "id": "1a31227d-c6fe-4c78-a3d6-353be70e56a6", + "name": "latest", + "symbol": "latest", + "precision": 18, + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/LATEST.svg", + "color": "FC81EA", + "type": "normal", + "ethereumType": "normal" + } + + ], + "nodes": [ + { + "url": "https://testnet-rpc.latestchain.io", + "name": "Latest https node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/latestchain.svg", + "addressPrefix": 0, + "options": [ + "testnet", + "ethereum", + "utilityFeePayment" + ] + }, + { + "disabled": false, + "chainId": "9630", + "name": "Latest Mainnet", + "externalApi": { + "history": { + "type": "zeta", + "url": "https://scan.latestchain.io/api/v2/addresses/" + } + }, + "assets": [ + { + "isUtility": true, + "id": "e7094e62-d86d-4c08-8497-9ebc4c9011ea", + "name": "latest", + "symbol": "latest", + "precision": 18, + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/LATEST.svg", + "color": "FC81EA", + "type": "normal", + "ethereumType": "normal" + } + + ], + "nodes": [ + { + "url": "https://mainnet-rpc.latestchain.io", + "name": "Latest https node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/latest.svg", + "addressPrefix": 0, + "options": [ + "ethereum", + "utilityFeePayment" + ] + }, + { + "disabled": false, + "chainId": "72778", + "name": "CAGA Ankara Testnet", + "externalApi": { + "history": { + "type": "etherscan", + "url": "https://explorer.ankara-cagacrypto.com/api" + } + }, + "assets": [ + { + "isUtility": true, + "id": "a9270ef5-379a-4228-8d72-e6efb2b5f7b4", + "name": "caga", + "symbol": "caga", + "precision": 18, + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/CAGA.svg", + "color": "FFFFFF", + "type": "normal", + "ethereumType": "normal" + } + + ], + "nodes": [ + { + "url": "wss://wss.ankara-cagacrypto.com", + "name": "Caga wss node" + }, + { + "url": "https://www.ankara-cagacrypto.com", + "name": "Caga https node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/cagachain.svg", + "addressPrefix": 0, + "options": [ + "testnet", + "ethereum", + "utilityFeePayment" + ] + }, + { + "disabled": false, + "chainId": "6f09966420b2608d1947ccfb0f2a362450d1fc7fd902c29b67c906eaa965a7ae", + "name": "Avail Goldberg Testnet", + "externalApi": { + "explorers": [{ + "type": "subscan", + "types": [ + "extrinsic", + "account" + ], + "url": "https://avail-testnet.subscan.io/{type}/{value}" + }] + }, + "assets": [ + { + "id": "72778e65-53b6-4cb4-bb4c-c029121eb494", + "name": "avail token", + "symbol": "avl", + "precision": 18, + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/AVL.svg", + "color": "56E5FF", + "isUtility": true, + "type": "normal" + } + ], + "nodes": [{ + "url": "wss://goldberg-testnet-rpc.avail.tools/ws", + "name": "Avail Goldberg Testnet node" + } + + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/white/Avail.svg", + "addressPrefix": 42 + } ] diff --git a/fearlessTests/Modules/LiquidityPoolDetails/LiquidityPoolDetailsTests.swift b/fearlessTests/Modules/LiquidityPoolDetails/LiquidityPoolDetailsTests.swift new file mode 100644 index 0000000000..acb9afe2ff --- /dev/null +++ b/fearlessTests/Modules/LiquidityPoolDetails/LiquidityPoolDetailsTests.swift @@ -0,0 +1,16 @@ +import XCTest + +class LiquidityPoolDetailsTests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + XCTFail("Did you forget to add tests?") + } +} diff --git a/fearlessTests/Modules/LiquidityPoolsOverview/LiquidityPoolsOverviewTests.swift b/fearlessTests/Modules/LiquidityPoolsOverview/LiquidityPoolsOverviewTests.swift new file mode 100644 index 0000000000..9f3e8fdebd --- /dev/null +++ b/fearlessTests/Modules/LiquidityPoolsOverview/LiquidityPoolsOverviewTests.swift @@ -0,0 +1,16 @@ +import XCTest + +class LiquidityPoolsOverviewTests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + XCTFail("Did you forget to add tests?") + } +} From 8419744ab65f3e165ce6adb42524163a00d9afa8 Mon Sep 17 00:00:00 2001 From: Alex Lebedko Date: Tue, 14 May 2024 16:04:25 +0700 Subject: [PATCH 006/115] lp details ui --- .../xcshareddata/swiftpm/Package.resolved | 8 +- .../LiquidityPoolDetailsAssembly.swift | 6 +- .../LiquidityPoolDetailsInteractor.swift | 2 + .../LiquidityPoolDetailsPresenter.swift | 7 +- .../LiquidityPoolDetailsViewController.swift | 7 +- .../LiquidityPoolDetailsViewLayout.swift | 118 +++++++++++++++++- .../LiquidityPoolsListViewController.swift | 2 +- 7 files changed, 139 insertions(+), 11 deletions(-) diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index de6bdd306a..2cb5285b8d 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/google-api-objectivec-client-for-rest.git", "state" : { - "revision" : "11941123a0d9c2641197fa83d57e6ff9f088b907", - "version" : "3.5.2" + "revision" : "d46fb27cf61e08285a727c18a2ae0dbc20d91b2f", + "version" : "3.5.4" } }, { @@ -221,8 +221,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/nicklockwood/SwiftFormat", "state" : { - "revision" : "ab238886b8b50f8b678b251f3c28c0c887305407", - "version" : "0.53.8" + "revision" : "05cb325003a673b3d177c711b3bc909cfee07622", + "version" : "0.53.9" } }, { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsAssembly.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsAssembly.swift index 63c54ce246..5f2e1fd643 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsAssembly.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsAssembly.swift @@ -4,16 +4,16 @@ import SoraFoundation final class LiquidityPoolDetailsAssembly { static func configureModule() -> LiquidityPoolDetailsModuleCreationResult? { let localizationManager = LocalizationManager.shared - + let interactor = LiquidityPoolDetailsInteractor() let router = LiquidityPoolDetailsRouter() - + let presenter = LiquidityPoolDetailsPresenter( interactor: interactor, router: router, localizationManager: localizationManager ) - + let view = LiquidityPoolDetailsViewController( output: presenter, localizationManager: localizationManager diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsInteractor.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsInteractor.swift index c6df638530..94ac1e9461 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsInteractor.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsInteractor.swift @@ -2,10 +2,12 @@ import UIKit final class LiquidityPoolDetailsInteractor { // MARK: - Private properties + private weak var output: LiquidityPoolDetailsInteractorOutput? } // MARK: - LiquidityPoolDetailsInteractorInput + extension LiquidityPoolDetailsInteractor: LiquidityPoolDetailsInteractorInput { func setup(with output: LiquidityPoolDetailsInteractorOutput) { self.output = output diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift index 33312ef17d..a7fe455c31 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift @@ -3,11 +3,13 @@ import SoraFoundation final class LiquidityPoolDetailsPresenter { // MARK: Private properties + private weak var view: LiquidityPoolDetailsViewInput? private let router: LiquidityPoolDetailsRouterInput private let interactor: LiquidityPoolDetailsInteractorInput // MARK: - Constructors + init( interactor: LiquidityPoolDetailsInteractorInput, router: LiquidityPoolDetailsRouterInput, @@ -17,11 +19,12 @@ final class LiquidityPoolDetailsPresenter { self.router = router self.localizationManager = localizationManager } - + // MARK: - Private methods } // MARK: - LiquidityPoolDetailsViewOutput + extension LiquidityPoolDetailsPresenter: LiquidityPoolDetailsViewOutput { func didLoad(view: LiquidityPoolDetailsViewInput) { self.view = view @@ -30,9 +33,11 @@ extension LiquidityPoolDetailsPresenter: LiquidityPoolDetailsViewOutput { } // MARK: - LiquidityPoolDetailsInteractorOutput + extension LiquidityPoolDetailsPresenter: LiquidityPoolDetailsInteractorOutput {} // MARK: - Localizable + extension LiquidityPoolDetailsPresenter: Localizable { func applyLocalization() {} } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewController.swift index 0701addf5d..cf3e4da6c0 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewController.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewController.swift @@ -5,9 +5,11 @@ final class LiquidityPoolDetailsViewController: UIViewController, ViewHolder { typealias RootViewType = LiquidityPoolDetailsViewLayout // MARK: Private properties + private let output: LiquidityPoolDetailsViewOutput // MARK: - Constructor + init( output: LiquidityPoolDetailsViewOutput, localizationManager: LocalizationManagerProtocol? @@ -23,6 +25,7 @@ final class LiquidityPoolDetailsViewController: UIViewController, ViewHolder { } // MARK: - Life cycle + override func loadView() { view = LiquidityPoolDetailsViewLayout() } @@ -31,14 +34,16 @@ final class LiquidityPoolDetailsViewController: UIViewController, ViewHolder { super.viewDidLoad() output.didLoad(view: self) } - + // MARK: - Private methods } // MARK: - LiquidityPoolDetailsViewInput + extension LiquidityPoolDetailsViewController: LiquidityPoolDetailsViewInput {} // MARK: - Localizable + extension LiquidityPoolDetailsViewController: Localizable { func applyLocalization() {} } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift index b6be4d5660..9d9a4edc01 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift @@ -1,13 +1,129 @@ import UIKit final class LiquidityPoolDetailsViewLayout: UIView { + let navigationBar: BaseNavigationBar = { + let bar = BaseNavigationBar() + bar.set(.push) + bar.backButton.backgroundColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0.08) + bar.backButton.layer.cornerRadius = bar.backButton.frame.size.height / 2 + bar.backgroundColor = R.color.colorBlack19() + return bar + }() + + let contentView: ScrollableContainerView = { + let view = ScrollableContainerView() + view.stackView.isLayoutMarginsRelativeArrangement = true + view.stackView.layoutMargins = UIEdgeInsets(top: 24.0, left: 0.0, bottom: 0.0, right: 0.0) + view.stackView.spacing = UIConstants.bigOffset + return view + }() + + let doubleImageView = PolkaswapDoubleSymbolView() + let swapStubTitle: UILabel = { + let label = UILabel() + label.font = .h3Title + label.textColor = R.color.colorStrokeGray() + return label + }() + + let amountsLabel: UILabel = { + let label = UILabel() + label.font = .h3Title + label.textColor = R.color.colorWhite() + label.numberOfLines = 2 + return label + }() + + let infoBackground: TriangularedView = { + let view = TriangularedView() + view.fillColor = R.color.colorSemiBlack()! + view.highlightedFillColor = R.color.colorSemiBlack()! + view.strokeColor = R.color.colorWhite16()! + view.highlightedStrokeColor = R.color.colorWhite16()! + view.strokeWidth = 0.5 + view.shadowOpacity = 0.0 + + return view + }() + + let infoViewsStackView = UIFactory.default.createVerticalStackView(spacing: UIConstants.bigOffset) + + let reservesView: TitleMultiValueView = makeRowView() + let apyView: TitleMultiValueView = makeRowView() + let rewardTokenView: TitleMultiValueView = makeRowView() + let baseAssetPooledView: TitleMultiValueView = makeRowView() + let targetAssetPooledView: TitleMultiValueView = makeRowView() override init(frame: CGRect) { super.init(frame: frame) + + backgroundColor = R.color.colorBlack19() + drawSubviews() + setupConstraints() + applyLocalization() } @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } -} \ No newline at end of file + + override func layoutSubviews() { + super.layoutSubviews() + + navigationBar.backButton.rounded() + } + + private func drawSubviews() { + addSubview(navigationBar) + addSubview(contentView) + + contentView.stackView.addArrangedSubview(infoBackground) + infoBackground.addSubview(infoViewsStackView) + infoViewsStackView.addArrangedSubview(doubleImageView) + infoViewsStackView.addArrangedSubview(swapStubTitle) + + } + + private func setupConstraints() { + navigationBar.snp.makeConstraints { make in + make.leading.top.trailing.equalToSuperview() + } + contentView.snp.makeConstraints { make in + make.top.equalTo(navigationBar.snp.bottom) + make.leading.trailing.bottom.equalToSuperview() + } + } + + private func setupDefaultRowConstraints(for view: UIView) { + view.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(16) + } + } + + private func setupDefaultSectionConstraints(for view: UIView) { + view.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(16) + make.height.equalTo(56) + } + } + + private static func makeRowView() -> TitleMultiValueView { + let view = TitleMultiValueView() + view.titleLabel.font = .p1Paragraph + view.titleLabel.textColor = R.color.colorWhite() + view.valueTop.font = .p1Paragraph + view.valueTop.textColor = R.color.colorWhite() + view.valueBottom.font = .p2Paragraph + view.valueBottom.textColor = R.color.colorStrokeGray() + view.borderView.isHidden = true + view.equalsLabelsWidth = true + view.valueTop.lineBreakMode = .byTruncatingTail + view.valueBottom.lineBreakMode = .byTruncatingMiddle + return view + } + + private func applyLocalization() { + + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift index 7496b0b93d..c28aeee22c 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift @@ -67,7 +67,7 @@ extension LiquidityPoolsListViewController: UITableViewDataSource, UITableViewDe func tableView(_: UITableView, heightForRowAt _: IndexPath) -> CGFloat { 44 } - + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) } From 6165379cead34fe5349476c49c035ce28a5be5c4 Mon Sep 17 00:00:00 2001 From: Alex Lebedko Date: Mon, 20 May 2024 11:54:49 +0700 Subject: [PATCH 007/115] lps details --- fearless.xcodeproj/project.pbxproj | 22 ++- .../Model/WalletAssetId+Display.swift | 5 +- fearless/Common/Model/WalletAssetId.swift | 1 + .../LiquidityPoolDetailsAssembly.swift | 27 ++- .../LiquidityPoolDetailsInteractor.swift | 130 +++++++++++++ .../LiquidityPoolDetailsPresenter.swift | 175 +++++++++++++++++- .../LiquidityPoolDetailsProtocols.swift | 14 +- .../LiquidityPoolDetailsViewController.swift | 21 ++- .../LiquidityPoolDetailsViewLayout.swift | 128 +++++++++++-- .../Resources/assets.json | 0 .../Resources/chains.json | 0 .../Resources/polkadot-9370metadata | 0 .../Resources/polkaswapSettings.json | 0 .../Resources/runtime-default.json | 0 .../Resources/runtime-empty.json | 0 .../Resources/runtime-kusama.json | 0 .../Resources/runtime-polkadot.json | 0 .../Resources/runtime-rococo.json | 0 .../Resources/runtime-westend.json | 0 .../Resources/types.json | 0 .../LiquidityPoolDetailsViewModel.swift | 14 ++ ...LiquidityPoolDetailsViewModelFactory.swift | 119 ++++++++++++ ...vailableLiquidityPoolsListInteractor.swift | 2 +- ...AvailableLiquidityPoolsListPresenter.swift | 31 +++- ...leLiquidityPoolsListViewModelFactory.swift | 16 +- .../UserLiquidityPoolsListInteractor.swift | 49 ++++- .../UserLiquidityPoolsListPresenter.swift | 41 +++- ...erLiquidityPoolsListViewModelFactory.swift | 18 +- .../LiquidityPoolsListAssembly.swift | 14 +- .../LiquidityPoolsListProtocols.swift | 21 ++- .../LiquidityPoolsListRouter.swift | 14 +- .../LiquidityPoolsListViewController.swift | 7 + .../LiquidityPoolsListViewLayout.swift | 22 ++- .../LiquidityPoolListCellModel.swift | 3 + .../ViewModel/LiquidityPoolListType.swift | 6 + .../LiquidityPoolListViewModel.swift | 2 + .../LiquidityPoolsOverviewAssembly.swift | 25 ++- .../LiquidityPoolsOverviewPresenter.swift | 20 +- .../LiquidityPoolsOverviewProtocols.swift | 17 +- .../LiquidityPoolsOverviewRouter.swift | 39 +++- .../LiquidityPoolsOverviewViewLayout.swift | 25 ++- .../MainTabBar/MainTabBarWireframe.swift | 12 +- .../ChainAccount/ChainAccountWireframe.swift | 2 +- 43 files changed, 956 insertions(+), 86 deletions(-) rename fearless/{ => Modules/LiquidityPools/LiquidityPoolDetails}/Resources/assets.json (100%) rename fearless/{ => Modules/LiquidityPools/LiquidityPoolDetails}/Resources/chains.json (100%) rename fearless/{ => Modules/LiquidityPools/LiquidityPoolDetails}/Resources/polkadot-9370metadata (100%) rename fearless/{ => Modules/LiquidityPools/LiquidityPoolDetails}/Resources/polkaswapSettings.json (100%) rename fearless/{ => Modules/LiquidityPools/LiquidityPoolDetails}/Resources/runtime-default.json (100%) rename fearless/{ => Modules/LiquidityPools/LiquidityPoolDetails}/Resources/runtime-empty.json (100%) rename fearless/{ => Modules/LiquidityPools/LiquidityPoolDetails}/Resources/runtime-kusama.json (100%) rename fearless/{ => Modules/LiquidityPools/LiquidityPoolDetails}/Resources/runtime-polkadot.json (100%) rename fearless/{ => Modules/LiquidityPools/LiquidityPoolDetails}/Resources/runtime-rococo.json (100%) rename fearless/{ => Modules/LiquidityPools/LiquidityPoolDetails}/Resources/runtime-westend.json (100%) rename fearless/{ => Modules/LiquidityPools/LiquidityPoolDetails}/Resources/types.json (100%) create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolDetails/ViewModel/LiquidityPoolDetailsViewModel.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolDetails/ViewModel/LiquidityPoolDetailsViewModelFactory.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListType.swift diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index 09b6a5ac1a..541c4b98b6 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -2213,6 +2213,7 @@ FA3F5B6B281BAF6600BEF03B /* StakingAmountParachainViewModelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3F5B6A281BAF6600BEF03B /* StakingAmountParachainViewModelState.swift */; }; FA402F2F27C7C646008CF986 /* ExportAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA402F2E27C7C646008CF986 /* ExportAction.swift */; }; FA44284229D44E51000142EB /* ChainStakingSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA44284129D44E51000142EB /* ChainStakingSettings.swift */; }; + FA4441342BF75FD90067C633 /* LiquidityPoolListType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA4441332BF75FD90067C633 /* LiquidityPoolListType.swift */; }; FA46D2C7283DDD07005A112B /* ParachainStakingCandidateMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA46D2C6283DDD07005A112B /* ParachainStakingCandidateMetadata.swift */; }; FA4889672B7F5E360092ABF8 /* GiantsquidExtrinsic.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA4889662B7F5E360092ABF8 /* GiantsquidExtrinsic.swift */; }; FA4B928F284493C60003BCEF /* DelegateCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA4B928E284493C60003BCEF /* DelegateCall.swift */; }; @@ -2347,6 +2348,8 @@ FA6DB7C62757C9B000233FBA /* ChainAccountWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA6DB7BF2757C9AF00233FBA /* ChainAccountWireframe.swift */; }; FA6DB7C82757C9B000233FBA /* ChainAccountInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA6DB7C12757C9AF00233FBA /* ChainAccountInteractor.swift */; }; FA6DB7C92757C9B000233FBA /* ChainAccountProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA6DB7C22757C9AF00233FBA /* ChainAccountProtocols.swift */; }; + FA6ECE742BF49C0C00481B2B /* LiquidityPoolDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA6ECE732BF49C0C00481B2B /* LiquidityPoolDetailsViewModel.swift */; }; + FA6ECE762BF49D3D00481B2B /* LiquidityPoolDetailsViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA6ECE752BF49D3D00481B2B /* LiquidityPoolDetailsViewModelFactory.swift */; }; FA7154C4286B0C7D00100672 /* SelectValidatorsStartTextsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA7154C3286B0C7D00100672 /* SelectValidatorsStartTextsViewModel.swift */; }; FA7254202AC2E48500EC47A6 /* CryptoProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA7253F32AC2E48400EC47A6 /* CryptoProvider.swift */; }; FA7254212AC2E48500EC47A6 /* WalletConnectPayloadSigner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA7253F42AC2E48400EC47A6 /* WalletConnectPayloadSigner.swift */; }; @@ -5337,6 +5340,7 @@ FA3F5B6A281BAF6600BEF03B /* StakingAmountParachainViewModelState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingAmountParachainViewModelState.swift; sourceTree = ""; }; FA402F2E27C7C646008CF986 /* ExportAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportAction.swift; sourceTree = ""; }; FA44284129D44E51000142EB /* ChainStakingSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainStakingSettings.swift; sourceTree = ""; }; + FA4441332BF75FD90067C633 /* LiquidityPoolListType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidityPoolListType.swift; sourceTree = ""; }; FA46D2C6283DDD07005A112B /* ParachainStakingCandidateMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParachainStakingCandidateMetadata.swift; sourceTree = ""; }; FA4889662B7F5E360092ABF8 /* GiantsquidExtrinsic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiantsquidExtrinsic.swift; sourceTree = ""; }; FA4B928E284493C60003BCEF /* DelegateCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegateCall.swift; sourceTree = ""; }; @@ -5473,6 +5477,8 @@ FA6DB7C02757C9AF00233FBA /* ChainAccountPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChainAccountPresenter.swift; sourceTree = ""; }; FA6DB7C12757C9AF00233FBA /* ChainAccountInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChainAccountInteractor.swift; sourceTree = ""; }; FA6DB7C22757C9AF00233FBA /* ChainAccountProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChainAccountProtocols.swift; sourceTree = ""; }; + FA6ECE732BF49C0C00481B2B /* LiquidityPoolDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidityPoolDetailsViewModel.swift; sourceTree = ""; }; + FA6ECE752BF49D3D00481B2B /* LiquidityPoolDetailsViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidityPoolDetailsViewModelFactory.swift; sourceTree = ""; }; FA6FC016067C632AF256EB62 /* PolkaswapSwapConfirmationProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PolkaswapSwapConfirmationProtocols.swift; sourceTree = ""; }; FA7154C3286B0C7D00100672 /* SelectValidatorsStartTextsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectValidatorsStartTextsViewModel.swift; sourceTree = ""; }; FA7253F32AC2E48400EC47A6 /* CryptoProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CryptoProvider.swift; sourceTree = ""; }; @@ -7403,12 +7409,14 @@ 478C62A42D572C8647512722 /* LiquidityPoolDetails */ = { isa = PBXGroup; children = ( + FA6ECE722BF49BE300481B2B /* ViewModel */, 9FED48DE9B681995E6E4A581 /* LiquidityPoolDetailsProtocols.swift */, 28E3B4BA1160B87C435C2AAF /* LiquidityPoolDetailsRouter.swift */, 81E9DD6EB14A352635BAC711 /* LiquidityPoolDetailsPresenter.swift */, C53DFFFB3B5B48DB51692EFA /* LiquidityPoolDetailsInteractor.swift */, 67BDE520860A67C800E7F4AB /* LiquidityPoolDetailsViewController.swift */, A4900562AFFD45F29F4C5DEF /* LiquidityPoolDetailsViewLayout.swift */, + 84452F6F25D5E2B300F47EC5 /* Resources */, 5F6F7AE5AFF3F2E7BADA02BB /* LiquidityPoolDetailsAssembly.swift */, ); path = LiquidityPoolDetails; @@ -9083,7 +9091,6 @@ children = ( FA8644342767AB7E00956D8E /* CoreLayer */, FA38C9A227606FDD005C5577 /* ApplicationLayer */, - 84452F6F25D5E2B300F47EC5 /* Resources */, 8490141D24A93027008F705E /* Fonts */, 849013E324A92915008F705E /* Configs */, 849013D124A92686008F705E /* Common */, @@ -12488,6 +12495,7 @@ children = ( FA1D01EE2BBE713D005B7071 /* LiquidityPoolListCellModel.swift */, FA1D01EF2BBE713D005B7071 /* LiquidityPoolListViewModel.swift */, + FA4441332BF75FD90067C633 /* LiquidityPoolListType.swift */, ); path = ViewModel; sourceTree = ""; @@ -13622,6 +13630,15 @@ path = ChainAccount; sourceTree = ""; }; + FA6ECE722BF49BE300481B2B /* ViewModel */ = { + isa = PBXGroup; + children = ( + FA6ECE732BF49C0C00481B2B /* LiquidityPoolDetailsViewModel.swift */, + FA6ECE752BF49D3D00481B2B /* LiquidityPoolDetailsViewModelFactory.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; FA7253F92AC2E48400EC47A6 /* Model */ = { isa = PBXGroup; children = ( @@ -16568,6 +16585,7 @@ 84D97EC1251FEE1E00F07405 /* WalletAssetId+Display.swift in Sources */, 076D9D3F293E367C002762E3 /* PolkaswapDex.swift in Sources */, 849244922514EDE800477C1B /* SelectableViewModel.swift in Sources */, + FA6ECE742BF49C0C00481B2B /* LiquidityPoolDetailsViewModel.swift in Sources */, 84CFF1E826526FBC00DB7CF7 /* StakingBondMoreViewFactory.swift in Sources */, FA38C9672758764B005C5577 /* VerticalContentButton.swift in Sources */, 8490147524A94A37008F705E /* RootWireframe.swift in Sources */, @@ -16766,6 +16784,7 @@ 84DA3B1424C6D7C700B5E27F /* RuntimeDispatchInfo.swift in Sources */, F4D96B6C2637EB1300B23D3D /* StakingBalanceActionsWidgetViewModel.swift in Sources */, FA8F6386298253ED004B8CD4 /* AddConnectionError.swift in Sources */, + FA6ECE762BF49D3D00481B2B /* LiquidityPoolDetailsViewModelFactory.swift in Sources */, 84A8FD8E265FDA76002ADB58 /* CrowdloanContributionConfirmData.swift in Sources */, 8428768B24AE046300D91AD8 /* AboutViewFactory.swift in Sources */, FAFFAEA229AC84D30074AF1F /* HexColorConverter.swift in Sources */, @@ -18388,6 +18407,7 @@ C0B0DDF638915E8259B1CD67 /* StakingRedeemPresenter.swift in Sources */, C4A4D40A08DAB4A71C21C1A8 /* StakingRedeemInteractor.swift in Sources */, FA4B92912844CF750003BCEF /* MetaAccountModelChangedEvent.swift in Sources */, + FA4441342BF75FD90067C633 /* LiquidityPoolListType.swift in Sources */, FABA163C2B0C9510001AF2F0 /* NetworkManagmentTableCell.swift in Sources */, FAC0BBDE291D0EB000E6F106 /* SelectAssetAssembly.swift in Sources */, FA99426D28053CFA00D771E5 /* WalletDetailsViewModelFactory.swift in Sources */, diff --git a/fearless/Common/Extension/Model/WalletAssetId+Display.swift b/fearless/Common/Extension/Model/WalletAssetId+Display.swift index fa03486dcc..ea193e6d99 100644 --- a/fearless/Common/Extension/Model/WalletAssetId+Display.swift +++ b/fearless/Common/Extension/Model/WalletAssetId+Display.swift @@ -7,7 +7,7 @@ extension WalletAssetId { case .kusama: return R.image.iconKsmSmallBg() case .westend: return R.image.iconWestendSmallBg() case .roc: return R.image.iconKsmSmallBg() - case .usd: return nil + case .usd, .pswap: return nil } } @@ -17,7 +17,7 @@ extension WalletAssetId { case .kusama: return R.image.iconKsmAsset() case .westend: return R.image.iconWestendAsset() case .roc: return R.image.iconKsmAsset() - case .usd: return nil + case .usd, .pswap: return nil } } @@ -28,6 +28,7 @@ extension WalletAssetId { case .westend: return "Westend" case .roc: return "Rococo" case .usd: return "Usd" + case .pswap: return "Pswap" } } } diff --git a/fearless/Common/Model/WalletAssetId.swift b/fearless/Common/Model/WalletAssetId.swift index 4bbb2b9986..8bb4aa67fc 100644 --- a/fearless/Common/Model/WalletAssetId.swift +++ b/fearless/Common/Model/WalletAssetId.swift @@ -6,4 +6,5 @@ enum WalletAssetId: String { case westend case usd case roc + case pswap = "0x0200050000000000000000000000000000000000000000000000000000000000" } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsAssembly.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsAssembly.swift index 5f2e1fd643..5471a6988b 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsAssembly.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsAssembly.swift @@ -1,17 +1,38 @@ import UIKit import SoraFoundation +import SSFPolkaswap +import SSFModels final class LiquidityPoolDetailsAssembly { - static func configureModule() -> LiquidityPoolDetailsModuleCreationResult? { + static func configureModule(assetIdPair: AssetIdPair, chain: ChainModel, wallet: MetaAccountModel, input: LiquidityPoolDetailsInput) -> LiquidityPoolDetailsModuleCreationResult? { let localizationManager = LocalizationManager.shared - let interactor = LiquidityPoolDetailsInteractor() + let chainRegistry = ChainRegistryFacade.sharedRegistry + let poolService = PolkaswapLiquidityPoolServiceAssembly.buildService(for: chain, chainRegistry: chainRegistry) + + let interactor = LiquidityPoolDetailsInteractor( + assetIdPair: assetIdPair, + chain: chain, + wallet: wallet, + liquidityPoolService: poolService, + priceLocalSubscriber: PriceLocalStorageSubscriberImpl.shared + ) let router = LiquidityPoolDetailsRouter() + let viewModelFactory = LiquidityPoolDetailsViewModelFactoryDefault( + modelFactory: LiquidityPoolsModelFactoryDefault(), + assetBalanceFormatterFactory: AssetBalanceFormatterFactory() + ) let presenter = LiquidityPoolDetailsPresenter( interactor: interactor, router: router, - localizationManager: localizationManager + localizationManager: localizationManager, + assetIdPair: assetIdPair, + logger: Logger.shared, + viewModelFactory: viewModelFactory, + chain: chain, + wallet: wallet, + input: input ) let view = LiquidityPoolDetailsViewController( diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsInteractor.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsInteractor.swift index 94ac1e9461..8753836b16 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsInteractor.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsInteractor.swift @@ -1,9 +1,52 @@ import UIKit +import SSFModels +import SSFPolkaswap +import SSFPools +import SSFStorageQueryKit + +protocol LiquidityPoolDetailsInteractorOutput: AnyObject { + func didReceiveLiquidityPair(liquidityPair: LiquidityPair?) + func didReceiveUserPool(pool: AccountPool?) + func didReceivePoolReserves(reserves: CachedStorageResponse?) + func didReceivePoolAPY(apy: PoolApyInfo?) + func didReceivePrices(result: Result<[PriceData], Error>) + + func didReceiveLiquidityPairError(error: Error) + func didReceiveUserPoolError(error: Error) + func didReceivePoolReservesError(error: Error) + func didReceivePoolApyError(error: Error) +} final class LiquidityPoolDetailsInteractor { // MARK: - Private properties private weak var output: LiquidityPoolDetailsInteractorOutput? + + private let assetIdPair: AssetIdPair + private let chain: ChainModel + private let wallet: MetaAccountModel + private let liquidityPoolService: PolkaswapLiquidityPoolService + private let priceLocalSubscriber: PriceLocalStorageSubscriber + private var priceProvider: AnySingleValueProvider<[PriceData]>? + + init( + assetIdPair: AssetIdPair, + chain: ChainModel, + wallet: MetaAccountModel, + liquidityPoolService: PolkaswapLiquidityPoolService, + priceLocalSubscriber: PriceLocalStorageSubscriber + ) { + self.assetIdPair = assetIdPair + self.chain = chain + self.wallet = wallet + self.liquidityPoolService = liquidityPoolService + self.priceLocalSubscriber = priceLocalSubscriber + } + + private func subscribeForPrices() { + let chainAssets = chain.chainAssets + priceProvider = priceLocalSubscriber.subscribeToPrices(for: chainAssets, listener: self) + } } // MARK: - LiquidityPoolDetailsInteractorInput @@ -11,5 +54,92 @@ final class LiquidityPoolDetailsInteractor { extension LiquidityPoolDetailsInteractor: LiquidityPoolDetailsInteractorInput { func setup(with output: LiquidityPoolDetailsInteractorOutput) { self.output = output + + fetchPoolInfo() + fetchUserPool() + fetchReserves() + subscribeForPrices() + } + + func fetchPoolInfo() { + Task { + do { + let poolStream = try await liquidityPoolService.subscribeLiquidityPool(assetIdPair: assetIdPair) + + for try await pool in poolStream { + await MainActor.run { + output?.didReceiveLiquidityPair(liquidityPair: pool.value) + } + + if let reservesId = pool.value?.reservesId { + fetchApy(reservesId: reservesId) + } + } + } catch { + await MainActor.run { + output?.didReceiveLiquidityPairError(error: error) + } + } + } + } + + func fetchUserPool() { + guard let accountId = wallet.fetch(for: chain.accountRequest())?.accountId else { + output?.didReceiveUserPoolError(error: ChainAccountFetchingError.accountNotExists) + return + } + + Task { + do { + let accountPool = try await liquidityPoolService.fetchUserPool(assetIdPair: assetIdPair, accountId: accountId) + await MainActor.run { + output?.didReceiveUserPool(pool: accountPool) + } + } catch { + await MainActor.run { + output?.didReceiveUserPoolError(error: error) + } + } + } + } + + private func fetchReserves() { + Task { + do { + let reservesStream = try await liquidityPoolService.subscribePoolReserves(assetIdPair: assetIdPair) + + for try await reserves in reservesStream { + await MainActor.run { + output?.didReceivePoolReserves(reserves: reserves) + } + } + } catch { + await MainActor.run { + output?.didReceivePoolReservesError(error: error) + } + } + } + } + + func fetchApy(reservesId: String) { + Task { + do { + let apy = try await liquidityPoolService.fetchPoolsAPY() + let address = try AddressFactory.address(for: Data(hex: reservesId), chain: chain) + await MainActor.run { + output?.didReceivePoolAPY(apy: apy.first(where: { $0.poolId == address })) + } + } catch { + await MainActor.run { + output?.didReceivePoolApyError(error: error) + } + } + } + } +} + +extension LiquidityPoolDetailsInteractor: PriceLocalSubscriptionHandler { + func handlePrices(result: Result<[PriceData], Error>) { + output?.didReceivePrices(result: result) } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift index a7fe455c31..d211d337a3 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift @@ -1,5 +1,76 @@ import Foundation import SoraFoundation +import SSFPolkaswap +import SSFModels +import SSFPools +import SSFStorageQueryKit + +enum LiquidityPoolDetailsInput { + case initial + case userPool(liquidityPair: LiquidityPair?, reserves: PolkaswapPoolReservesInfo?, apyInfo: PoolApyInfo?, accountPool: AccountPool?) + case availablePool(liquidityPair: LiquidityPair?, reserves: PolkaswapPoolReservesInfo?, apyInfo: PoolApyInfo?) + + var liquidityPair: LiquidityPair? { + switch self { + case .initial: + return nil + case let .userPool(liquidityPair, _, _, _): + return liquidityPair + case let .availablePool(liquidityPair, _, _): + return liquidityPair + } + } + + var reserves: PolkaswapPoolReservesInfo? { + switch self { + case .initial: + return nil + case let .userPool(_, reserves, _, _): + return reserves + case let .availablePool(_, reserves, _): + return reserves + } + } + + var apyInfo: PoolApyInfo? { + switch self { + case .initial: + return nil + case let .userPool(_, _, apyInfo, _): + return apyInfo + case let .availablePool(_, _, apyInfo): + return apyInfo + } + } + + var accountPool: AccountPool? { + switch self { + case .initial: + return nil + case let .userPool(_, _, _, accountPool): + return accountPool + case .availablePool: + return nil + } + } + + var isUserPool: Bool { + switch self { + case .userPool: + return true + default: + return false + } + } +} + +protocol LiquidityPoolDetailsViewInput: ControllerBackedProtocol { + func bind(viewModel: LiquidityPoolDetailsViewModel?) +} + +protocol LiquidityPoolDetailsInteractorInput: AnyObject { + func setup(with output: LiquidityPoolDetailsInteractorOutput) +} final class LiquidityPoolDetailsPresenter { // MARK: Private properties @@ -7,20 +78,70 @@ final class LiquidityPoolDetailsPresenter { private weak var view: LiquidityPoolDetailsViewInput? private let router: LiquidityPoolDetailsRouterInput private let interactor: LiquidityPoolDetailsInteractorInput + private let assetIdPair: AssetIdPair + private let logger: LoggerProtocol + private let viewModelFactory: LiquidityPoolDetailsViewModelFactory + private let chain: ChainModel + private let wallet: MetaAccountModel + private let input: LiquidityPoolDetailsInput + + private var liquidityPair: LiquidityPair? + private var accountPoolInfo: AccountPool? + private var reserves: CachedStorageResponse? + private var apyInfo: PoolApyInfo? + private var priceData: [PriceData]? // MARK: - Constructors init( interactor: LiquidityPoolDetailsInteractorInput, router: LiquidityPoolDetailsRouterInput, - localizationManager: LocalizationManagerProtocol + localizationManager: LocalizationManagerProtocol, + assetIdPair: AssetIdPair, + logger: LoggerProtocol, + viewModelFactory: LiquidityPoolDetailsViewModelFactory, + chain: ChainModel, + wallet: MetaAccountModel, + input: LiquidityPoolDetailsInput ) { self.interactor = interactor self.router = router + self.assetIdPair = assetIdPair + self.logger = logger + self.viewModelFactory = viewModelFactory + self.chain = chain + self.wallet = wallet + self.input = input + self.localizationManager = localizationManager } // MARK: - Private methods + + private func provideViewModel() { + let liquidityPair = self.liquidityPair ?? input.liquidityPair + guard let liquidityPair else { + return + } + + let reserves = reserves ?? CachedStorageResponse(value: input.reserves, type: .remote) + let apy = apyInfo ?? input.apyInfo + let accountPoolInfo = accountPoolInfo ?? input.accountPool + + let viewModel = viewModelFactory.buildViewModel( + liquidityPair: liquidityPair, + reserves: reserves, + apyInfo: apy, + chain: chain, + prices: priceData, + locale: selectedLocale, + wallet: wallet, + accountPoolInfo: accountPoolInfo, + input: input + ) + + view?.bind(viewModel: viewModel) + } } // MARK: - LiquidityPoolDetailsViewOutput @@ -30,11 +151,61 @@ extension LiquidityPoolDetailsPresenter: LiquidityPoolDetailsViewOutput { self.view = view interactor.setup(with: self) } + + func backButtonClicked() { + router.dismiss(view: view) + } } // MARK: - LiquidityPoolDetailsInteractorOutput -extension LiquidityPoolDetailsPresenter: LiquidityPoolDetailsInteractorOutput {} +extension LiquidityPoolDetailsPresenter: LiquidityPoolDetailsInteractorOutput { + func didReceiveLiquidityPair(liquidityPair: SSFPools.LiquidityPair?) { + self.liquidityPair = liquidityPair + provideViewModel() + } + + func didReceiveUserPool(pool: AccountPool?) { + accountPoolInfo = pool + provideViewModel() + } + + func didReceivePoolReserves(reserves: CachedStorageResponse?) { + self.reserves = reserves + provideViewModel() + } + + func didReceivePoolAPY(apy: PoolApyInfo?) { + apyInfo = apy + provideViewModel() + } + + func didReceivePrices(result: Result<[PriceData], Error>) { + switch result { + case let .success(prices): + priceData = prices + provideViewModel() + case let .failure(error): + logger.customError(error) + } + } + + func didReceiveLiquidityPairError(error: Error) { + logger.customError(error) + } + + func didReceiveUserPoolError(error: Error) { + logger.customError(error) + } + + func didReceivePoolReservesError(error: Error) { + logger.customError(error) + } + + func didReceivePoolApyError(error: Error) { + logger.customError(error) + } +} // MARK: - Localizable diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsProtocols.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsProtocols.swift index 9a5f941277..ee9ecc0e99 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsProtocols.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsProtocols.swift @@ -1,18 +1,6 @@ typealias LiquidityPoolDetailsModuleCreationResult = (view: LiquidityPoolDetailsViewInput, input: LiquidityPoolDetailsModuleInput) -protocol LiquidityPoolDetailsViewInput: ControllerBackedProtocol {} - -protocol LiquidityPoolDetailsViewOutput: AnyObject { - func didLoad(view: LiquidityPoolDetailsViewInput) -} - -protocol LiquidityPoolDetailsInteractorInput: AnyObject { - func setup(with output: LiquidityPoolDetailsInteractorOutput) -} - -protocol LiquidityPoolDetailsInteractorOutput: AnyObject {} - -protocol LiquidityPoolDetailsRouterInput: AnyObject {} +protocol LiquidityPoolDetailsRouterInput: AnyObject, AnyDismissable {} protocol LiquidityPoolDetailsModuleInput: AnyObject {} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewController.swift index cf3e4da6c0..84d76d79b0 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewController.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewController.swift @@ -1,7 +1,12 @@ import UIKit import SoraFoundation -final class LiquidityPoolDetailsViewController: UIViewController, ViewHolder { +protocol LiquidityPoolDetailsViewOutput: AnyObject { + func didLoad(view: LiquidityPoolDetailsViewInput) + func backButtonClicked() +} + +final class LiquidityPoolDetailsViewController: UIViewController, ViewHolder, HiddableBarWhenPushed { typealias RootViewType = LiquidityPoolDetailsViewLayout // MARK: Private properties @@ -33,6 +38,10 @@ final class LiquidityPoolDetailsViewController: UIViewController, ViewHolder { override func viewDidLoad() { super.viewDidLoad() output.didLoad(view: self) + + rootView.navigationBar.backButton.addAction { [weak self] in + self?.output.backButtonClicked() + } } // MARK: - Private methods @@ -40,10 +49,16 @@ final class LiquidityPoolDetailsViewController: UIViewController, ViewHolder { // MARK: - LiquidityPoolDetailsViewInput -extension LiquidityPoolDetailsViewController: LiquidityPoolDetailsViewInput {} +extension LiquidityPoolDetailsViewController: LiquidityPoolDetailsViewInput { + func bind(viewModel: LiquidityPoolDetailsViewModel?) { + rootView.bind(viewModel: viewModel) + } +} // MARK: - Localizable extension LiquidityPoolDetailsViewController: Localizable { - func applyLocalization() {} + func applyLocalization() { + rootView.locale = selectedLocale + } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift index 9d9a4edc01..9ca15fedce 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift @@ -17,12 +17,13 @@ final class LiquidityPoolDetailsViewLayout: UIView { view.stackView.spacing = UIConstants.bigOffset return view }() - + let doubleImageView = PolkaswapDoubleSymbolView() - let swapStubTitle: UILabel = { + let pairTitleLabel: UILabel = { let label = UILabel() label.font = .h3Title - label.textColor = R.color.colorStrokeGray() + label.textColor = R.color.colorWhite() + label.textAlignment = .center return label }() @@ -45,14 +46,42 @@ final class LiquidityPoolDetailsViewLayout: UIView { return view }() - - let infoViewsStackView = UIFactory.default.createVerticalStackView(spacing: UIConstants.bigOffset) - + + let supplyButton = UIFactory.default.createMainActionButton() + let removeButton: TriangularedButton = { + let button = UIFactory.default.createDisabledButton() + button.isHidden = true + return button + }() + + var locale: Locale = .current { + didSet { + applyLocalization() + } + } + + let infoViewsStackView: UIStackView = { + let stackView = UIFactory.default.createVerticalStackView(spacing: UIConstants.bigOffset) + stackView.alignment = .center + stackView.layoutMargins = UIEdgeInsets(top: 24.0, left: 0.0, bottom: 0.0, right: 0.0) + return stackView + }() + let reservesView: TitleMultiValueView = makeRowView() let apyView: TitleMultiValueView = makeRowView() let rewardTokenView: TitleMultiValueView = makeRowView() - let baseAssetPooledView: TitleMultiValueView = makeRowView() - let targetAssetPooledView: TitleMultiValueView = makeRowView() + + let baseAssetPooledView: TitleMultiValueView = { + let view = makeRowView() + view.isHidden = true + return view + }() + + let targetAssetPooledView: TitleMultiValueView = { + let view = makeRowView() + view.isHidden = true + return view + }() override init(frame: CGRect) { super.init(frame: frame) @@ -67,22 +96,53 @@ final class LiquidityPoolDetailsViewLayout: UIView { required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func layoutSubviews() { super.layoutSubviews() navigationBar.backButton.rounded() } + func bind(viewModel: LiquidityPoolDetailsViewModel?) { + if let tokenPairIconsViewModel = viewModel?.tokenPairIconsViewModel { + doubleImageView.bind(viewModel: tokenPairIconsViewModel) + } + pairTitleLabel.text = viewModel?.pairTitleLabelText + reservesView.bind(viewModel: viewModel?.reservesViewModel) + apyView.bind(viewModel: viewModel?.apyViewModel) + rewardTokenView.valueTop.text = viewModel?.rewardTokenLabelText + + baseAssetPooledView.isHidden = viewModel?.userPoolFieldsHidden == true + targetAssetPooledView.isHidden = viewModel?.userPoolFieldsHidden == true + removeButton.isHidden = viewModel?.userPoolFieldsHidden == true + + baseAssetPooledView.bindBalance(viewModel: viewModel?.baseAssetViewModel) + targetAssetPooledView.bindBalance(viewModel: viewModel?.targetAssetViewModel) + + if let baseAssetName = viewModel?.baseAssetName.uppercased() { + baseAssetPooledView.titleLabel.text = "Your \(baseAssetName) Pooled" + } + + if let targetAssetName = viewModel?.targetAssetName.uppercased() { + targetAssetPooledView.titleLabel.text = "Your \(targetAssetName) Pooled" + } + } + private func drawSubviews() { addSubview(navigationBar) addSubview(contentView) - + contentView.stackView.addArrangedSubview(infoBackground) infoBackground.addSubview(infoViewsStackView) infoViewsStackView.addArrangedSubview(doubleImageView) - infoViewsStackView.addArrangedSubview(swapStubTitle) - + infoViewsStackView.addArrangedSubview(pairTitleLabel) + infoViewsStackView.addArrangedSubview(reservesView) + infoViewsStackView.addArrangedSubview(apyView) + infoViewsStackView.addArrangedSubview(rewardTokenView) + infoViewsStackView.addArrangedSubview(baseAssetPooledView) + infoViewsStackView.addArrangedSubview(targetAssetPooledView) + contentView.stackView.addArrangedSubview(supplyButton) + contentView.stackView.addArrangedSubview(removeButton) } private func setupConstraints() { @@ -93,8 +153,39 @@ final class LiquidityPoolDetailsViewLayout: UIView { make.top.equalTo(navigationBar.snp.bottom) make.leading.trailing.bottom.equalToSuperview() } + infoBackground.snp.makeConstraints { make in + make.leading.trailing.top.equalToSuperview().inset(16) + } + infoViewsStackView.snp.makeConstraints { make in + make.top.leading.trailing.equalToSuperview() + make.bottom.equalToSuperview().inset(16) + } + pairTitleLabel.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview() + } + + doubleImageView.snp.makeConstraints { make in + make.top.equalToSuperview().inset(24) + } + + [ + reservesView, + apyView, + rewardTokenView, + baseAssetPooledView, + targetAssetPooledView + ].forEach { + setupDefaultRowConstraints(for: $0) + } + + [supplyButton, removeButton].forEach { + $0.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(16) + make.height.equalTo(UIConstants.actionHeight) + } + } } - + private func setupDefaultRowConstraints(for view: UIView) { view.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(16) @@ -107,7 +198,7 @@ final class LiquidityPoolDetailsViewLayout: UIView { make.height.equalTo(56) } } - + private static func makeRowView() -> TitleMultiValueView { let view = TitleMultiValueView() view.titleLabel.font = .p1Paragraph @@ -122,8 +213,13 @@ final class LiquidityPoolDetailsViewLayout: UIView { view.valueBottom.lineBreakMode = .byTruncatingMiddle return view } - + private func applyLocalization() { - + reservesView.titleLabel.text = "TVL" + apyView.titleLabel.text = "Strategic Bonus APY" + rewardTokenView.titleLabel.text = "Rewards Payout In" + supplyButton.imageWithTitleView?.title = "Supply Liquidity" + removeButton.imageWithTitleView?.title = "Remove Liquidity" + navigationBar.setTitle("Pool details") } } diff --git a/fearless/Resources/assets.json b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/Resources/assets.json similarity index 100% rename from fearless/Resources/assets.json rename to fearless/Modules/LiquidityPools/LiquidityPoolDetails/Resources/assets.json diff --git a/fearless/Resources/chains.json b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/Resources/chains.json similarity index 100% rename from fearless/Resources/chains.json rename to fearless/Modules/LiquidityPools/LiquidityPoolDetails/Resources/chains.json diff --git a/fearless/Resources/polkadot-9370metadata b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/Resources/polkadot-9370metadata similarity index 100% rename from fearless/Resources/polkadot-9370metadata rename to fearless/Modules/LiquidityPools/LiquidityPoolDetails/Resources/polkadot-9370metadata diff --git a/fearless/Resources/polkaswapSettings.json b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/Resources/polkaswapSettings.json similarity index 100% rename from fearless/Resources/polkaswapSettings.json rename to fearless/Modules/LiquidityPools/LiquidityPoolDetails/Resources/polkaswapSettings.json diff --git a/fearless/Resources/runtime-default.json b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/Resources/runtime-default.json similarity index 100% rename from fearless/Resources/runtime-default.json rename to fearless/Modules/LiquidityPools/LiquidityPoolDetails/Resources/runtime-default.json diff --git a/fearless/Resources/runtime-empty.json b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/Resources/runtime-empty.json similarity index 100% rename from fearless/Resources/runtime-empty.json rename to fearless/Modules/LiquidityPools/LiquidityPoolDetails/Resources/runtime-empty.json diff --git a/fearless/Resources/runtime-kusama.json b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/Resources/runtime-kusama.json similarity index 100% rename from fearless/Resources/runtime-kusama.json rename to fearless/Modules/LiquidityPools/LiquidityPoolDetails/Resources/runtime-kusama.json diff --git a/fearless/Resources/runtime-polkadot.json b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/Resources/runtime-polkadot.json similarity index 100% rename from fearless/Resources/runtime-polkadot.json rename to fearless/Modules/LiquidityPools/LiquidityPoolDetails/Resources/runtime-polkadot.json diff --git a/fearless/Resources/runtime-rococo.json b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/Resources/runtime-rococo.json similarity index 100% rename from fearless/Resources/runtime-rococo.json rename to fearless/Modules/LiquidityPools/LiquidityPoolDetails/Resources/runtime-rococo.json diff --git a/fearless/Resources/runtime-westend.json b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/Resources/runtime-westend.json similarity index 100% rename from fearless/Resources/runtime-westend.json rename to fearless/Modules/LiquidityPools/LiquidityPoolDetails/Resources/runtime-westend.json diff --git a/fearless/Resources/types.json b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/Resources/types.json similarity index 100% rename from fearless/Resources/types.json rename to fearless/Modules/LiquidityPools/LiquidityPoolDetails/Resources/types.json diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/ViewModel/LiquidityPoolDetailsViewModel.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/ViewModel/LiquidityPoolDetailsViewModel.swift new file mode 100644 index 0000000000..040191a953 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/ViewModel/LiquidityPoolDetailsViewModel.swift @@ -0,0 +1,14 @@ +import Foundation + +struct LiquidityPoolDetailsViewModel { + let pairTitleLabelText: String + let baseAssetName: String + let targetAssetName: String + let reservesViewModel: TitleMultiValueViewModel? + let apyViewModel: TitleMultiValueViewModel? + let rewardTokenLabelText: String + let baseAssetViewModel: BalanceViewModelProtocol? + let targetAssetViewModel: BalanceViewModelProtocol? + let tokenPairIconsViewModel: PolkaswapDoubleSymbolViewModel? + let userPoolFieldsHidden: Bool +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/ViewModel/LiquidityPoolDetailsViewModelFactory.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/ViewModel/LiquidityPoolDetailsViewModelFactory.swift new file mode 100644 index 0000000000..5c219ebb84 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/ViewModel/LiquidityPoolDetailsViewModelFactory.swift @@ -0,0 +1,119 @@ +import Foundation +import SSFPolkaswap +import SSFPools +import SSFModels +import SSFStorageQueryKit +import SoraFoundation + +protocol LiquidityPoolDetailsViewModelFactory { + func buildViewModel( + liquidityPair: LiquidityPair, + reserves: CachedStorageResponse?, + apyInfo: PoolApyInfo?, + chain: ChainModel, + prices: [PriceData]?, + locale: Locale, + wallet: MetaAccountModel, + accountPoolInfo: AccountPool?, + input: LiquidityPoolDetailsInput + ) -> LiquidityPoolDetailsViewModel? +} + +final class LiquidityPoolDetailsViewModelFactoryDefault: LiquidityPoolDetailsViewModelFactory { + private let modelFactory: LiquidityPoolsModelFactory + private let assetBalanceFormatterFactory: AssetBalanceFormatterFactoryProtocol + + init( + modelFactory: LiquidityPoolsModelFactory, + assetBalanceFormatterFactory: AssetBalanceFormatterFactoryProtocol + ) { + self.modelFactory = modelFactory + self.assetBalanceFormatterFactory = assetBalanceFormatterFactory + } + + func buildViewModel( + liquidityPair: LiquidityPair, + reserves: CachedStorageResponse?, + apyInfo: PoolApyInfo?, + chain: ChainModel, + prices: [PriceData]?, + locale: Locale, + wallet: MetaAccountModel, + accountPoolInfo: AccountPool?, + input: LiquidityPoolDetailsInput + ) -> LiquidityPoolDetailsViewModel? { + guard + let baseAsset = chain.assets.first(where: { $0.currencyId == liquidityPair.baseAssetId }), + let targetAsset = chain.assets.first(where: { $0.currencyId == liquidityPair.targetAssetId }), + let rewardAsset = chain.assets.first(where: { $0.currencyId == liquidityPair.rewardAssetId }) + else { + return nil + } + + let fiatFormatter = fiatFormatter(for: wallet.selectedCurrency, locale: locale) + + let baseAssetPrice = prices?.first(where: { $0.priceId == baseAsset.priceId }) + let targetAssetPrice = prices?.first(where: { $0.priceId == targetAsset.priceId }) + + let reservesValue = modelFactory.buildReserves( + pool: liquidityPair, + chain: chain, + reserves: reserves?.value, + baseAssetPrice: baseAssetPrice, + targetAssetPrice: targetAssetPrice + ) + let reservesString = reservesValue.flatMap { fiatFormatter.stringFromDecimal($0) } + let reservesLabelText: String? = reservesString.flatMap { "\($0) TVL" } + + let apyLabelText = apyInfo?.apy.flatMap { NumberFormatter.percentAPY.stringFromDecimal($0) } + let tokenPairsIconViewModel = PolkaswapDoubleSymbolViewModel( + leftViewModel: RemoteImageViewModel(url: baseAsset.icon), + rightViewModel: RemoteImageViewModel(url: targetAsset.icon), + leftShadowColor: HexColorConverter.hexStringToUIColor(hex: baseAsset.color)?.cgColor, + rightShadowColor: HexColorConverter.hexStringToUIColor(hex: targetAsset.color)?.cgColor + ) + + let baseAssetBalanceViewModelFactory = createBalanceViewModelFactory(for: ChainAsset(chain: chain, asset: baseAsset), wallet: wallet) + let targetAssetBalanceViewModelFactory = createBalanceViewModelFactory(for: ChainAsset(chain: chain, asset: targetAsset), wallet: wallet) + + let baseAssetViewModel = accountPoolInfo?.baseAssetPooled.flatMap { + baseAssetBalanceViewModelFactory.balanceFromPrice($0, priceData: baseAssetPrice, usageCase: .detailsCrypto) + } + + let targetAssetViewModel = accountPoolInfo?.targetAssetPooled.flatMap { + targetAssetBalanceViewModelFactory.balanceFromPrice($0, priceData: targetAssetPrice, usageCase: .detailsCrypto) + } + let reservesViewModel = reservesLabelText.flatMap { TitleMultiValueViewModel(title: $0, subtitle: nil) } + let apyViewModel = apyLabelText.flatMap { TitleMultiValueViewModel(title: $0, subtitle: nil) } + + return LiquidityPoolDetailsViewModel( + pairTitleLabelText: "\(baseAsset.symbol.uppercased()) > \(targetAsset.symbol.uppercased())", + baseAssetName: baseAsset.symbol.uppercased(), + targetAssetName: targetAsset.symbol.uppercased(), + reservesViewModel: reservesViewModel, + apyViewModel: apyViewModel, + rewardTokenLabelText: rewardAsset.symbol.uppercased(), + baseAssetViewModel: baseAssetViewModel?.value(for: locale), + targetAssetViewModel: targetAssetViewModel?.value(for: locale), + tokenPairIconsViewModel: tokenPairsIconViewModel, + userPoolFieldsHidden: !input.isUserPool && accountPoolInfo == nil + ) + } + + private func fiatFormatter( + for currency: Currency, + locale: Locale + ) -> TokenFormatter { + let displayInfo = AssetBalanceDisplayInfo.forCurrency(currency) + let tokenFormatter = assetBalanceFormatterFactory.createTokenFormatter(for: displayInfo, usageCase: .fiat) + let tokenFormatterValue = tokenFormatter.value(for: locale) + return tokenFormatterValue + } + + private func createBalanceViewModelFactory(for chainAsset: ChainAsset, wallet: MetaAccountModel) -> BalanceViewModelFactoryProtocol { + BalanceViewModelFactory( + targetAssetInfo: chainAsset.assetDisplayInfo, + selectedMetaAccount: wallet + ) + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListInteractor.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListInteractor.swift index 5943e30511..3026a781f4 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListInteractor.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListInteractor.swift @@ -35,7 +35,7 @@ final class AvailableLiquidityPoolsListInteractor { private func fetchReserves(pools: [LiquidityPair]) { Task { do { - let reservesStream = try await liquidityPoolService.subscribePoolReserves(pools: pools) + let reservesStream = try await liquidityPoolService.subscribePoolsReserves(pools: pools) for try await reserves in reservesStream { await MainActor.run { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListPresenter.swift index 8ee23110ce..ffc853ca19 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListPresenter.swift @@ -19,6 +19,9 @@ final class AvailableLiquidityPoolsListPresenter { private let wallet: MetaAccountModel private let viewModelFactory: AvailableLiquidityPoolsListViewModelFactory private weak var view: LiquidityPoolsListViewInput? + private let router: LiquidityPoolsListRouterInput + private weak var moduleOutput: LiquidityPoolsListModuleOutput? + private let type: LiquidityPoolListType private var pairs: [LiquidityPair]? private var reserves: CachedStorageResponse<[PolkaswapPoolReservesInfo]>? @@ -28,16 +31,23 @@ final class AvailableLiquidityPoolsListPresenter { init( logger: Logger, interactor: AvailableLiquidityPoolsListInteractorInput, + router: LiquidityPoolsListRouterInput, chain: ChainModel, wallet: MetaAccountModel, viewModelFactory: AvailableLiquidityPoolsListViewModelFactory, - localizationManager: LocalizationManagerProtocol + localizationManager: LocalizationManagerProtocol, + moduleOutput: LiquidityPoolsListModuleOutput?, + type: LiquidityPoolListType ) { self.logger = logger self.interactor = interactor + self.router = router self.chain = chain self.wallet = wallet self.viewModelFactory = viewModelFactory + self.moduleOutput = moduleOutput + self.type = type + self.localizationManager = localizationManager } @@ -49,7 +59,8 @@ final class AvailableLiquidityPoolsListPresenter { chain: chain, prices: prices, locale: selectedLocale, - wallet: wallet + wallet: wallet, + type: type ) view?.didReceive(viewModel: viewModel) @@ -62,6 +73,22 @@ extension AvailableLiquidityPoolsListPresenter: LiquidityPoolsListViewOutput { interactor.setup(with: self) } + + func didTapOn(viewModel: LiquidityPoolListCellModel) { + let liquidityPair = viewModel.liquidityPair + let reserves = reserves?.value?.first(where: { $0.poolId == liquidityPair.pairId }) + let reservesAddress = liquidityPair.reservesId.map { try? AddressFactory.address(for: Data(hex: $0), chain: chain) } + let apyInfo = apy?.first(where: { $0.poolId == reservesAddress }) + + let assetIdPair = AssetIdPair(baseAssetIdCode: viewModel.liquidityPair.baseAssetId, targetAssetIdCode: viewModel.liquidityPair.targetAssetId) + let input = LiquidityPoolDetailsInput.availablePool(liquidityPair: liquidityPair, reserves: reserves, apyInfo: apyInfo) + + router.showPoolDetails(assetIdPair: assetIdPair, chain: chain, wallet: wallet, input: input, from: view) + } + + func didTapMoreButton() { + moduleOutput?.didTapMoreAvailablePools() + } } extension AvailableLiquidityPoolsListPresenter: AvailableLiquidityPoolsListInteractorOutput { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListViewModelFactory.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListViewModelFactory.swift index 505aa9c70d..6b830ba9a5 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListViewModelFactory.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListViewModelFactory.swift @@ -14,7 +14,8 @@ protocol AvailableLiquidityPoolsListViewModelFactory { chain: ChainModel, prices: [PriceData]?, locale: Locale, - wallet: MetaAccountModel + wallet: MetaAccountModel, + type: LiquidityPoolListType ) -> LiquidityPoolListViewModel } @@ -34,7 +35,8 @@ final class AvailableLiquidityPoolsListViewModelFactoryDefault: AvailableLiquidi chain: ChainModel, prices: [PriceData]?, locale: Locale, - wallet: MetaAccountModel + wallet: MetaAccountModel, + type: LiquidityPoolListType ) -> LiquidityPoolListViewModel { let poolViewModels: [LiquidityPoolListCellModel]? = pairs?.sorted().compactMap { pair in let baseAsset = chain.assets.first(where: { $0.currencyId == pair.baseAssetId }) @@ -79,14 +81,18 @@ final class AvailableLiquidityPoolsListViewModelFactoryDefault: AvailableLiquidi apyLabelText: apyLabelText, stakingStatusLabelText: nil, reservesLabelValue: reservesLabelValue, - sortValue: reservesValue.or(.zero) + sortValue: reservesValue.or(.zero), + liquidityPair: pair ) }.sorted(by: { $0.sortValue > $1.sortValue }) + let filteredViewModels = type == .embed ? Array(poolViewModels.or([]).prefix(10)) : poolViewModels.or([]) return LiquidityPoolListViewModel( - poolViewModels: poolViewModels, + poolViewModels: filteredViewModels, titleLabelText: "Available pools", - moreButtonVisible: true + moreButtonVisible: type == .embed && (filteredViewModels.count < pairs?.count ?? 0), + backgroundVisible: type == .full, + refreshAvailable: type == .full ) } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListInteractor.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListInteractor.swift index e9e1e96d60..5e424f4d1b 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListInteractor.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListInteractor.swift @@ -5,14 +5,16 @@ import SSFModels import SSFStorageQueryKit protocol UserLiquidityPoolsListInteractorOutput { - func didReceiveUserPools(pools: [LiquidityPair]?) + func didReceiveLiquidityPairs(pools: [LiquidityPair]?) func didReceivePoolsReserves(reserves: CachedStorageResponse<[PolkaswapPoolReservesInfo]>?) func didReceivePoolsAPY(apy: [PoolApyInfo]) + func didReceiveUserPools(accountPools: [AccountPool]?) func didReceivePrices(result: Result<[PriceData], Error>) func didReceiveLiquidityPairsError(error: Error) func didReceivePoolsReservesError(error: Error) func didReceivePoolsApyError(error: Error) + func didReceiveUserPoolsError(error: Error) } final class UserLiquidityPoolsListInteractor { @@ -35,6 +37,24 @@ final class UserLiquidityPoolsListInteractor { self.wallet = wallet } +// private func fetchReserves(pools: [AccountPool]) { +// Task { +// do { +// let reservesStream = try await liquidityPoolService.subscribePoolsReserves(pools: pools) +// +// for try await reserves in reservesStream { +// await MainActor.run { +// output?.didReceivePoolsReserves(reserves: reserves) +// } +// } +// } catch { +// await MainActor.run { +// output?.didReceivePoolsReservesError(error: error) +// } +// } +// } +// } + private func subscribeForPrices() { let chainAssets = chain.chainAssets priceProvider = priceLocalSubscriber.subscribeToPrices(for: chainAssets, listener: self) @@ -50,18 +70,43 @@ extension UserLiquidityPoolsListInteractor: UserLiquidityPoolsListInteractorInpu subscribeForPrices() } + func fetchUserPools() { + guard let accountId = wallet.fetch(for: chain.accountRequest())?.accountId else { + output?.didReceiveLiquidityPairsError(error: ChainAccountFetchingError.accountNotExists) + return + } + + Task { + do { + let accountPools = try await liquidityPoolService.fetchUserPools(accountId: accountId) + await MainActor.run { + output?.didReceiveUserPools(accountPools: accountPools) + } + +// if let pools = accountPools { +// fetchReserves(pools: pools) +// } + } catch { + await MainActor.run { + output?.didReceiveUserPoolsError(error: error) + } + } + } + } + func fetchPools() { guard let accountId = wallet.fetch(for: chain.accountRequest())?.accountId else { output?.didReceiveLiquidityPairsError(error: ChainAccountFetchingError.accountNotExists) return } + Task { do { let userPoolsStream = try await liquidityPoolService.subscribeUserPools(accountId: accountId) for try await userPools in userPoolsStream { await MainActor.run { - output?.didReceiveUserPools(pools: userPools.value) + output?.didReceiveLiquidityPairs(pools: userPools.value) } } } catch { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift index daa9c3d169..7d6316fb8b 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift @@ -15,29 +15,40 @@ protocol UserLiquidityPoolsListInteractorInput { final class UserLiquidityPoolsListPresenter { private let logger: Logger private let interactor: UserLiquidityPoolsListInteractorInput + private let router: LiquidityPoolsListRouterInput private let chain: ChainModel private let wallet: MetaAccountModel private let viewModelFactory: UserLiquidityPoolsListViewModelFactory private weak var view: LiquidityPoolsListViewInput? + private weak var moduleOutput: LiquidityPoolsListModuleOutput? + private let type: LiquidityPoolListType private var pools: [LiquidityPair]? private var reserves: CachedStorageResponse<[PolkaswapPoolReservesInfo]>? private var apy: [PoolApyInfo]? + private var accountPools: [AccountPool]? private var prices: [PriceData]? init( logger: Logger, interactor: UserLiquidityPoolsListInteractorInput, + router: LiquidityPoolsListRouterInput, chain: ChainModel, wallet: MetaAccountModel, viewModelFactory: UserLiquidityPoolsListViewModelFactory, - localizationManager: LocalizationManagerProtocol + localizationManager: LocalizationManagerProtocol, + moduleOutput: LiquidityPoolsListModuleOutput?, + type: LiquidityPoolListType ) { self.logger = logger self.interactor = interactor + self.router = router self.chain = chain self.wallet = wallet self.viewModelFactory = viewModelFactory + self.moduleOutput = moduleOutput + self.type = type + self.localizationManager = localizationManager } @@ -49,7 +60,8 @@ final class UserLiquidityPoolsListPresenter { chain: chain, prices: prices, locale: selectedLocale, - wallet: wallet + wallet: wallet, + type: type ) view?.didReceive(viewModel: viewModel) @@ -62,10 +74,29 @@ extension UserLiquidityPoolsListPresenter: LiquidityPoolsListViewOutput { interactor.setup(with: self) } + + func didTapOn(viewModel: LiquidityPoolListCellModel) { + let liquidityPair = viewModel.liquidityPair + let reserves = reserves?.value?.first(where: { $0.poolId == liquidityPair.pairId }) + let reservesAddress = liquidityPair.reservesId.map { try? AddressFactory.address(for: Data(hex: $0), chain: chain) } + let apyInfo = apy?.first(where: { $0.poolId == reservesAddress }) + let accountPool = accountPools?.first(where: { $0.poolId == liquidityPair.pairId }) + let assetIdPair = AssetIdPair(baseAssetIdCode: viewModel.liquidityPair.baseAssetId, targetAssetIdCode: viewModel.liquidityPair.targetAssetId) + let input = LiquidityPoolDetailsInput.userPool(liquidityPair: liquidityPair, reserves: reserves, apyInfo: apyInfo, accountPool: accountPool) + router.showPoolDetails(assetIdPair: assetIdPair, chain: chain, wallet: wallet, input: input, from: view) + } + + func didTapMoreButton() { + moduleOutput?.didTapMoreUserPools() + } } extension UserLiquidityPoolsListPresenter: UserLiquidityPoolsListInteractorOutput { - func didReceiveUserPools(pools: [LiquidityPair]?) { + func didReceiveUserPools(accountPools: [AccountPool]?) { + self.accountPools = accountPools + } + + func didReceiveLiquidityPairs(pools: [LiquidityPair]?) { self.pools = pools provideViewModel() } @@ -101,6 +132,10 @@ extension UserLiquidityPoolsListPresenter: UserLiquidityPoolsListInteractorOutpu func didReceivePoolsApyError(error: Error) { logger.customError(error) } + + func didReceiveUserPoolsError(error: Error) { + logger.customError(error) + } } extension UserLiquidityPoolsListPresenter: LiquidityPoolsListModuleInput {} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListViewModelFactory.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListViewModelFactory.swift index 1d32556726..e4faf15da5 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListViewModelFactory.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListViewModelFactory.swift @@ -14,7 +14,8 @@ protocol UserLiquidityPoolsListViewModelFactory { chain: ChainModel, prices: [PriceData]?, locale: Locale, - wallet: MetaAccountModel + wallet: MetaAccountModel, + type: LiquidityPoolListType ) -> LiquidityPoolListViewModel } @@ -34,7 +35,8 @@ final class UserLiquidityPoolsListViewModelFactoryDefault: UserLiquidityPoolsLis chain: ChainModel, prices: [PriceData]?, locale: Locale, - wallet: MetaAccountModel + wallet: MetaAccountModel, + type: LiquidityPoolListType ) -> LiquidityPoolListViewModel { let poolViewModels: [LiquidityPoolListCellModel]? = pools?.sorted().compactMap { pair in let baseAsset = chain.assets.first(where: { $0.currencyId == pair.baseAssetId }) @@ -55,8 +57,9 @@ final class UserLiquidityPoolsListViewModelFactoryDefault: UserLiquidityPoolsLis let reservesAddress = pair.reservesId.map { try? AddressFactory.address(for: Data(hex: $0), chain: chain) } let rewardTokenNameLabelText = "Earn \(rewardAsset.symbol.uppercased())" - let apyValue = apyInfos?.first(where: { $0.poolId == reservesAddress })?.apy - let apyLabelText = apyValue.flatMap { "\($0)% APY" } + let apyInfo = apyInfos?.first(where: { $0.poolId == reservesAddress }) + let apyValue = apyInfo?.apy + let apyLabelText = apyValue.flatMap { NumberFormatter.percentAPY.stringFromDecimal($0) } let baseAssetPrice = prices?.first(where: { $0.priceId == baseAsset.priceId }) let targetAssetPrice = prices?.first(where: { $0.priceId == targetAsset.priceId }) @@ -79,14 +82,17 @@ final class UserLiquidityPoolsListViewModelFactoryDefault: UserLiquidityPoolsLis apyLabelText: apyLabelText, stakingStatusLabelText: nil, reservesLabelValue: reservesLabelValue, - sortValue: reservesValue.or(.zero) + sortValue: reservesValue.or(.zero), + liquidityPair: pair ) }.sorted(by: { $0.sortValue > $1.sortValue }) return LiquidityPoolListViewModel( poolViewModels: poolViewModels, titleLabelText: "User pools", - moreButtonVisible: true + moreButtonVisible: type == .embed && (poolViewModels?.count ?? 0 < pools?.count ?? 0), + backgroundVisible: type == .full, + refreshAvailable: type == .full ) } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListAssembly.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListAssembly.swift index 29e45958f9..5bd844940e 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListAssembly.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListAssembly.swift @@ -8,7 +8,7 @@ import sorawallet import SSFRuntimeCodingService final class LiquidityPoolsListAssembly { - static func configureAvailablePoolsModule(chain: ChainModel, wallet: MetaAccountModel) -> LiquidityPoolsListModuleCreationResult? { + static func configureAvailablePoolsModule(chain: ChainModel, wallet: MetaAccountModel, moduleOutput: LiquidityPoolsListModuleOutput?, type: LiquidityPoolListType) -> LiquidityPoolsListModuleCreationResult? { let localizationManager = LocalizationManager.shared let chainRegistry = ChainRegistryFacade.sharedRegistry @@ -26,10 +26,13 @@ final class LiquidityPoolsListAssembly { let presenter = AvailableLiquidityPoolsListPresenter( logger: Logger.shared, interactor: interactor, + router: router, chain: chain, wallet: wallet, viewModelFactory: viewModelFactory, - localizationManager: LocalizationManager.shared + localizationManager: LocalizationManager.shared, + moduleOutput: moduleOutput, + type: type ) let view = LiquidityPoolsListViewController( @@ -40,7 +43,7 @@ final class LiquidityPoolsListAssembly { return (view, presenter) } - static func configureUserPoolsModule(chain: ChainModel, wallet: MetaAccountModel) -> LiquidityPoolsListModuleCreationResult? { + static func configureUserPoolsModule(chain: ChainModel, wallet: MetaAccountModel, moduleOutput: LiquidityPoolsListModuleOutput?, type: LiquidityPoolListType) -> LiquidityPoolsListModuleCreationResult? { let localizationManager = LocalizationManager.shared let chainRegistry = ChainRegistryFacade.sharedRegistry @@ -59,10 +62,13 @@ final class LiquidityPoolsListAssembly { let presenter = UserLiquidityPoolsListPresenter( logger: Logger.shared, interactor: interactor, + router: router, chain: chain, wallet: wallet, viewModelFactory: viewModelFactory, - localizationManager: LocalizationManager.shared + localizationManager: LocalizationManager.shared, + moduleOutput: moduleOutput, + type: type ) let view = LiquidityPoolsListViewController( diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListProtocols.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListProtocols.swift index 3683188d17..319313e683 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListProtocols.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListProtocols.swift @@ -1,3 +1,7 @@ +import SSFPolkaswap +import SSFModels +import SSFPools + typealias LiquidityPoolsListModuleCreationResult = (view: LiquidityPoolsListViewInput, input: LiquidityPoolsListModuleInput) protocol LiquidityPoolsListViewInput: ControllerBackedProtocol { @@ -6,6 +10,8 @@ protocol LiquidityPoolsListViewInput: ControllerBackedProtocol { protocol LiquidityPoolsListViewOutput: AnyObject { func didLoad(view: LiquidityPoolsListViewInput) + func didTapOn(viewModel: LiquidityPoolListCellModel) + func didTapMoreButton() } protocol LiquidityPoolsListInteractorInput: AnyObject { @@ -13,8 +19,19 @@ protocol LiquidityPoolsListInteractorInput: AnyObject { func fetchPools() } -protocol LiquidityPoolsListRouterInput: AnyObject {} +protocol LiquidityPoolsListRouterInput: AnyObject { + func showPoolDetails( + assetIdPair: AssetIdPair, + chain: ChainModel, + wallet: MetaAccountModel, + input: LiquidityPoolDetailsInput, + from view: ControllerBackedProtocol? + ) +} protocol LiquidityPoolsListModuleInput: AnyObject {} -protocol LiquidityPoolsListModuleOutput: AnyObject {} +protocol LiquidityPoolsListModuleOutput: AnyObject { + func didTapMoreUserPools() + func didTapMoreAvailablePools() +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListRouter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListRouter.swift index 7910ac4971..89d0005cf2 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListRouter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListRouter.swift @@ -1,3 +1,15 @@ import Foundation +import SSFModels +import SSFPolkaswap -final class LiquidityPoolsListRouter: LiquidityPoolsListRouterInput {} +final class LiquidityPoolsListRouter: LiquidityPoolsListRouterInput { + func showPoolDetails(assetIdPair: AssetIdPair, chain: ChainModel, wallet: MetaAccountModel, input: LiquidityPoolDetailsInput, from view: ControllerBackedProtocol?) { + let module = LiquidityPoolDetailsAssembly.configureModule(assetIdPair: assetIdPair, chain: chain, wallet: wallet, input: input) + + guard let viewController = module?.view.controller else { + return + } + + view?.controller.navigationController?.pushViewController(viewController, animated: true) + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift index c28aeee22c..d4275f6e44 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift @@ -38,6 +38,10 @@ final class LiquidityPoolsListViewController: UIViewController, ViewHolder { rootView.tableView.delegate = self rootView.tableView.dataSource = self rootView.tableView.registerClassForCell(LiquidityPoolListCell.self) + + rootView.moreButton.addAction { [weak self] in + self?.output.didTapMoreButton() + } } // MARK: - Private methods @@ -70,6 +74,9 @@ extension LiquidityPoolsListViewController: UITableViewDataSource, UITableViewDe func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) + + guard let cellModels else { return } + output.didTapOn(viewModel: cellModels[indexPath.row]) } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift index 8733c76fee..8aac65dd2b 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift @@ -30,10 +30,16 @@ final class LiquidityPoolsListViewLayout: UIView { view.backgroundColor = .clear view.separatorStyle = .none view.contentInset = .zero - view.refreshControl = UIRefreshControl() return view }() + private let backgroundImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFill + imageView.image = R.image.backgroundImage() + return imageView + }() + var locale: Locale = .current { didSet { applyLocalization() @@ -42,8 +48,6 @@ final class LiquidityPoolsListViewLayout: UIView { override init(frame: CGRect) { super.init(frame: frame) -// backgroundColor = R.color.colorWhite4() - drawSubviews() setupConstraints() } @@ -56,6 +60,10 @@ final class LiquidityPoolsListViewLayout: UIView { func bind(viewModel: LiquidityPoolListViewModel) { titleLabel.text = viewModel.titleLabelText moreButton.isHidden = !viewModel.moreButtonVisible + backgroundImageView.isHidden = !viewModel.backgroundVisible + + tableView.refreshControl = viewModel.refreshAvailable ? UIRefreshControl() : nil + tableView.isScrollEnabled = viewModel.refreshAvailable } override func layoutSubviews() { @@ -64,6 +72,7 @@ final class LiquidityPoolsListViewLayout: UIView { } private func drawSubviews() { + addSubview(backgroundImageView) addSubview(topBar) addSubview(tableView) @@ -72,9 +81,14 @@ final class LiquidityPoolsListViewLayout: UIView { } private func setupConstraints() { + backgroundImageView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + topBar.snp.makeConstraints { make in + make.top.equalTo(safeAreaLayoutGuide) make.height.equalTo(42) - make.leading.trailing.top.equalToSuperview() + make.leading.trailing.equalToSuperview() } tableView.snp.makeConstraints { make in diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListCellModel.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListCellModel.swift index cf96499c71..b3f5d71adc 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListCellModel.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListCellModel.swift @@ -1,4 +1,6 @@ import Foundation +import SSFPools +import SSFPolkaswap struct LiquidityPoolListCellModel { let tokenPairIconsVieWModel: TokenPairsIconViewModel @@ -8,4 +10,5 @@ struct LiquidityPoolListCellModel { let stakingStatusLabelText: String? let reservesLabelValue: ShimmeredLabelState? let sortValue: Decimal + let liquidityPair: LiquidityPair } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListType.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListType.swift new file mode 100644 index 0000000000..b9d77904a2 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListType.swift @@ -0,0 +1,6 @@ +import Foundation + +enum LiquidityPoolListType { + case embed + case full +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListViewModel.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListViewModel.swift index 377fb42fbf..44bcd9ffc4 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListViewModel.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListViewModel.swift @@ -4,4 +4,6 @@ struct LiquidityPoolListViewModel { let poolViewModels: [LiquidityPoolListCellModel]? let titleLabelText: String let moreButtonVisible: Bool + let backgroundVisible: Bool + let refreshAvailable: Bool } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewAssembly.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewAssembly.swift index 0e2932e1a7..e90500b8f6 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewAssembly.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewAssembly.swift @@ -3,7 +3,12 @@ import SoraFoundation import SSFModels final class LiquidityPoolsOverviewAssembly { - static func configureModule(chain: ChainModel, wallet: MetaAccountModel) -> LiquidityPoolsOverviewModuleCreationResult? { + static func configureModule(wallet: MetaAccountModel) -> LiquidityPoolsOverviewModuleCreationResult? { + let chainRegistry = ChainRegistryFacade.sharedRegistry + guard let chain = chainRegistry.availableChains.first(where: { $0.chainId == Chain.soraMain.genesisHash }) else { + return nil + } + let localizationManager = LocalizationManager.shared let interactor = LiquidityPoolsOverviewInteractor() @@ -12,11 +17,13 @@ final class LiquidityPoolsOverviewAssembly { let presenter = LiquidityPoolsOverviewPresenter( interactor: interactor, router: router, - localizationManager: localizationManager + localizationManager: localizationManager, + chain: chain, + wallet: wallet ) - let userPoolsModule = configureUserPoolsModule(chain: chain, wallet: wallet) - let availablePoolsModule = configureAvailablePoolsModule(chain: chain, wallet: wallet) + let userPoolsModule = configureUserPoolsModule(chain: chain, wallet: wallet, moduleOutput: presenter) + let availablePoolsModule = configureAvailablePoolsModule(chain: chain, wallet: wallet, moduleOutput: presenter) guard let userPoolsModule, let availablePoolsModule else { return nil @@ -34,15 +41,17 @@ final class LiquidityPoolsOverviewAssembly { private static func configureUserPoolsModule( chain: ChainModel, - wallet: MetaAccountModel + wallet: MetaAccountModel, + moduleOutput: LiquidityPoolsListModuleOutput? ) -> LiquidityPoolsListModuleCreationResult? { - LiquidityPoolsListAssembly.configureUserPoolsModule(chain: chain, wallet: wallet) + LiquidityPoolsListAssembly.configureUserPoolsModule(chain: chain, wallet: wallet, moduleOutput: moduleOutput, type: .embed) } private static func configureAvailablePoolsModule( chain: ChainModel, - wallet: MetaAccountModel + wallet: MetaAccountModel, + moduleOutput: LiquidityPoolsListModuleOutput? ) -> LiquidityPoolsListModuleCreationResult? { - LiquidityPoolsListAssembly.configureAvailablePoolsModule(chain: chain, wallet: wallet) + LiquidityPoolsListAssembly.configureAvailablePoolsModule(chain: chain, wallet: wallet, moduleOutput: moduleOutput, type: .embed) } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewPresenter.swift index 3ae1fe6115..2fcc301bb9 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewPresenter.swift @@ -1,5 +1,6 @@ import Foundation import SoraFoundation +import SSFModels final class LiquidityPoolsOverviewPresenter { // MARK: Private properties @@ -7,16 +8,23 @@ final class LiquidityPoolsOverviewPresenter { private weak var view: LiquidityPoolsOverviewViewInput? private let router: LiquidityPoolsOverviewRouterInput private let interactor: LiquidityPoolsOverviewInteractorInput + private let chain: ChainModel + private let wallet: MetaAccountModel // MARK: - Constructors init( interactor: LiquidityPoolsOverviewInteractorInput, router: LiquidityPoolsOverviewRouterInput, - localizationManager: LocalizationManagerProtocol + localizationManager: LocalizationManagerProtocol, + chain: ChainModel, + wallet: MetaAccountModel ) { self.interactor = interactor self.router = router + self.chain = chain + self.wallet = wallet + self.localizationManager = localizationManager } @@ -43,3 +51,13 @@ extension LiquidityPoolsOverviewPresenter: Localizable { } extension LiquidityPoolsOverviewPresenter: LiquidityPoolsOverviewModuleInput {} + +extension LiquidityPoolsOverviewPresenter: LiquidityPoolsListModuleOutput { + func didTapMoreUserPools() { + router.showAllUserPools(chain: chain, wallet: wallet, from: view, moduleOutput: self) + } + + func didTapMoreAvailablePools() { + router.showAllAvailablePools(chain: chain, wallet: wallet, from: view, moduleOutput: self) + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewProtocols.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewProtocols.swift index df67f0f1db..1e2eae403c 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewProtocols.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewProtocols.swift @@ -1,3 +1,5 @@ +import SSFModels + typealias LiquidityPoolsOverviewModuleCreationResult = (view: LiquidityPoolsOverviewViewInput, input: LiquidityPoolsOverviewModuleInput) protocol LiquidityPoolsOverviewViewInput: ControllerBackedProtocol {} @@ -12,7 +14,20 @@ protocol LiquidityPoolsOverviewInteractorInput: AnyObject { protocol LiquidityPoolsOverviewInteractorOutput: AnyObject {} -protocol LiquidityPoolsOverviewRouterInput: AnyObject {} +protocol LiquidityPoolsOverviewRouterInput: AnyObject { + func showAllAvailablePools( + chain: ChainModel, + wallet: MetaAccountModel, + from view: ControllerBackedProtocol?, + moduleOutput: LiquidityPoolsListModuleOutput? + ) + func showAllUserPools( + chain: ChainModel, + wallet: MetaAccountModel, + from view: ControllerBackedProtocol?, + moduleOutput: LiquidityPoolsListModuleOutput? + ) +} protocol LiquidityPoolsOverviewModuleInput: AnyObject {} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewRouter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewRouter.swift index 9c3359bd88..c6c9933337 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewRouter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewRouter.swift @@ -1,3 +1,40 @@ import Foundation +import SSFModels -final class LiquidityPoolsOverviewRouter: LiquidityPoolsOverviewRouterInput {} +final class LiquidityPoolsOverviewRouter: LiquidityPoolsOverviewRouterInput { + func showAllAvailablePools( + chain: ChainModel, + wallet: MetaAccountModel, + from view: ControllerBackedProtocol?, + moduleOutput: LiquidityPoolsListModuleOutput? + ) { + guard let controller = LiquidityPoolsListAssembly.configureAvailablePoolsModule( + chain: chain, + wallet: wallet, + moduleOutput: moduleOutput, + type: .full + )?.view.controller else { + return + } + + view?.controller.navigationController?.pushViewController(controller, animated: true) + } + + func showAllUserPools( + chain: ChainModel, + wallet: MetaAccountModel, + from view: ControllerBackedProtocol?, + moduleOutput: LiquidityPoolsListModuleOutput? + ) { + guard let controller = LiquidityPoolsListAssembly.configureUserPoolsModule( + chain: chain, + wallet: wallet, + moduleOutput: moduleOutput, + type: .full + )?.view.controller else { + return + } + + view?.controller.navigationController?.pushViewController(controller, animated: true) + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewLayout.swift index 05851388cc..54bb766ac4 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewLayout.swift @@ -1,6 +1,18 @@ import UIKit final class LiquidityPoolsOverviewViewLayout: UIView { + let navigationBar: BaseNavigationBar = { + let bar = BaseNavigationBar() + bar.backgroundColor = R.color.colorBlack19() + return bar + }() + + let polkaswapImageView: UIImageView = { + let imageView = UIImageView() + imageView.image = R.image.polkaswap() + return imageView + }() + private let backgroundImageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFill @@ -40,10 +52,13 @@ final class LiquidityPoolsOverviewViewLayout: UIView { private func drawSubviews() { addSubview(backgroundImageView) + addSubview(navigationBar) addSubview(scrollView) scrollView.addSubview(scrollViewBackgroundView) scrollViewBackgroundView.addSubview(userPoolsContainerView) scrollViewBackgroundView.addSubview(availablePoolsContainerView) + + navigationBar.setLeftViews([polkaswapImageView]) } private func setupConstraints() { @@ -51,8 +66,13 @@ final class LiquidityPoolsOverviewViewLayout: UIView { make.edges.equalToSuperview() } + navigationBar.snp.makeConstraints { make in + make.leading.trailing.top.equalToSuperview() + } + scrollView.snp.makeConstraints { make in - make.edges.equalToSuperview() + make.leading.trailing.bottom.equalToSuperview() + make.top.equalTo(navigationBar.snp.bottom) } scrollViewBackgroundView.snp.makeConstraints { make in @@ -65,8 +85,9 @@ final class LiquidityPoolsOverviewViewLayout: UIView { } availablePoolsContainerView.snp.makeConstraints { make in - make.leading.trailing.bottom.equalToSuperview().inset(16) + make.leading.trailing.equalToSuperview().inset(16) make.top.equalTo(userPoolsContainerView.snp.bottom).offset(16) + make.bottom.lessThanOrEqualToSuperview().offset(16) } } } diff --git a/fearless/Modules/MainTabBar/MainTabBarWireframe.swift b/fearless/Modules/MainTabBar/MainTabBarWireframe.swift index 25f35201e3..827750678a 100644 --- a/fearless/Modules/MainTabBar/MainTabBarWireframe.swift +++ b/fearless/Modules/MainTabBar/MainTabBarWireframe.swift @@ -6,12 +6,20 @@ final class MainTabBarWireframe: MainTabBarWireframeProtocol { func presentPolkaswap(on view: ControllerBackedProtocol?, wallet: MetaAccountModel) { guard let tabBarController = view?.controller, - let viewController = PolkaswapAdjustmentAssembly.configureModule(chainAsset: nil, wallet: wallet)?.view.controller + let viewController = LiquidityPoolsOverviewAssembly.configureModule(wallet: wallet)?.view.controller else { return } - let navigationController = FearlessNavigationController(rootViewController: viewController) + +// guard +// let tabBarController = view?.controller, +// let viewController = PolkaswapAdjustmentAssembly.configureModule(chainAsset: nil, wallet: wallet)?.view.controller +// else { +// return +// } +// +// let navigationController = FearlessNavigationController(rootViewController: viewController) let presentingController = tabBarController.topModalViewController presentingController.present(navigationController, animated: true, completion: nil) } diff --git a/fearless/Modules/NewWallet/ChainAccount/ChainAccountWireframe.swift b/fearless/Modules/NewWallet/ChainAccount/ChainAccountWireframe.swift index 1366193ce2..4df0cde6ce 100644 --- a/fearless/Modules/NewWallet/ChainAccount/ChainAccountWireframe.swift +++ b/fearless/Modules/NewWallet/ChainAccount/ChainAccountWireframe.swift @@ -221,7 +221,7 @@ final class ChainAccountWireframe: ChainAccountWireframeProtocol { chainAsset: ChainAsset, wallet: MetaAccountModel ) { - guard let module = LiquidityPoolsOverviewAssembly.configureModule(chain: chainAsset.chain, wallet: wallet) else { + guard let module = PolkaswapAdjustmentAssembly.configureModule(chainAsset: chainAsset, wallet: wallet) else { return } let navigationController = FearlessNavigationController(rootViewController: module.view.controller) From cbf4a80887974c88a14a8cb5eae3ebe52d5b5e02 Mon Sep 17 00:00:00 2001 From: Alex Lebedko Date: Mon, 20 May 2024 13:21:08 +0700 Subject: [PATCH 008/115] lp supply module --- fearless.xcodeproj/project.pbxproj | 48 +++ .../LiquidityPoolSupplyAssembly.swift | 24 ++ .../LiquidityPoolSupplyInteractor.swift | 21 ++ .../LiquidityPoolSupplyPresenter.swift | 331 ++++++++++++++++++ .../LiquidityPoolSupplyProtocols.swift | 19 + .../LiquidityPoolSupplyRouter.swift | 26 ++ .../LiquidityPoolSupplyViewController.swift | 232 ++++++++++++ .../LiquidityPoolSupplyViewLayout.swift | 296 ++++++++++++++++ .../PolkaswapAdjustmentViewController.swift | 1 - .../LiquidityPoolSupplyTests.swift | 16 + 10 files changed, 1013 insertions(+), 1 deletion(-) create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyAssembly.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyInteractor.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyProtocols.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyRouter.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift create mode 100644 fearlessTests/Modules/LiquidityPoolSupply/LiquidityPoolSupplyTests.swift diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index 541c4b98b6..dada13f5eb 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -223,6 +223,7 @@ 0D02CBA6399C508D9F25AC8D /* PolkaswapAdjustmentAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FC428EB695A80397BDC621C /* PolkaswapAdjustmentAssembly.swift */; }; 0D05777212DFA8CFC5A692E3 /* WalletsManagmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2970944BE4B937A39E07C608 /* WalletsManagmentViewController.swift */; }; 0D6C27413F73190438306EC1 /* NetworkInfoInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA8D61B74FE9C5199FD0AEBC /* NetworkInfoInteractor.swift */; }; + 0D7DDA00BBF1D0CFD9A26306 /* LiquidityPoolSupplyProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FB7091F743BBACC09553298 /* LiquidityPoolSupplyProtocols.swift */; }; 0DAA7B1B7DF576C761DEF046 /* WalletSendConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A22A2EB487E282DCA93C676 /* WalletSendConfirmWireframe.swift */; }; 0DAEDA34F5BCECE5BD64DF26 /* WalletTransactionHistoryWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B3B14C046584AAAF483715F /* WalletTransactionHistoryWireframe.swift */; }; 0E30EB59B860DE611FF0DC7D /* NftCollectionRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92E3687D398A32597A888B82 /* NftCollectionRouter.swift */; }; @@ -384,6 +385,7 @@ 5C796EF8ED29F564B5D1126B /* CrowdloanContributionConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F75722D2F921FD1C2D4105D /* CrowdloanContributionConfirmViewController.swift */; }; 5D0665A581B9F8DFDBD0CF7B /* WalletChainAccountDashboardWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885A9E2D3619FEFC5ED0C093 /* WalletChainAccountDashboardWireframe.swift */; }; 5DDD2206DF795CF205610455 /* AccountExportPasswordPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31226053044986BC828AA912 /* AccountExportPasswordPresenter.swift */; }; + 5E8504507116E0177D70314B /* LiquidityPoolSupplyViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438E01C5C877428168E9F3F8 /* LiquidityPoolSupplyViewLayout.swift */; }; 5E9402965D385607E04156DC /* NftDetailsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DEDC1A0ED429DD43EC621E /* NftDetailsPresenter.swift */; }; 5F5825D27863628412B672CA /* NftSendConfirmRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 803E71983CD61FFBFE98DA7A /* NftSendConfirmRouter.swift */; }; 607699C7CEEDA3598613DCA0 /* NetworkInfoViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EDA19BC3280F1838C687EC8 /* NetworkInfoViewFactory.swift */; }; @@ -396,6 +398,7 @@ 64B7826F78B8AE649B1EF08F /* CrowdloanContributionSetupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C01DCD4DA014E8FB50B9F11 /* CrowdloanContributionSetupTests.swift */; }; 65909D701527D99837B439D9 /* StakingRewardDetailsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 638A65DAC86BAF9EB4D2F2F8 /* StakingRewardDetailsWireframe.swift */; }; 65E0BC7A96EDE5E52D32A11B /* AllDoneViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A5A7C701EF966BF48D6B9E /* AllDoneViewController.swift */; }; + 66AECEC6A6EB8184114B041E /* LiquidityPoolSupplyRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6503D178156C6407EC848D41 /* LiquidityPoolSupplyRouter.swift */; }; 69DE177B9D1745FEE848E870 /* WalletTransactionDetailsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B37BB3FF5CDF7EA9D7371B7 /* WalletTransactionDetailsInteractor.swift */; }; 69EF1DC4093AC9AF06D71CF4 /* AnalyticsRewardDetailsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29100320799A2B46836A257B /* AnalyticsRewardDetailsViewFactory.swift */; }; 6A16194632D42862692CC067 /* PolkaswapSwapConfirmationInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9B5FD28F2B9A0B0D8ED3607 /* PolkaswapSwapConfirmationInteractor.swift */; }; @@ -1309,6 +1312,7 @@ 85A093F6055DDD2E2E9253F2 /* ControllerAccountProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = F829E7F8B39EE7D977001510 /* ControllerAccountProtocols.swift */; }; 85B1B7387F09A8405C4E688A /* WalletSendConfirmTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AF4258723E2FACBBA556D00 /* WalletSendConfirmTests.swift */; }; 872DF7DE5A001DF5B8A4E288 /* StakingBondMoreConfirmationFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BA5883C1103D3A2218D839 /* StakingBondMoreConfirmationFlow.swift */; }; + 87C1FC2909A8360DDBA625E5 /* LiquidityPoolSupplyInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BA528679A82B9A327853804 /* LiquidityPoolSupplyInteractor.swift */; }; 885551F78A5926D16D5AF0CB /* ControllerAccountPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E5CB64B91B35804B3671456 /* ControllerAccountPresenter.swift */; }; 886E8CF81EF2566D98D9693E /* ExportSeedViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FA66143B25AA70B02CE461 /* ExportSeedViewFactory.swift */; }; 887DCCC498EB8472021DCE3E /* PolkaswapSwapConfirmationRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC50F2FF6E8EBC00B56CB86D /* PolkaswapSwapConfirmationRouter.swift */; }; @@ -1530,6 +1534,7 @@ B7E42D9E7CA509D7BE723357 /* PolkaswapTransaktionSettingsRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EFBB5CE05706ADFEF00796 /* PolkaswapTransaktionSettingsRouter.swift */; }; B893A2515909AB6915196317 /* NetworkInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B0E2EDF787BF82F16663215 /* NetworkInfoViewController.swift */; }; BA7AEE82627CFC0AFD69B299 /* RecommendedValidatorListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2580363AC3E4A9CD40256E /* RecommendedValidatorListPresenter.swift */; }; + BC2DF589C6623601C39EF8F4 /* LiquidityPoolSupplyPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F848482B2AD7D6831B0CCE /* LiquidityPoolSupplyPresenter.swift */; }; BD571417BD18C711B76E1D62 /* ExportSeedWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B4C1B5D56DB69BA0AECF731 /* ExportSeedWireframe.swift */; }; BDE80F08EBEE3B0C95598EA8 /* WalletSendConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE152D16E6C78D297BFFC3C /* WalletSendConfirmInteractor.swift */; }; BE3F6213B26F35EB6324DBD8 /* ControllerAccountWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDB9EDB05686DF11958145E1 /* ControllerAccountWireframe.swift */; }; @@ -1689,6 +1694,7 @@ D83B47B07C0D40A327AC44F7 /* CustomValidatorListViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F52B8815D6AF5E69B145D245 /* CustomValidatorListViewFactory.swift */; }; D8581E5440A19D977E17BFDE /* StakingAmountViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D39D54DC9992CF9CB6699AA3 /* StakingAmountViewFactory.swift */; }; D886425A55425810AD070AB5 /* ControllerAccountConfirmationWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96C3B5ABF4A8124848EFD17 /* ControllerAccountConfirmationWireframe.swift */; }; + D8C33C4064C3B2F30BB478A5 /* LiquidityPoolSupplyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C4CAC8978B0848DF5FD6FE /* LiquidityPoolSupplyTests.swift */; }; D9E803290BE797D889EA372D /* AssetSelectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2655EE5CFAAD20C0FF59188 /* AssetSelectionTests.swift */; }; DA5B38EE4622B33AFCA11A50 /* WalletsManagmentPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41E19988955B1C159EDA2555 /* WalletsManagmentPresenter.swift */; }; DA62812C23210601F4ECF84D /* ContactsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B06C949668CFFDE6F739CC0 /* ContactsProtocols.swift */; }; @@ -1700,6 +1706,7 @@ DBA6A0A26D77E7A587C51792 /* PolkaswapSwapConfirmationProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA6FC016067C632AF256EB62 /* PolkaswapSwapConfirmationProtocols.swift */; }; DBBD1651F45FECA1B17AAF40 /* CreateContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 480AE579B48B3DA9C247CCB5 /* CreateContactViewController.swift */; }; DCDAE9E8C805B71A8F8CEFBD /* YourValidatorListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890203CBFFCBF517C0BAA396 /* YourValidatorListTests.swift */; }; + DCE13AA9F3BA0EB54F793017 /* LiquidityPoolSupplyAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62928FB3556EEA3A228131AC /* LiquidityPoolSupplyAssembly.swift */; }; DD1ADD4F777B0C6398C73805 /* NftDetailsRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B08B264C09E63A936384E2A /* NftDetailsRouter.swift */; }; DD96B37BBEE1535951802B55 /* AllDoneProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C90AA5852CFA841CED20631 /* AllDoneProtocols.swift */; }; DDEEF4532805420415471B6A /* NftDetailsAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194E6EDD65CA3FB0B1F6727C /* NftDetailsAssembly.swift */; }; @@ -1740,6 +1747,7 @@ F0C3DB0CEE1975626B0014A8 /* StakingUnbondConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C34D496D0F57E685237B3A7 /* StakingUnbondConfirmInteractor.swift */; }; F17C7FA0DB540A803558D1BB /* AnalyticsRewardDetailsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85211D55E2AF0A697FB3EB84 /* AnalyticsRewardDetailsPresenter.swift */; }; F20C8D17ABF18B7104E14394 /* StakingAmountInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 312DE7ADA5ABC3214AD3D4AD /* StakingAmountInteractor.swift */; }; + F31469BD18062A4A008FE39E /* LiquidityPoolSupplyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497EFA05A2D8076BFE82964D /* LiquidityPoolSupplyViewController.swift */; }; F38273DB7A8B139B9FD8FBE2 /* MainNftContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFC9C09ABBCEB6E581134E84 /* MainNftContainerViewController.swift */; }; F3D2AC37709EAF088A594B73 /* AccountManagementViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FCA2DD3A8898D64CBC9F97 /* AccountManagementViewController.swift */; }; F3E88A15A597C7B9B92C097B /* NetworkIssuesNotificationProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = F02C3AF74DE2F2CDBD165803 /* NetworkIssuesNotificationProtocols.swift */; }; @@ -3408,6 +3416,7 @@ 31226053044986BC828AA912 /* AccountExportPasswordPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountExportPasswordPresenter.swift; sourceTree = ""; }; 312DE7ADA5ABC3214AD3D4AD /* StakingAmountInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingAmountInteractor.swift; sourceTree = ""; }; 31302AE4D5325D2AEC030832 /* AllDoneRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AllDoneRouter.swift; sourceTree = ""; }; + 31F848482B2AD7D6831B0CCE /* LiquidityPoolSupplyPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyPresenter.swift; sourceTree = ""; }; 320E2207FB549F7C31A80441 /* SelectMarketViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectMarketViewController.swift; sourceTree = ""; }; 3211D250E4167C916B8B9D6A /* NetworkIssuesNotificationRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NetworkIssuesNotificationRouter.swift; sourceTree = ""; }; 326260E461C031624CDB62BA /* WalletOptionInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletOptionInteractor.swift; sourceTree = ""; }; @@ -3446,6 +3455,7 @@ 40B47961B2254E8A4D8EC588 /* StakingAmountPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingAmountPresenter.swift; sourceTree = ""; }; 4191E0055768541F6A3D8A61 /* StakingRewardPayoutsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRewardPayoutsInteractor.swift; sourceTree = ""; }; 41E19988955B1C159EDA2555 /* WalletsManagmentPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletsManagmentPresenter.swift; sourceTree = ""; }; + 438E01C5C877428168E9F3F8 /* LiquidityPoolSupplyViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyViewLayout.swift; sourceTree = ""; }; 446B4A7184327D64AE8F0610 /* WarningAlertProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WarningAlertProtocols.swift; sourceTree = ""; }; 447D03660EFFD8CF46374D85 /* NftSendConfirmViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftSendConfirmViewController.swift; sourceTree = ""; }; 447FE0DC90DF4B4D13CADE62 /* NodeSelectionInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NodeSelectionInteractor.swift; sourceTree = ""; }; @@ -3457,6 +3467,7 @@ 480AE579B48B3DA9C247CCB5 /* CreateContactViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateContactViewController.swift; sourceTree = ""; }; 48C158C8D1855BCE53636934 /* AccountCreateProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountCreateProtocols.swift; sourceTree = ""; }; 4952E0B8F8DCD92FE4A37892 /* AddCustomNodeViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddCustomNodeViewLayout.swift; sourceTree = ""; }; + 497EFA05A2D8076BFE82964D /* LiquidityPoolSupplyViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyViewController.swift; sourceTree = ""; }; 49C564052FAA3160AA8975CB /* NftSendAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftSendAssembly.swift; sourceTree = ""; }; 49EBB77A32A59568B0DACFE5 /* StakingPoolCreateProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPoolCreateProtocols.swift; sourceTree = ""; }; 4B3B14C046584AAAF483715F /* WalletTransactionHistoryWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletTransactionHistoryWireframe.swift; sourceTree = ""; }; @@ -3518,12 +3529,14 @@ 61E1CD099F7C30ABE0E8A001 /* StakingUnbondSetupTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondSetupTests.swift; sourceTree = ""; }; 61EBE466BDCF77E65FDCDF81 /* ExportMnemonicPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExportMnemonicPresenter.swift; sourceTree = ""; }; 6216F6F1B91F798F07695FB6 /* StakingAmountWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingAmountWireframe.swift; sourceTree = ""; }; + 62928FB3556EEA3A228131AC /* LiquidityPoolSupplyAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyAssembly.swift; sourceTree = ""; }; 62FA66143B25AA70B02CE461 /* ExportSeedViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExportSeedViewFactory.swift; sourceTree = ""; }; 638A65DAC86BAF9EB4D2F2F8 /* StakingRewardDetailsWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRewardDetailsWireframe.swift; sourceTree = ""; }; 639AD37D87BD08106E7E6E2A /* WarningAlertWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WarningAlertWireframe.swift; sourceTree = ""; }; 63B11A7ADEF107B6341C378F /* Pods-fearlessAll-fearlessIntegrationTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-fearlessAll-fearlessIntegrationTests.release.xcconfig"; path = "Target Support Files/Pods-fearlessAll-fearlessIntegrationTests/Pods-fearlessAll-fearlessIntegrationTests.release.xcconfig"; sourceTree = ""; }; 63CCCC63389A70294F816143 /* NodeSelectionProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NodeSelectionProtocols.swift; sourceTree = ""; }; 63F4BE52D0625CD8C21D2460 /* CrowdloanListViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanListViewLayout.swift; sourceTree = ""; }; + 6503D178156C6407EC848D41 /* LiquidityPoolSupplyRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyRouter.swift; sourceTree = ""; }; 65AD15693E21C869DE1FDD17 /* UsernameSetupWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UsernameSetupWireframe.swift; sourceTree = ""; }; 66FFB904E5A83F2EFBCCBBF8 /* StakingPoolInfoTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPoolInfoTests.swift; sourceTree = ""; }; 6717FF1B7777400B62F028C3 /* LiquidityPoolsOverviewInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsOverviewInteractor.swift; sourceTree = ""; }; @@ -4222,6 +4235,7 @@ 84C3F78B26020DE800D47501 /* StakingStateMachineProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingStateMachineProtocols.swift; sourceTree = ""; }; 84C4C2D6255D2B780045B582 /* PinChangeWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinChangeWireframe.swift; sourceTree = ""; }; 84C4C2F8255DB9510045B582 /* PinChangeInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinChangeInteractor.swift; sourceTree = ""; }; + 84C4CAC8978B0848DF5FD6FE /* LiquidityPoolSupplyTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyTests.swift; sourceTree = ""; }; 84C515FA26D84F8C000DBA45 /* AccountImportWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountImportWrapper.swift; sourceTree = ""; }; 84C6800D24D6ECE800006BF5 /* ExpandableActionControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableActionControl.swift; sourceTree = ""; }; 84C6800F24D6EE4500006BF5 /* PlusIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlusIndicatorView.swift; sourceTree = ""; }; @@ -4458,6 +4472,7 @@ 8E83833EB33E51A12F96F83B /* WalletChainAccountDashboardPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletChainAccountDashboardPresenter.swift; sourceTree = ""; }; 8F04623D6CE10669D914CA2F /* PolkaswapTransaktionSettingsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PolkaswapTransaktionSettingsViewController.swift; sourceTree = ""; }; 8F0FEFEAEC5CDCD7C9BF77D5 /* PolkaswapSwapConfirmationPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PolkaswapSwapConfirmationPresenter.swift; sourceTree = ""; }; + 8FB7091F743BBACC09553298 /* LiquidityPoolSupplyProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyProtocols.swift; sourceTree = ""; }; 900E67A76C0702F8123EBB47 /* AddCustomNodeInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddCustomNodeInteractor.swift; sourceTree = ""; }; 906C55FC079AF6112AF0745B /* YourValidatorListWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = YourValidatorListWireframe.swift; sourceTree = ""; }; 909ABB38BF714F553063697F /* StakingRebondConfirmationFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRebondConfirmationFlow.swift; sourceTree = ""; }; @@ -4484,6 +4499,7 @@ 9A9EBD3B7AEA5EF594DFEB49 /* PolkaswapAdjustmentProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PolkaswapAdjustmentProtocols.swift; sourceTree = ""; }; 9AEA7ECB8434DF494D2B25B9 /* ChainAccountTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChainAccountTests.swift; sourceTree = ""; }; 9B5626189788682A84D4E9D7 /* SelectValidatorsConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectValidatorsConfirmPresenter.swift; sourceTree = ""; }; + 9BA528679A82B9A327853804 /* LiquidityPoolSupplyInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyInteractor.swift; sourceTree = ""; }; 9C01DCD4DA014E8FB50B9F11 /* CrowdloanContributionSetupTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionSetupTests.swift; sourceTree = ""; }; 9C05A688EA7379572BBCE545 /* SelectMarketRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectMarketRouter.swift; sourceTree = ""; }; 9FED48DE9B681995E6E4A581 /* LiquidityPoolDetailsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolDetailsProtocols.swift; sourceTree = ""; }; @@ -6298,6 +6314,14 @@ path = StakingUnbondSetup; sourceTree = ""; }; + 04265CAB3B55A71F49337293 /* LiquidityPoolSupply */ = { + isa = PBXGroup; + children = ( + 84C4CAC8978B0848DF5FD6FE /* LiquidityPoolSupplyTests.swift */, + ); + path = LiquidityPoolSupply; + sourceTree = ""; + }; 046ABDDCA893D13536190B6C /* NftCollection */ = { isa = PBXGroup; children = ( @@ -7688,6 +7712,7 @@ D734A66128ED3C8653098101 /* ClaimCrowdloanRewards */, 9468F26496DF90FEE7016AA3 /* LiquidityPoolsOverview */, 9439C16432098735E8F4C122 /* LiquidityPoolDetails */, + 04265CAB3B55A71F49337293 /* LiquidityPoolSupply */, ); path = Modules; sourceTree = ""; @@ -11940,6 +11965,20 @@ path = WalletsManagment; sourceTree = ""; }; + EE3CBEFADE55F44DE05DCEF2 /* LiquidityPoolSupply */ = { + isa = PBXGroup; + children = ( + 8FB7091F743BBACC09553298 /* LiquidityPoolSupplyProtocols.swift */, + 6503D178156C6407EC848D41 /* LiquidityPoolSupplyRouter.swift */, + 31F848482B2AD7D6831B0CCE /* LiquidityPoolSupplyPresenter.swift */, + 9BA528679A82B9A327853804 /* LiquidityPoolSupplyInteractor.swift */, + 497EFA05A2D8076BFE82964D /* LiquidityPoolSupplyViewController.swift */, + 438E01C5C877428168E9F3F8 /* LiquidityPoolSupplyViewLayout.swift */, + 62928FB3556EEA3A228131AC /* LiquidityPoolSupplyAssembly.swift */, + ); + path = LiquidityPoolSupply; + sourceTree = ""; + }; EFC2669335EE7F7297DF423E /* CrowdloanContributionSetup */ = { isa = PBXGroup; children = ( @@ -12540,6 +12579,7 @@ FA1D51D42BCFD410001353E7 /* LiquidityPools */ = { isa = PBXGroup; children = ( + EE3CBEFADE55F44DE05DCEF2 /* LiquidityPoolSupply */, 478C62A42D572C8647512722 /* LiquidityPoolDetails */, 675669EAE3DAF27168F1B390 /* LiquidityPoolsOverview */, FA1D51D52BCFD41F001353E7 /* Common */, @@ -18923,6 +18963,13 @@ A1C05D0028CD04C16AB6082F /* LiquidityPoolDetailsViewController.swift in Sources */, 3A4026E96615A1D53DAF12D8 /* LiquidityPoolDetailsViewLayout.swift in Sources */, 7DCC939E882247E141B1DE34 /* LiquidityPoolDetailsAssembly.swift in Sources */, + 0D7DDA00BBF1D0CFD9A26306 /* LiquidityPoolSupplyProtocols.swift in Sources */, + 66AECEC6A6EB8184114B041E /* LiquidityPoolSupplyRouter.swift in Sources */, + BC2DF589C6623601C39EF8F4 /* LiquidityPoolSupplyPresenter.swift in Sources */, + 87C1FC2909A8360DDBA625E5 /* LiquidityPoolSupplyInteractor.swift in Sources */, + F31469BD18062A4A008FE39E /* LiquidityPoolSupplyViewController.swift in Sources */, + 5E8504507116E0177D70314B /* LiquidityPoolSupplyViewLayout.swift in Sources */, + DCE13AA9F3BA0EB54F793017 /* LiquidityPoolSupplyAssembly.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -19095,6 +19142,7 @@ 99DCBCC3298620721B213012 /* ClaimCrowdloanRewardsTests.swift in Sources */, 60C22E112CA857A2EA5A129E /* LiquidityPoolsOverviewTests.swift in Sources */, 6D0E50CEBCB73C23A75A7F46 /* LiquidityPoolDetailsTests.swift in Sources */, + D8C33C4064C3B2F30BB478A5 /* LiquidityPoolSupplyTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyAssembly.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyAssembly.swift new file mode 100644 index 0000000000..53ce0a3c9e --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyAssembly.swift @@ -0,0 +1,24 @@ +import UIKit +import SoraFoundation + +final class LiquidityPoolSupplyAssembly { + static func configureModule() -> LiquidityPoolSupplyModuleCreationResult? { + let localizationManager = LocalizationManager.shared + + let interactor = LiquidityPoolSupplyInteractor() + let router = LiquidityPoolSupplyRouter() + + let presenter = LiquidityPoolSupplyPresenter( + interactor: interactor, + router: router, + localizationManager: localizationManager + ) + + let view = LiquidityPoolSupplyViewController( + output: presenter, + localizationManager: localizationManager + ) + + return (view, presenter) + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyInteractor.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyInteractor.swift new file mode 100644 index 0000000000..9d433aa385 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyInteractor.swift @@ -0,0 +1,21 @@ +import UIKit +import SSFPools + +protocol LiquidityPoolSupplyInteractorOutput: AnyObject {} + +final class LiquidityPoolSupplyInteractor { + // MARK: - Private properties + private weak var output: LiquidityPoolSupplyInteractorOutput? + private let lpService: PoolsOperationService + + init(lpService: PoolsOperationService) { + + } +} + +// MARK: - LiquidityPoolSupplyInteractorInput +extension LiquidityPoolSupplyInteractor: LiquidityPoolSupplyInteractorInput { + func setup(with output: LiquidityPoolSupplyInteractorOutput) { + self.output = output + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift new file mode 100644 index 0000000000..fabc108bd8 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift @@ -0,0 +1,331 @@ +import Foundation +import SoraFoundation +import SSFModels + +protocol LiquidityPoolSupplyViewInput: ControllerBackedProtocol { + func didReceiveSwapFrom(viewModel: AssetBalanceViewModelProtocol?) + func didReceiveSwapTo(viewModel: AssetBalanceViewModelProtocol?) + func didReceiveSwapFrom(amountInputViewModel: IAmountInputViewModel?) + func didReceiveSwapTo(amountInputViewModel: IAmountInputViewModel?) + func didReceiveNetworkFee(fee: BalanceViewModelProtocol?) + func setButtonLoadingState(isLoading: Bool) + func didUpdating() +} + +protocol LiquidityPoolSupplyInteractorInput: AnyObject { + func setup(with output: LiquidityPoolSupplyInteractorOutput) +} + +final class LiquidityPoolSupplyPresenter { + private enum InputTag: Int { + case swapFrom = 0 + case swapTo + } + + private enum Constants { + static let slippadgeTolerance: Float = 0.5 + } + + // MARK: Private properties + private weak var view: LiquidityPoolSupplyViewInput? + private let router: LiquidityPoolSupplyRouterInput + private let interactor: LiquidityPoolSupplyInteractorInput + private let dataValidatingFactory: SendDataValidatingFactory + private let logger: LoggerProtocol + + private let wallet: MetaAccountModel + private let xorChainAsset: ChainAsset + private var swapFromChainAsset: ChainAsset? + private var swapToChainAsset: ChainAsset? + private var prices: [PriceData]? + private var swapVariant: SwapVariant = .desiredInput + + private var slippadgeTolerance: Float = Constants.slippadgeTolerance + private var swapFromInputResult: AmountInputResult? + private var swapFromBalance: Decimal? + private var swapToInputResult: AmountInputResult? + private var swapToBalance: Decimal? + + private var networkFeeViewModel: BalanceViewModelProtocol? + private var networkFee: Decimal? + private var xorBalance: Decimal? + private var xorBalanceMinusFee: Decimal { + (xorBalance ?? 0) - (networkFee ?? 0) + } + + // MARK: - Constructors + init( + interactor: LiquidityPoolSupplyInteractorInput, + router: LiquidityPoolSupplyRouterInput, + localizationManager: LocalizationManagerProtocol + ) { + self.interactor = interactor + self.router = router + self.localizationManager = localizationManager + } + + // MARK: - Private methods + + private func runLoadingState() { + view?.setButtonLoadingState(isLoading: true) + } + + private func checkLoadingState() { + view?.setButtonLoadingState(isLoading: false) + } + + private func provideFromAssetVewModel(updateAmountInput: Bool = true) { + var balance: Decimal? = swapFromBalance + if swapFromChainAsset == xorChainAsset, let xorBalance = xorBalance, let networkFee = networkFee { + balance = xorBalance - networkFee + } + let inputAmount = swapFromInputResult? + .absoluteValue(from: balance ?? .zero) + let balanceViewModelFactory = buildBalanceSwapToViewModelFactory( + wallet: wallet, + for: swapFromChainAsset + ) + + let swapFromPrice = prices?.first(where: { priceData in + swapFromChainAsset?.asset.priceId == priceData.priceId + }) + + let viewModel = balanceViewModelFactory?.createAssetBalanceViewModel( + inputAmount, + balance: swapFromBalance, + priceData: swapFromPrice + ).value(for: selectedLocale) + + let inputViewModel = balanceViewModelFactory? + .createBalanceInputViewModel(inputAmount) + .value(for: selectedLocale) + + view?.didReceiveSwapFrom(viewModel: viewModel) + if updateAmountInput { + view?.didReceiveSwapFrom(amountInputViewModel: inputViewModel) + } + + checkLoadingState() + } + + private func provideToAssetVewModel(updateAmountInput: Bool = true) { + let inputAmount = swapToInputResult? + .absoluteValue(from: swapToBalance ?? .zero) + let balanceViewModelFactory = buildBalanceSwapToViewModelFactory( + wallet: wallet, + for: swapToChainAsset + ) + + let swapToPrice = prices?.first(where: { priceData in + swapToChainAsset?.asset.priceId == priceData.priceId + }) + + let viewModel = balanceViewModelFactory?.createAssetBalanceViewModel( + inputAmount, + balance: swapToBalance, + priceData: swapToPrice + ).value(for: selectedLocale) + + let inputViewModel = balanceViewModelFactory? + .createBalanceInputViewModel(inputAmount) + .value(for: selectedLocale) + + view?.didReceiveSwapTo(viewModel: viewModel) + if updateAmountInput { + view?.didReceiveSwapTo(amountInputViewModel: inputViewModel) + } + + checkLoadingState() + } + + private func provideFeeViewModel() { + guard let swapFromFee = networkFee else { + return + } + let balanceViewModelFactory = createBalanceViewModelFactory(for: xorChainAsset) + let feeViewModel = balanceViewModelFactory.balanceFromPrice( + swapFromFee, + priceData: prices?.first(where: { price in + price.priceId == xorChainAsset.asset.priceId + }), + isApproximately: true, + usageCase: .detailsCrypto + ).value(for: selectedLocale) + DispatchQueue.main.async { + self.view?.didReceiveNetworkFee(fee: feeViewModel) + } + networkFeeViewModel = feeViewModel + + checkLoadingState() + } + + private func buildBalanceSwapToViewModelFactory( + wallet: MetaAccountModel, + for chainAsset: ChainAsset? + ) -> BalanceViewModelFactoryProtocol? { + guard let chainAsset = chainAsset else { + return nil + } + let assetInfo = chainAsset.asset + .displayInfo(with: chainAsset.chain.icon) + let balanceViewModelFactory = BalanceViewModelFactory( + targetAssetInfo: assetInfo, + selectedMetaAccount: wallet + ) + return balanceViewModelFactory + } + + private func createBalanceViewModelFactory(for chainAsset: ChainAsset) -> BalanceViewModelFactory { + let assetInfo = chainAsset.asset.displayInfo(with: chainAsset.chain.icon) + let balanceViewModelFactory = BalanceViewModelFactory( + targetAssetInfo: assetInfo, + selectedMetaAccount: wallet + ) + return balanceViewModelFactory + } + + private func runCanXorPayValidation(sendAmount: Decimal) { + DataValidationRunner(validators: [ + dataValidatingFactory.canPayFeeAndAmount( + balanceType: .utility(balance: xorBalance), + feeAndTip: networkFee ?? .zero, + sendAmount: sendAmount, + locale: selectedLocale + ) + ]).runValidation {} + } +} + +// MARK: - LiquidityPoolSupplyViewOutput +extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { + func didTapBackButton() { + router.dismiss(view: view) + } + + func didTapApyInfo() { + var infoText: String + var infoTitle: String + infoTitle = R.string.localizable + .polkaswapMinReceived(preferredLanguages: selectedLocale.rLanguages) + infoText = R.string.localizable + .polkaswapMinimumReceivedInfo(preferredLanguages: selectedLocale.rLanguages) + router.presentInfo( + message: infoText, + title: infoTitle, + from: view + ) + } + + func didTapPreviewButton() { + + } + + func selectFromAmountPercentage(_ percentage: Float) { + runLoadingState() + + swapVariant = .desiredInput + swapFromInputResult = .rate(Decimal(Double(percentage))) + provideFromAssetVewModel() + + if swapFromChainAsset == xorChainAsset { + let inputAmount = swapFromInputResult? + .absoluteValue(from: xorBalanceMinusFee) + runCanXorPayValidation(sendAmount: inputAmount ?? .zero) + } + } + + func updateFromAmount(_ newValue: Decimal) { + runLoadingState() + + swapVariant = .desiredInput + swapFromInputResult = .absolute(newValue) + provideFromAssetVewModel(updateAmountInput: false) + } + + func selectToAmountPercentage(_ percentage: Float) { + runLoadingState() + + swapVariant = .desiredOutput + swapToInputResult = .rate(Decimal(Double(percentage))) + provideToAssetVewModel() + } + + func updateToAmount(_ newValue: Decimal) { + runLoadingState() + + swapVariant = .desiredOutput + swapToInputResult = .absolute(newValue) + provideToAssetVewModel(updateAmountInput: false) + } + + func didTapSelectFromAsset() { + let showChainAssets = xorChainAsset.chain.chainAssets + .filter { $0.chainAssetId != swapToChainAsset?.chainAssetId } + router.showSelectAsset( + from: view, + wallet: wallet, + chainAssets: showChainAssets, + selectedAssetId: swapFromChainAsset?.asset.id, + contextTag: InputTag.swapFrom.rawValue, + output: self + ) + } + + func didTapSelectToAsset() { + let showChainAssets = xorChainAsset.chain.chainAssets + .filter { $0.chainAssetId != swapFromChainAsset?.chainAssetId } + router.showSelectAsset( + from: view, + wallet: wallet, + chainAssets: showChainAssets, + selectedAssetId: swapToChainAsset?.asset.id, + contextTag: InputTag.swapTo.rawValue, + output: self + ) + } + + func didLoad(view: LiquidityPoolSupplyViewInput) { + self.view = view + interactor.setup(with: self) + } +} + +// MARK: - LiquidityPoolSupplyInteractorOutput +extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyInteractorOutput {} + +// MARK: - Localizable +extension LiquidityPoolSupplyPresenter: Localizable { + func applyLocalization() {} +} + +extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyModuleInput {} + +// MARK: - SelectAssetModuleOutput + +extension LiquidityPoolSupplyPresenter: SelectAssetModuleOutput { + func assetSelection( + didCompleteWith chainAsset: ChainAsset?, + contextTag: Int? + ) { + view?.didUpdating() + guard let rawValue = contextTag, + let input = InputTag(rawValue: rawValue), + let chainAsset = chainAsset + else { + return + } + + switch input { + case .swapFrom: + swapFromChainAsset = chainAsset + provideFromAssetVewModel() + case .swapTo: + swapToChainAsset = chainAsset + provideToAssetVewModel() + } + + runLoadingState() + + //TODO : Refresh fee + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyProtocols.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyProtocols.swift new file mode 100644 index 0000000000..aa371ff413 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyProtocols.swift @@ -0,0 +1,19 @@ +typealias LiquidityPoolSupplyModuleCreationResult = ( + view: LiquidityPoolSupplyViewInput, + input: LiquidityPoolSupplyModuleInput +) + +protocol LiquidityPoolSupplyRouterInput: AnyObject, AnyDismissable, SheetAlertPresentable { + func showSelectAsset( + from view: ControllerBackedProtocol?, + wallet: MetaAccountModel, + chainAssets: [ChainAsset]?, + selectedAssetId: AssetModel.Id?, + contextTag: Int?, + output: SelectAssetModuleOutput + ) +} + +protocol LiquidityPoolSupplyModuleInput: AnyObject {} + +protocol LiquidityPoolSupplyModuleOutput: AnyObject {} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyRouter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyRouter.swift new file mode 100644 index 0000000000..d75e7adbc2 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyRouter.swift @@ -0,0 +1,26 @@ +import Foundation + +final class LiquidityPoolSupplyRouter: LiquidityPoolSupplyRouterInput { + func showSelectAsset( + from view: ControllerBackedProtocol?, + wallet: MetaAccountModel, + chainAssets: [ChainAsset]?, + selectedAssetId: AssetModel.Id?, + contextTag: Int?, + output: SelectAssetModuleOutput + ) { + guard let module = SelectAssetAssembly.configureModule( + wallet: wallet, + selectedAssetId: selectedAssetId, + chainAssets: chainAssets, + searchTextsViewModel: .searchAssetPlaceholder, + output: output, + contextTag: contextTag, + isFullSize: true + ) else { + return + } + + view?.controller.present(module.view.controller, animated: true) + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift new file mode 100644 index 0000000000..ef3437b58b --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift @@ -0,0 +1,232 @@ +import UIKit +import SoraFoundation +import SnapKit + +protocol LiquidityPoolSupplyViewOutput: AnyObject { + func didLoad(view: LiquidityPoolSupplyViewInput) + func didTapBackButton() + func didTapApyInfo() + func didTapPreviewButton() + func selectFromAmountPercentage(_ percentage: Float) + func updateFromAmount(_ newValue: Decimal) + func selectToAmountPercentage(_ percentage: Float) + func updateToAmount(_ newValue: Decimal) + func didTapSelectFromAsset() + func didTapSelectToAsset() +} + +final class LiquidityPoolSupplyViewController: UIViewController, ViewHolder, HiddableBarWhenPushed { + typealias RootViewType = LiquidityPoolSupplyViewLayout + var keyboardHandler: FearlessKeyboardHandler? + + private enum Constants { + static let delay: CGFloat = 0.7 + } + + // MARK: Private properties + private let output: LiquidityPoolSupplyViewOutput + + private var amountFromInputViewModel: IAmountInputViewModel? + private var amountToInputViewModel: IAmountInputViewModel? + + // MARK: - Constructor + init( + output: LiquidityPoolSupplyViewOutput, + localizationManager: LocalizationManagerProtocol? + ) { + self.output = output + super.init(nibName: nil, bundle: nil) + self.localizationManager = localizationManager + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Life cycle + override func loadView() { + view = LiquidityPoolSupplyViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + output.didLoad(view: self) + setupActions() + configure() + addEndEditingTapGesture(for: rootView.contentView) + } + + // MARK: - Private methods + + private func configure() { + navigationController?.setNavigationBarHidden(true, animated: true) + + rootView.swapToInputView.textField.delegate = self + rootView.swapFromInputView.textField.delegate = self + + let locale = localizationManager?.selectedLocale ?? Locale.current + let accessoryView = UIFactory.default.createAmountAccessoryView(for: self, locale: locale) + rootView.swapFromInputView.textField.inputAccessoryView = accessoryView + updatePreviewButton() + } + + private func updatePreviewButton() { + let isEnabled = amountFromInputViewModel?.isValid == true && amountToInputViewModel?.isValid == true + rootView.previewButton.set(enabled: isEnabled) + } + + private func setupActions() { + rootView.backButton.addTarget( + self, + action: #selector(handleTapBackButton), + for: .touchUpInside + ) + rootView.swapFromInputView.selectHandler = { [weak self] in + self?.output.didTapSelectFromAsset() + } + rootView.swapToInputView.selectHandler = { [weak self] in + self?.output.didTapSelectToAsset() + } + + let tapMinReceiveInfo = UITapGestureRecognizer( + target: self, + action: #selector(handleTapApyInfo) + ) + rootView.minMaxReceivedView.titleLabel + .addGestureRecognizer(tapMinReceiveInfo) + + rootView.previewButton.addTarget( + self, + action: #selector(handleTapPreviewButton), + for: .touchUpInside + ) + } + + // MARK: - Private actions + + @objc private func handleTapBackButton() { + output.didTapBackButton() + } + + @objc private func handleTapMarketButton() { + output.didTapMarketButton() + } + + @objc private func handleTapApyInfo() { + output.didTapApyInfo() + } + + @objc private func handleTapPreviewButton() { + output.didTapPreviewButton() + } +} + +// MARK: - LiquidityPoolSupplyViewInput +extension LiquidityPoolSupplyViewController: LiquidityPoolSupplyViewInput {} + +// MARK: - Localizable +extension LiquidityPoolSupplyViewController: Localizable { + func applyLocalization() { + rootView.locale = selectedLocale + } +} +// MARK: - AmountInputAccessoryViewDelegate + +extension LiquidityPoolSupplyViewController: AmountInputAccessoryViewDelegate { + func didSelect(on _: AmountInputAccessoryView, percentage: Float) { + if rootView.swapFromInputView.textField.isFirstResponder { + output.selectFromAmountPercentage(percentage) + } else if rootView.swapToInputView.textField.isFirstResponder { + output.selectToAmountPercentage(percentage) + } + } + + func didSelectDone(on _: AmountInputAccessoryView) { + if rootView.swapFromInputView.textField.isFirstResponder { + rootView.swapFromInputView.textField.resignFirstResponder() + } else if rootView.swapToInputView.textField.isFirstResponder { + rootView.swapToInputView.textField.resignFirstResponder() + } + } +} + +// MARK: - UITextFieldDelegate + +extension LiquidityPoolSupplyViewController: UITextFieldDelegate { + func textField( + _ textField: UITextField, + shouldChangeCharactersIn range: NSRange, + replacementString string: String + ) -> Bool { + if textField == rootView.swapFromInputView.textField { + return amountFromInputViewModel?.didReceiveReplacement(string, for: range) ?? false + } else if textField == rootView.swapToInputView.textField { + return amountToInputViewModel?.didReceiveReplacement(string, for: range) ?? false + } + return true + } + + func textFieldDidBeginEditing(_ textField: UITextField) { + let swapFromIsFirstResponder = textField == rootView.swapFromInputView.textField + rootView.swapFromInputView.set(highlighted: swapFromIsFirstResponder, animated: false) + + if textField == rootView.swapToInputView.textField, amountToInputViewModel != nil { + let swapToIsFirstResponder = textField == rootView.swapToInputView.textField + rootView.swapToInputView.set(highlighted: swapToIsFirstResponder, animated: false) + } else if textField == rootView.swapToInputView.textField { + rootView.swapToInputView.textField.resignFirstResponder() + output.didTapSelectToAsset() + } + } + + func textFieldDidEndEditing(_: UITextField) { + rootView.swapFromInputView.set(highlighted: false, animated: false) + rootView.swapToInputView.set(highlighted: false, animated: false) + } +} + +// MARK: - AmountInputViewModelObserver + +extension LiquidityPoolSupplyViewController: AmountInputViewModelObserver { + func amountInputDidChange() { + rootView.swapFromInputView.inputFieldText = amountFromInputViewModel?.displayAmount + rootView.swapToInputView.inputFieldText = amountToInputViewModel?.displayAmount + + NSObject.cancelPreviousPerformRequests( + withTarget: self, + selector: #selector(updateAmounts), + object: nil + ) + perform(#selector(updateAmounts), with: nil, afterDelay: Constants.delay) + } + + @objc private func updateAmounts() { + if rootView.swapFromInputView.textField.isFirstResponder { + guard let amountFrom = amountFromInputViewModel?.decimalAmount else { + output.updateFromAmount(0) + + return + } + output.updateFromAmount(amountFrom) + } + + if rootView.swapToInputView.textField.isFirstResponder { + guard let amountTo = amountToInputViewModel?.decimalAmount else { + output.updateToAmount(0) + + return + } + output.updateToAmount(amountTo) + } + } +} + +// MARK: - KeyboardViewAdoptable + +extension LiquidityPoolSupplyViewController: KeyboardViewAdoptable { + var target: Constraint? { rootView.keyboardAdoptableConstraint } + + func offsetFromKeyboardWithInset(_: CGFloat) -> CGFloat { UIConstants.bigOffset } + func updateWhileKeyboardFrameChanging(_: CGRect) {} +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift new file mode 100644 index 0000000000..b359e95b18 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift @@ -0,0 +1,296 @@ +import UIKit +import SnapKit + +final class LiquidityPoolSupplyViewLayout: UIView { + private enum Constants { + static let navigationBarHeight: CGFloat = 56.0 + static let backButtonSize = CGSize(width: 32, height: 32) + static let imageWidth: CGFloat = 12 + static let imageHeight: CGFloat = 12 + static let imageVerticalPosition: CGFloat = 2 + static let disclaimerMinHeight: CGFloat = 42.0 + } + + var keyboardAdoptableConstraint: Constraint? + + // MARK: navigation + + let navigationViewContainer = UIView() + + let backButton: UIButton = { + let button = UIButton() + button.setImage(R.image.iconBack(), for: .normal) + button.layer.masksToBounds = true + button.backgroundColor = R.color.colorWhite8() + return button + }() + + let polkaswapImageView: UIImageView = { + let imageView = UIImageView() + imageView.image = R.image.polkaswap() + return imageView + }() + + // MARK: content + + let contentView: ScrollableContainerView = { + let view = ScrollableContainerView() + view.stackView.isLayoutMarginsRelativeArrangement = true + view.stackView.layoutMargins = UIEdgeInsets(top: UIConstants.bigOffset, left: 0.0, bottom: 0.0, right: 0.0) + view.stackView.spacing = UIConstants.bigOffset + return view + }() + + let swapFromInputView = SelectableAmountInputView(type: .swapSend) + let swapToInputView = SelectableAmountInputView(type: .swapReceive) + let switchSwapButton: UIButton = { + let button = UIButton() + button.setImage(R.image.iconSwitch(), for: .normal) + return button + }() + + let minMaxReceivedView = UIFactory.default.createMultiView() + let swapRouteView: TitleMultiValueView = { + let view = UIFactory.default.createMultiView() + return view + }() + + let fromPerToPriceView = UIFactory.default.createMultiView() + let toPerFromPriceView = UIFactory.default.createMultiView() + let networkFeeView = UIFactory.default.createMultiView() + + private lazy var multiViews = [ + minMaxReceivedView, + swapRouteView, + fromPerToPriceView, + toPerFromPriceView, + networkFeeView + ] + + let previewButton: TriangularedButton = { + let button = TriangularedButton() + button.applyEnabledStyle() + return button + }() + + var locale: Locale = .current { + didSet { + applyLocalization() + } + } + + // MARK: - Lifecycle + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = R.color.colorBlack19() + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + backButton.rounded() + } + + // MARK: - Public methods + + func bind(fee: BalanceViewModelProtocol?) { + networkFeeView.bindBalance(viewModel: fee) + networkFeeView.isHidden = false + } + + func bindSwapFrom(assetViewModel: AssetBalanceViewModelProtocol?) { + swapFromInputView.bind(viewModel: assetViewModel) + } + + func bindSwapTo(assetViewModel: AssetBalanceViewModelProtocol?) { + swapToInputView.bind(viewModel: assetViewModel) + } + + func bindDetails(viewModel: PolkaswapAdjustmentDetailsViewModel?) { + guard let viewModel = viewModel else { + multiViews.forEach { $0.isHidden = true } + return + } + minMaxReceivedView.bindBalance(viewModel: viewModel.minMaxReceiveVieModel) + swapRouteView.valueTop.text = viewModel.route + fromPerToPriceView.titleLabel.text = viewModel.fromPerToTitle + fromPerToPriceView.valueTop.text = viewModel.fromPerToValue + toPerFromPriceView.titleLabel.text = viewModel.toPerFromTitle + toPerFromPriceView.valueTop.text = viewModel.toPerFromValue + multiViews.forEach { $0.isHidden = false } + } + + func bind(swapVariant: SwapVariant) { + var text: String + switch swapVariant { + case .desiredInput: + text = R.string.localizable + .polkaswapMinReceived(preferredLanguages: locale.rLanguages) + case .desiredOutput: + text = R.string.localizable + .polkaswapMaxReceived(preferredLanguages: locale.rLanguages) + } + setInfoImage(for: minMaxReceivedView.titleLabel, text: text) + } + + // MARK: - Private methods + + private func setupLayout() { + addSubview(navigationViewContainer) + setupNavigationLayout(for: navigationViewContainer) + setupContentsLayout() + } + + private func setupNavigationLayout(for container: UIView) { + container.snp.makeConstraints { make in + make.top.leading.trailing.equalToSuperview() + make.height.equalTo(Constants.navigationBarHeight) + } + + container.addSubview(backButton) + container.addSubview(polkaswapImageView) + + backButton.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.leading.equalTo(UIConstants.bigOffset) + make.size.equalTo(Constants.backButtonSize) + } + + polkaswapImageView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.leading.equalTo(backButton.snp.trailing).offset(UIConstants.defaultOffset) + } + } + + private func setupContentsLayout() { + addSubview(contentView) + addSubview(previewButton) + + contentView.snp.makeConstraints { make in + make.top.equalTo(navigationViewContainer.snp.bottom) + make.leading.trailing.equalToSuperview() + make.bottom.equalTo(previewButton.snp.top).offset(-UIConstants.bigOffset) + } + + previewButton.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(UIConstants.bigOffset) + make.height.equalTo(UIConstants.actionHeight) + keyboardAdoptableConstraint = make.bottom.equalToSuperview().inset(UIConstants.bigOffset).constraint + } + + let switchInputsView = createSwitchInputsView() + contentView.stackView.addArrangedSubview(switchInputsView) + switchInputsView.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview() + } + + let feesView = createFeesView() + contentView.stackView.addArrangedSubview(feesView) + feesView.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(UIConstants.bigOffset) + } + } + + private func createSwitchInputsView() -> UIView { + let container = UIView() + container.addSubview(swapFromInputView) + container.addSubview(swapToInputView) + container.addSubview(switchSwapButton) + + swapFromInputView.snp.makeConstraints { make in + make.top.equalToSuperview() + make.leading.trailing.equalToSuperview().inset(UIConstants.bigOffset) + } + + swapToInputView.snp.makeConstraints { make in + make.top.equalTo(swapFromInputView.snp.bottom).offset(UIConstants.defaultOffset) + make.leading.trailing.equalToSuperview().inset(UIConstants.bigOffset) + make.bottom.equalToSuperview() + } + + switchSwapButton.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.centerY.equalTo(swapFromInputView.snp.bottom).offset(UIConstants.defaultOffset / 2) + } + + return container + } + + private func createFeesView() -> UIView { + func makeCommonConstraints(for view: UIView) { + view.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview() + } + } + + let container = UIFactory.default.createVerticalStackView() + + multiViews.forEach { + container.addArrangedSubview($0) + makeCommonConstraints(for: $0) + $0.isHidden = true + $0.titleLabel.isUserInteractionEnabled = true + } + + return container + } + + private func createMultiView() -> TitleMultiValueView { + let view = TitleMultiValueView() + view.borderView.borderType = .none + view.titleLabel.font = .p2Paragraph + view.titleLabel.textColor = R.color.colorStrokeGray() + view.valueTop.font = .h6Title + view.valueTop.textColor = R.color.colorWhite() + view.valueBottom.font = .p2Paragraph + view.valueBottom.textColor = R.color.colorStrokeGray() + return view + } + + private func applyLocalization() { + swapFromInputView.locale = locale + swapToInputView.locale = locale + + swapRouteView.titleLabel.text = R.string.localizable + .polkaswapConfirmationRouteStub(preferredLanguages: locale.rLanguages) + + let texts = [ + R.string.localizable + .polkaswapNetworkFee(preferredLanguages: locale.rLanguages) + ] + + [ + networkFeeView.titleLabel + ].enumerated().forEach { index, label in + setInfoImage(for: label, text: texts[index]) + } + + previewButton.imageWithTitleView?.title = R.string.localizable + .commonPreview(preferredLanguages: locale.rLanguages) + } + + private func setInfoImage(for label: UILabel, text: String) { + let attributedString = NSMutableAttributedString(string: text) + + let imageAttachment = NSTextAttachment() + imageAttachment.image = R.image.iconInfoFilled() + imageAttachment.bounds = CGRect( + x: 0, + y: -Constants.imageVerticalPosition, + width: Constants.imageWidth, + height: Constants.imageHeight + ) + + let imageString = NSAttributedString(attachment: imageAttachment) + attributedString.append(NSAttributedString(string: " ")) + attributedString.append(imageString) + + label.attributedText = attributedString + } +} diff --git a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewController.swift b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewController.swift index 4edbd66e07..6a09440d5a 100644 --- a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewController.swift +++ b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewController.swift @@ -1,6 +1,5 @@ import UIKit import SoraFoundation - import SnapKit final class PolkaswapAdjustmentViewController: UIViewController, ViewHolder, HiddableBarWhenPushed { diff --git a/fearlessTests/Modules/LiquidityPoolSupply/LiquidityPoolSupplyTests.swift b/fearlessTests/Modules/LiquidityPoolSupply/LiquidityPoolSupplyTests.swift new file mode 100644 index 0000000000..d655926282 --- /dev/null +++ b/fearlessTests/Modules/LiquidityPoolSupply/LiquidityPoolSupplyTests.swift @@ -0,0 +1,16 @@ +import XCTest + +class LiquidityPoolSupplyTests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + XCTFail("Did you forget to add tests?") + } +} From a89e988c5265cf879373d09958f6ae70fcea90d3 Mon Sep 17 00:00:00 2001 From: Alex Lebedko Date: Fri, 24 May 2024 15:39:53 +0700 Subject: [PATCH 009/115] supply input --- .../xcshareddata/swiftpm/Package.resolved | 57 ++-- fearless/Common/Model/AssetVisibility.swift | 16 +- .../ChainRegistry/ChainAccountModel.swift | 15 +- .../ChainRegistry/MetaAccountModel.swift | 31 +- fearless/Common/Model/Currency.swift | 54 ++-- .../EntityToModel/ChainModelMapper.swift | 3 +- .../EntityToModel/MetaAccountMapper.swift | 1 + .../contents | 1 + .../AmountInputView/AmountInputViewV2.swift | 4 + ...TitleSwitchTableViewCellModelFactory.swift | 9 +- .../ViewModel/TitleSwitchViewModel.swift | 1 + .../BackupWalletViewModelFactory.swift | 1 + .../CrossChainDepsContainer.swift | 2 +- .../LiquidityPoolDetailsPresenter.swift | 10 + .../LiquidityPoolDetailsProtocols.swift | 8 +- .../LiquidityPoolDetailsRouter.swift | 14 +- .../LiquidityPoolDetailsViewController.swift | 8 + .../LiquidityPoolSupplyAssembly.swift | 65 ++++- .../LiquidityPoolSupplyInteractor.swift | 106 ++++++- .../LiquidityPoolSupplyPresenter.swift | 264 +++++++++++++++--- .../LiquidityPoolSupplyProtocols.swift | 2 + .../LiquidityPoolSupplyRouter.swift | 1 + .../LiquidityPoolSupplyViewController.swift | 87 ++++-- .../LiquidityPoolSupplyViewLayout.swift | 12 +- .../Modules/Profile/ProfileInteractor.swift | 1 + .../Modules/Profile/ProfilePresenter.swift | 1 + .../Modules/Profile/ProfileProtocol.swift | 1 + .../SelectCurrencyInteractor.swift | 1 + .../SelectCurrencyPresenter.swift | 1 + .../SelectCurrencyProtocols.swift | 2 + .../SelectCurrencyViewModelFactory.swift | 1 + 31 files changed, 619 insertions(+), 161 deletions(-) diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2cb5285b8d..c5987471bd 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", "state" : { - "revision" : "7892a123f7e8d0fe62f9f03728b17bbd4f94df5c", - "version" : "1.8.1" + "revision" : "c9c3df6ab812de32bae61fc0cd1bf6d45170ebf0", + "version" : "1.8.2" } }, { @@ -140,8 +140,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-atomics.git", "state" : { - "revision" : "6c89474e62719ddcc1e9614989fff2f68208fe10", - "version" : "1.1.0" + "revision" : "cd142fd2f64be2100422d658e7411e39489da985", + "version" : "1.2.0" } }, { @@ -149,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", - "version" : "1.0.4" + "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", + "version" : "1.1.0" } }, { @@ -158,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-http-types", "state" : { - "revision" : "1827dc94bdab2eb5f2fc804e9b0cb43574282566", - "version" : "1.0.2" + "revision" : "9bee2fdb79cc740081abd8ebd80738063d632286", + "version" : "1.1.0" } }, { @@ -167,8 +167,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "3db5c4aeee8100d2db6f1eaf3864afdad5dc68fd", - "version" : "2.59.0" + "revision" : "359c461e5561d22c6334828806cc25d759ca7aa6", + "version" : "2.65.0" } }, { @@ -176,8 +176,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-extras.git", "state" : { - "revision" : "798c962495593a23fdea0c0c63fd55571d8dff51", - "version" : "1.20.0" + "revision" : "a3b640d7dc567225db7c94386a6e71aded1bfa63", + "version" : "1.22.0" } }, { @@ -185,8 +185,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-http2.git", "state" : { - "revision" : "9c22e4f810ce780453f563fba98e1a1039f83d56", - "version" : "1.28.1" + "revision" : "c6afe04165c865faaa687b42c32ed76dfcc91076", + "version" : "1.31.0" } }, { @@ -194,8 +194,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-ssl.git", "state" : { - "revision" : "320bd978cceb8e88c125dcbb774943a92f6286e9", - "version" : "2.25.0" + "revision" : "7c381eb6083542b124a6c18fae742f55001dc2b5", + "version" : "2.26.0" } }, { @@ -203,8 +203,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-transport-services.git", "state" : { - "revision" : "e7403c35ca6bb539a7ca353b91cc2d8ec0362d58", - "version" : "1.19.0" + "revision" : "38ac8221dd20674682148d6451367f89c2652980", + "version" : "1.21.0" } }, { @@ -216,13 +216,22 @@ "version" : "1.0.3" } }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "f9266c85189c2751589a50ea5aec72799797e471", + "version" : "1.3.0" + } + }, { "identity" : "swiftformat", "kind" : "remoteSourceControl", "location" : "https://github.com/nicklockwood/SwiftFormat", "state" : { - "revision" : "05cb325003a673b3d177c711b3bc909cfee07622", - "version" : "0.53.9" + "revision" : "4bf475154c1c98dcdf751037f930f8e5c72597a4", + "version" : "0.53.10" } }, { @@ -239,8 +248,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SwiftyBeaver/SwiftyBeaver.git", "state" : { - "revision" : "1ae1f8fe06ce9e3bd84d1e821dcca5054dc6fb3c", - "version" : "2.1.0" + "revision" : "8cba041db09596183331d123f337d0eb2e6e8e91", + "version" : "2.1.1" } }, { @@ -284,8 +293,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/websocket-kit", "state" : { - "revision" : "53fe0639a98903858d0196b699720decb42aee7b", - "version" : "2.14.0" + "revision" : "4232d34efa49f633ba61afde365d3896fc7f8740", + "version" : "2.15.0" } }, { diff --git a/fearless/Common/Model/AssetVisibility.swift b/fearless/Common/Model/AssetVisibility.swift index 543917411f..28a15710b6 100644 --- a/fearless/Common/Model/AssetVisibility.swift +++ b/fearless/Common/Model/AssetVisibility.swift @@ -1,11 +1,11 @@ import Foundation import RobinHood -struct AssetVisibility: Codable, Equatable, Hashable, Identifiable { - var identifier: String { - assetId - } - - let assetId: String - var hidden: Bool -} +// struct AssetVisibility: Codable, Equatable, Hashable, Identifiable { +// var identifier: String { +// assetId +// } +// +// let assetId: String +// var hidden: Bool +// } diff --git a/fearless/Common/Model/ChainRegistry/ChainAccountModel.swift b/fearless/Common/Model/ChainRegistry/ChainAccountModel.swift index 6275784364..d22ce7405a 100644 --- a/fearless/Common/Model/ChainRegistry/ChainAccountModel.swift +++ b/fearless/Common/Model/ChainRegistry/ChainAccountModel.swift @@ -1,12 +1,13 @@ import Foundation +import SSFModels -struct ChainAccountModel: Equatable, Hashable, Codable { - let chainId: String - let accountId: AccountId - let publicKey: Data - let cryptoType: UInt8 - let ethereumBased: Bool -} +// struct ChainAccountModel: Equatable, Hashable, Codable { +// let chainId: String +// let accountId: AccountId +// let publicKey: Data +// let cryptoType: UInt8 +// let ethereumBased: Bool +// } extension ChainAccountModel { func toAddress(addressPrefix: UInt16) -> AccountAddress? { diff --git a/fearless/Common/Model/ChainRegistry/MetaAccountModel.swift b/fearless/Common/Model/ChainRegistry/MetaAccountModel.swift index e8cafddd1c..2ebc7eb780 100644 --- a/fearless/Common/Model/ChainRegistry/MetaAccountModel.swift +++ b/fearless/Common/Model/ChainRegistry/MetaAccountModel.swift @@ -12,17 +12,40 @@ struct MetaAccountModel: Equatable, Codable { let substratePublicKey: Data let ethereumAddress: Data? let ethereumPublicKey: Data? - let chainAccounts: Set + let chainAccounts: Set let assetKeysOrder: [String]? - let assetFilterOptions: [FilterOption] + let assetFilterOptions: [SSFModels.FilterOption] let canExportEthereumMnemonic: Bool let unusedChainIds: [String]? - let selectedCurrency: Currency + let selectedCurrency: SSFModels.Currency let networkManagmentFilter: String? - let assetsVisibility: [AssetVisibility] + let assetsVisibility: [SSFModels.AssetVisibility] let zeroBalanceAssetsHidden: Bool let hasBackup: Bool let favouriteChainIds: [ChainModel.Id] + + var utilsModel: SSFModels.MetaAccountModel { + SSFModels.MetaAccountModel( + metaId: metaId, + name: name, + substrateAccountId: substrateAccountId, + substrateCryptoType: substrateCryptoType, + substratePublicKey: substratePublicKey, + ethereumAddress: ethereumAddress, + ethereumPublicKey: ethereumPublicKey, + chainAccounts: chainAccounts, + assetKeysOrder: assetKeysOrder, + assetFilterOptions: assetFilterOptions, + canExportEthereumMnemonic: canExportEthereumMnemonic, + unusedChainIds: unusedChainIds, + selectedCurrency: selectedCurrency, + networkManagmentFilter: networkManagmentFilter, + assetsVisibility: assetsVisibility, + zeroBalanceAssetsHidden: zeroBalanceAssetsHidden, + hasBackup: hasBackup, + favouriteChainIds: favouriteChainIds + ) + } } extension MetaAccountModel { diff --git a/fearless/Common/Model/Currency.swift b/fearless/Common/Model/Currency.swift index e0786fb766..82583e939b 100644 --- a/fearless/Common/Model/Currency.swift +++ b/fearless/Common/Model/Currency.swift @@ -1,29 +1,29 @@ import Foundation -struct Currency: Codable, Equatable, Hashable { - let id: String - let symbol: String - let name: String - let icon: String - var isSelected: Bool? - - static func defaultCurrency() -> Currency { - Currency( - id: "usd", - symbol: "$", - name: "US Dollar", - icon: "https://raw.githubusercontent.com/soramitsu/fearless-utils/android/2.0.2/icons/fiat/usd.svg", - isSelected: true - ) - } - - static func euro() -> Currency { - Currency( - id: "eur", - symbol: "€", - name: "Euro", - icon: "https://raw.githubusercontent.com/soramitsu/fearless-utils/android/2.0.2/icons/fiat/eur.svg", - isSelected: false - ) - } -} +// struct Currency: Codable, Equatable, Hashable { +// let id: String +// let symbol: String +// let name: String +// let icon: String +// var isSelected: Bool? +// +// static func defaultCurrency() -> Currency { +// Currency( +// id: "usd", +// symbol: "$", +// name: "US Dollar", +// icon: "https://raw.githubusercontent.com/soramitsu/fearless-utils/android/2.0.2/icons/fiat/usd.svg", +// isSelected: true +// ) +// } +// +// static func euro() -> Currency { +// Currency( +// id: "eur", +// symbol: "€", +// name: "Euro", +// icon: "https://raw.githubusercontent.com/soramitsu/fearless-utils/android/2.0.2/icons/fiat/eur.svg", +// isSelected: false +// ) +// } +// } diff --git a/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift b/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift index 10fc8f9b83..7d17191721 100644 --- a/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift +++ b/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift @@ -486,7 +486,7 @@ extension ChainModelMapper: CoreDataMapperProtocol { disabled: entity.disabled, chainId: entity.chainId!, parentId: entity.parentId, - paraId: nil, + paraId: entity.paraId, name: entity.name!, xcm: xcm, nodes: Set(nodes), addressPrefix: UInt16(bitPattern: entity.addressPrefix), @@ -523,6 +523,7 @@ extension ChainModelMapper: CoreDataMapperProtocol { } entity.disabled = model.disabled entity.chainId = model.chainId + entity.paraId = model.paraId entity.parentId = model.parentId entity.name = model.name entity.types = model.types?.url diff --git a/fearless/Common/Storage/EntityToModel/MetaAccountMapper.swift b/fearless/Common/Storage/EntityToModel/MetaAccountMapper.swift index cc106de2f6..065441cc37 100644 --- a/fearless/Common/Storage/EntityToModel/MetaAccountMapper.swift +++ b/fearless/Common/Storage/EntityToModel/MetaAccountMapper.swift @@ -2,6 +2,7 @@ import Foundation import RobinHood import CoreData import SSFAccountManagmentStorage +import SSFModels final class MetaAccountMapper { var entityIdentifierFieldName: String { #keyPath(CDMetaAccount.metaId) } diff --git a/fearless/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v5.xcdatamodel/contents b/fearless/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v5.xcdatamodel/contents index 161db312ed..792771c0fb 100644 --- a/fearless/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v5.xcdatamodel/contents +++ b/fearless/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v5.xcdatamodel/contents @@ -37,6 +37,7 @@ + diff --git a/fearless/Common/View/AmountInputView/AmountInputViewV2.swift b/fearless/Common/View/AmountInputView/AmountInputViewV2.swift index 050566986e..570a5ea58b 100644 --- a/fearless/Common/View/AmountInputView/AmountInputViewV2.swift +++ b/fearless/Common/View/AmountInputView/AmountInputViewV2.swift @@ -112,6 +112,10 @@ final class AmountInputViewV2: UIView { // MARK: - Public methods + func set(highlighted: Bool, animated: Bool) { + triangularedBackgroundView.set(highlighted: highlighted, animated: animated) + } + func bind(viewModel: AssetBalanceViewModelProtocol) { iconView.isHidden = (viewModel.iconViewModel == nil) viewModel.iconViewModel?.cancel(on: iconView) diff --git a/fearless/Common/ViewController/ModalPicker/ViewModel/TitleSwitchTableViewCellModelFactory.swift b/fearless/Common/ViewController/ModalPicker/ViewModel/TitleSwitchTableViewCellModelFactory.swift index 9d4bc3b2e8..1756607cff 100644 --- a/fearless/Common/ViewController/ModalPicker/ViewModel/TitleSwitchTableViewCellModelFactory.swift +++ b/fearless/Common/ViewController/ModalPicker/ViewModel/TitleSwitchTableViewCellModelFactory.swift @@ -1,4 +1,5 @@ import Foundation +import SSFModels protocol TitleSwitchTableViewCellModelFactoryProtocol { func createFilters( @@ -14,10 +15,10 @@ protocol TitleSwitchTableViewCellModelFactoryProtocol { ) -> [SortPickerTableViewCellModel] } -enum FilterOption: String, Codable { - case hideZeroBalance - case hiddenSectionOpen -} +// enum FilterOption: String, Codable { +// case hideZeroBalance +// case hiddenSectionOpen +// } enum PoolSortOption: Equatable { case totalStake(assetSymbol: String) diff --git a/fearless/Common/ViewController/ModalPicker/ViewModel/TitleSwitchViewModel.swift b/fearless/Common/ViewController/ModalPicker/ViewModel/TitleSwitchViewModel.swift index d69a0801ef..0f12a2b30e 100644 --- a/fearless/Common/ViewController/ModalPicker/ViewModel/TitleSwitchViewModel.swift +++ b/fearless/Common/ViewController/ModalPicker/ViewModel/TitleSwitchViewModel.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import SSFModels protocol TitleSwitchTableViewCellModelDelegate: AnyObject { func switchOptionChangeState(option: FilterOption, isOn: Bool) diff --git a/fearless/Modules/BackupWallet/BackupWalletViewModelFactory.swift b/fearless/Modules/BackupWallet/BackupWalletViewModelFactory.swift index e14673088f..5f2817dfe4 100644 --- a/fearless/Modules/BackupWallet/BackupWalletViewModelFactory.swift +++ b/fearless/Modules/BackupWallet/BackupWalletViewModelFactory.swift @@ -1,6 +1,7 @@ import Foundation import SoraFoundation import SSFCloudStorage +import SSFModels protocol BackupWalletViewModelFactoryProtocol { func createViewModel( diff --git a/fearless/Modules/CrossChainConfirmation/CrossChainDepsContainer.swift b/fearless/Modules/CrossChainConfirmation/CrossChainDepsContainer.swift index 37dfa28fac..6b7d87c1a2 100644 --- a/fearless/Modules/CrossChainConfirmation/CrossChainDepsContainer.swift +++ b/fearless/Modules/CrossChainConfirmation/CrossChainDepsContainer.swift @@ -91,7 +91,7 @@ final class CrossChainDepsContainer { accountResponse: response ) - let signingWrapperData = XcmAssembly.SigningWrapperData( + let signingWrapperData = SigningWrapperData( publicKeyData: response.publicKey, secretKeyData: secretKeyData ) diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift index d211d337a3..27d7e1e4ff 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift @@ -155,6 +155,16 @@ extension LiquidityPoolDetailsPresenter: LiquidityPoolDetailsViewOutput { func backButtonClicked() { router.dismiss(view: view) } + + func supplyButtonClicked() { + guard let liquidityPair else { + return + } + + router.showSupplyFlow(liquidityPair: liquidityPair, chain: chain, wallet: wallet, from: view) + } + + func removeButtonClicked() {} } // MARK: - LiquidityPoolDetailsInteractorOutput diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsProtocols.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsProtocols.swift index ee9ecc0e99..ed37be2125 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsProtocols.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsProtocols.swift @@ -1,6 +1,12 @@ +import SSFPools +import SSFModels + typealias LiquidityPoolDetailsModuleCreationResult = (view: LiquidityPoolDetailsViewInput, input: LiquidityPoolDetailsModuleInput) -protocol LiquidityPoolDetailsRouterInput: AnyObject, AnyDismissable {} +protocol LiquidityPoolDetailsRouterInput: AnyObject, AnyDismissable { + func showSupplyFlow(liquidityPair: LiquidityPair, chain: ChainModel, wallet: MetaAccountModel, from view: ControllerBackedProtocol?) + func showRemoveFlow(liquidityPair: LiquidityPair, chain: ChainModel, wallet: MetaAccountModel, from view: ControllerBackedProtocol?) +} protocol LiquidityPoolDetailsModuleInput: AnyObject {} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsRouter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsRouter.swift index 451317e9ff..ac1cd88191 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsRouter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsRouter.swift @@ -1,3 +1,15 @@ import Foundation +import SSFModels +import SSFPools -final class LiquidityPoolDetailsRouter: LiquidityPoolDetailsRouterInput {} +final class LiquidityPoolDetailsRouter: LiquidityPoolDetailsRouterInput { + func showSupplyFlow(liquidityPair: LiquidityPair, chain: ChainModel, wallet: MetaAccountModel, from view: ControllerBackedProtocol?) { + guard let controller = LiquidityPoolSupplyAssembly.configureModule(chain: chain, wallet: wallet, liquidityPair: liquidityPair)?.view.controller else { + return + } + + view?.controller.navigationController?.pushViewController(controller, animated: true) + } + + func showRemoveFlow(liquidityPair _: LiquidityPair, chain _: ChainModel, wallet _: MetaAccountModel, from _: ControllerBackedProtocol?) {} +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewController.swift index 84d76d79b0..fd921e3048 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewController.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewController.swift @@ -4,6 +4,8 @@ import SoraFoundation protocol LiquidityPoolDetailsViewOutput: AnyObject { func didLoad(view: LiquidityPoolDetailsViewInput) func backButtonClicked() + func supplyButtonClicked() + func removeButtonClicked() } final class LiquidityPoolDetailsViewController: UIViewController, ViewHolder, HiddableBarWhenPushed { @@ -42,6 +44,12 @@ final class LiquidityPoolDetailsViewController: UIViewController, ViewHolder, Hi rootView.navigationBar.backButton.addAction { [weak self] in self?.output.backButtonClicked() } + rootView.supplyButton.addAction { [weak self] in + self?.output.supplyButtonClicked() + } + rootView.removeButton.addAction { [weak self] in + self?.output.removeButtonClicked() + } } // MARK: - Private methods diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyAssembly.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyAssembly.swift index 53ce0a3c9e..9f8e7c3fcb 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyAssembly.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyAssembly.swift @@ -1,19 +1,57 @@ import UIKit import SoraFoundation +import SSFPools +import SSFPolkaswap +import SSFModels +import SoraKeystore final class LiquidityPoolSupplyAssembly { - static func configureModule() -> LiquidityPoolSupplyModuleCreationResult? { + static func configureModule(chain: ChainModel, wallet: MetaAccountModel, liquidityPair: LiquidityPair) -> LiquidityPoolSupplyModuleCreationResult? { + guard let response = wallet.fetch(for: chain.accountRequest()) else { + return nil + } + + guard let secretKeyData = try? fetchSecretKey( + for: chain, + metaId: wallet.metaId, + accountResponse: response + ) else { + return nil + } + let localizationManager = LocalizationManager.shared - - let interactor = LiquidityPoolSupplyInteractor() + let chainRegistry = ChainRegistryFacade.sharedRegistry + let lpDataService = PolkaswapLiquidityPoolServiceAssembly.buildService(for: chain, chainRegistry: chainRegistry) + let signingWrapperData = SigningWrapperData(publicKeyData: response.publicKey, secretKeyData: secretKeyData) + + guard let lpOperationService = try? PolkaswapLiquidityPoolServiceAssembly.buildOperationService( + for: chain, + wallet: wallet.utilsModel, + chainRegistry: chainRegistry, + signingWrapperData: signingWrapperData + ) else { + return nil + } + + let accountInfoSubscriptionAdapter = AccountInfoSubscriptionAdapter( + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, + selectedMetaAccount: wallet + ) + + let interactor = LiquidityPoolSupplyInteractor(lpOperationService: lpOperationService, lpDataService: lpDataService, liquidityPair: liquidityPair, priceLocalSubscriber: PriceLocalStorageSubscriberImpl.shared, chain: chain, accountInfoSubscriptionAdapter: accountInfoSubscriptionAdapter) let router = LiquidityPoolSupplyRouter() - + let presenter = LiquidityPoolSupplyPresenter( interactor: interactor, router: router, - localizationManager: localizationManager + liquidityPair: liquidityPair, + localizationManager: localizationManager, + chain: chain, + logger: Logger.shared, + wallet: wallet, + dataValidatingFactory: SendDataValidatingFactory(presentable: router) ) - + let view = LiquidityPoolSupplyViewController( output: presenter, localizationManager: localizationManager @@ -21,4 +59,19 @@ final class LiquidityPoolSupplyAssembly { return (view, presenter) } + + static func fetchSecretKey( + for chain: ChainModel, + metaId: String, + accountResponse: ChainAccountResponse + ) throws -> Data { + let accountId = accountResponse.isChainAccount ? accountResponse.accountId : nil + let tag: String = chain.isEthereumBased + ? KeystoreTagV2.ethereumSecretKeyTagForMetaId(metaId, accountId: accountId) + : KeystoreTagV2.substrateSecretKeyTagForMetaId(metaId, accountId: accountId) + + let keystore = Keychain() + let secretKey = try keystore.fetchKey(for: tag) + return secretKey + } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyInteractor.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyInteractor.swift index 9d433aa385..69ab71ae49 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyInteractor.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyInteractor.swift @@ -1,21 +1,117 @@ import UIKit import SSFPools +import SSFPolkaswap +import SSFModels +import BigInt -protocol LiquidityPoolSupplyInteractorOutput: AnyObject {} +protocol LiquidityPoolSupplyInteractorOutput: AnyObject { + func didReceiveDexId(_ dexId: String) + func didReceiveDexIdError(_ error: Error) + func didReceiveFee(_ fee: BigUInt) + func didReceiveFeeError(_ error: Error) + func didReceivePricesData(result: Result<[PriceData], Error>) + func didReceiveAccountInfo(result: Result, for chainAsset: ChainAsset) +} final class LiquidityPoolSupplyInteractor { // MARK: - Private properties + private weak var output: LiquidityPoolSupplyInteractorOutput? - private let lpService: PoolsOperationService - - init(lpService: PoolsOperationService) { - + private let lpOperationService: PoolsOperationService + private let lpDataService: PolkaswapLiquidityPoolService + private let liquidityPair: LiquidityPair + private let priceLocalSubscriber: PriceLocalStorageSubscriber + private let chain: ChainModel + private let accountInfoSubscriptionAdapter: AccountInfoSubscriptionAdapterProtocol + + private var pricesProvider: AnySingleValueProvider<[PriceData]>? + + init( + lpOperationService: PoolsOperationService, + lpDataService: PolkaswapLiquidityPoolService, + liquidityPair: LiquidityPair, + priceLocalSubscriber: PriceLocalStorageSubscriber, + chain: ChainModel, + accountInfoSubscriptionAdapter: AccountInfoSubscriptionAdapterProtocol + ) { + self.lpOperationService = lpOperationService + self.lpDataService = lpDataService + self.liquidityPair = liquidityPair + self.priceLocalSubscriber = priceLocalSubscriber + self.chain = chain + self.accountInfoSubscriptionAdapter = accountInfoSubscriptionAdapter + } + + private func fetchDexId() { + Task { + do { + let dexId = try await lpDataService.fetchDexId(baseAssetId: liquidityPair.baseAssetId) + output?.didReceiveDexId(dexId) + } catch { + output?.didReceiveDexIdError(error) + } + } + } + + private func subscribeToPrices() { + let chainAssets = chain.chainAssets + pricesProvider = priceLocalSubscriber.subscribeToPrices(for: chainAssets, listener: self) + + guard chainAssets.isNotEmpty else { + output?.didReceivePricesData(result: .success([])) + return + } + pricesProvider = priceLocalSubscriber.subscribeToPrices(for: chainAssets, listener: self) + } + + private func subscribeToAccountInfo() { + let chainAssets = chain.chainAssets + accountInfoSubscriptionAdapter.subscribe( + chainsAssets: chainAssets, + handler: self, + deliveryOn: .main + ) } } // MARK: - LiquidityPoolSupplyInteractorInput + extension LiquidityPoolSupplyInteractor: LiquidityPoolSupplyInteractorInput { func setup(with output: LiquidityPoolSupplyInteractorOutput) { self.output = output + fetchDexId() + subscribeToPrices() + subscribeToAccountInfo() + } + + func estimateFee(supplyLiquidityInfo: SupplyLiquidityInfo) { + Task { + do { + let fee = try await lpOperationService.estimateFee(liquidityOperation: .substrateSupplyLiquidity(supplyLiquidityInfo)) + output?.didReceiveFee(fee) + } catch { + output?.didReceiveFeeError(error) + } + } + } +} + +// MARK: - PriceLocalStorageSubscriber + +extension LiquidityPoolSupplyInteractor: PriceLocalSubscriptionHandler { + func handlePrices(result: Result<[PriceData], Error>) { + output?.didReceivePricesData(result: result) + } +} + +// MARK: - AccountInfoSubscriptionAdapterHandler + +extension LiquidityPoolSupplyInteractor: AccountInfoSubscriptionAdapterHandler { + func handleAccountInfo( + result: Result, + accountId _: AccountId, + chainAsset: ChainAsset + ) { + output?.didReceiveAccountInfo(result: result, for: chainAsset) } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift index fabc108bd8..0fdbb902b8 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift @@ -1,6 +1,8 @@ import Foundation import SoraFoundation import SSFModels +import SSFPools +import BigInt protocol LiquidityPoolSupplyViewInput: ControllerBackedProtocol { func didReceiveSwapFrom(viewModel: AssetBalanceViewModelProtocol?) @@ -14,6 +16,7 @@ protocol LiquidityPoolSupplyViewInput: ControllerBackedProtocol { protocol LiquidityPoolSupplyInteractorInput: AnyObject { func setup(with output: LiquidityPoolSupplyInteractorOutput) + func estimateFee(supplyLiquidityInfo: SupplyLiquidityInfo) } final class LiquidityPoolSupplyPresenter { @@ -21,20 +24,22 @@ final class LiquidityPoolSupplyPresenter { case swapFrom = 0 case swapTo } - + private enum Constants { static let slippadgeTolerance: Float = 0.5 } - + // MARK: Private properties + private weak var view: LiquidityPoolSupplyViewInput? private let router: LiquidityPoolSupplyRouterInput private let interactor: LiquidityPoolSupplyInteractorInput private let dataValidatingFactory: SendDataValidatingFactory private let logger: LoggerProtocol + private let liquidityPair: LiquidityPair + private let chain: ChainModel private let wallet: MetaAccountModel - private let xorChainAsset: ChainAsset private var swapFromChainAsset: ChainAsset? private var swapToChainAsset: ChainAsset? private var prices: [PriceData]? @@ -45,7 +50,7 @@ final class LiquidityPoolSupplyPresenter { private var swapFromBalance: Decimal? private var swapToInputResult: AmountInputResult? private var swapToBalance: Decimal? - + private var networkFeeViewModel: BalanceViewModelProtocol? private var networkFee: Decimal? private var xorBalance: Decimal? @@ -53,30 +58,76 @@ final class LiquidityPoolSupplyPresenter { (xorBalance ?? 0) - (networkFee ?? 0) } + private var baseTargetRate: Decimal? + + private var dexId: String? + // MARK: - Constructors + init( interactor: LiquidityPoolSupplyInteractorInput, router: LiquidityPoolSupplyRouterInput, - localizationManager: LocalizationManagerProtocol + liquidityPair: LiquidityPair, + localizationManager: LocalizationManagerProtocol, + chain: ChainModel, + logger: LoggerProtocol, + wallet: MetaAccountModel, + dataValidatingFactory: SendDataValidatingFactory ) { self.interactor = interactor self.router = router + self.liquidityPair = liquidityPair + self.chain = chain + self.logger = logger + self.wallet = wallet + self.dataValidatingFactory = dataValidatingFactory + self.localizationManager = localizationManager } - + // MARK: - Private methods - + + private func refreshFee() { + guard + let dexId, + let baseAsset = chain.assets.first(where: { $0.currencyId == liquidityPair.baseAssetId }), + let targetAsset = chain.assets.first(where: { $0.currencyId == liquidityPair.targetAssetId }) + else { + return + } + + let baseAssetInfo = PooledAssetInfo(id: liquidityPair.baseAssetId, precision: Int16(baseAsset.precision)) + let targetAssetInfo = PooledAssetInfo(id: liquidityPair.targetAssetId, precision: Int16(targetAsset.precision)) + + let baseAssetAmount = swapFromInputResult?.absoluteValue(from: swapFromBalance ?? .zero) ?? .zero + let targetAssetAmount = swapToInputResult?.absoluteValue(from: swapToBalance ?? .zero) ?? .zero + let supplyLiquidityInfo = SupplyLiquidityInfo( + dexId: dexId, + baseAsset: baseAssetInfo, + targetAsset: targetAssetInfo, + baseAssetAmount: baseAssetAmount, + targetAssetAmount: targetAssetAmount, + slippage: Decimal(floatLiteral: Double(slippadgeTolerance)) + ) + + interactor.estimateFee(supplyLiquidityInfo: supplyLiquidityInfo) + } + private func runLoadingState() { - view?.setButtonLoadingState(isLoading: true) + DispatchQueue.main.async { [weak self] in + self?.view?.setButtonLoadingState(isLoading: true) + } } private func checkLoadingState() { - view?.setButtonLoadingState(isLoading: false) + DispatchQueue.main.async { [weak self] in + self?.view?.setButtonLoadingState(isLoading: false) + } } - + private func provideFromAssetVewModel(updateAmountInput: Bool = true) { var balance: Decimal? = swapFromBalance - if swapFromChainAsset == xorChainAsset, let xorBalance = xorBalance, let networkFee = networkFee { + if swapFromChainAsset == chain.utilityChainAssets().first, let xorBalance = xorBalance, let networkFee = networkFee { balance = xorBalance - networkFee } let inputAmount = swapFromInputResult? @@ -100,9 +151,12 @@ final class LiquidityPoolSupplyPresenter { .createBalanceInputViewModel(inputAmount) .value(for: selectedLocale) - view?.didReceiveSwapFrom(viewModel: viewModel) - if updateAmountInput { - view?.didReceiveSwapFrom(amountInputViewModel: inputViewModel) + DispatchQueue.main.async { [weak self] in + self?.view?.didReceiveSwapFrom(viewModel: viewModel) + + if updateAmountInput { + self?.view?.didReceiveSwapFrom(amountInputViewModel: inputViewModel) + } } checkLoadingState() @@ -130,16 +184,19 @@ final class LiquidityPoolSupplyPresenter { .createBalanceInputViewModel(inputAmount) .value(for: selectedLocale) - view?.didReceiveSwapTo(viewModel: viewModel) - if updateAmountInput { - view?.didReceiveSwapTo(amountInputViewModel: inputViewModel) + DispatchQueue.main.async { [weak self] in + self?.view?.didReceiveSwapTo(viewModel: viewModel) + + if updateAmountInput { + self?.view?.didReceiveSwapTo(amountInputViewModel: inputViewModel) + } } checkLoadingState() } - + private func provideFeeViewModel() { - guard let swapFromFee = networkFee else { + guard let swapFromFee = networkFee, let xorChainAsset = chain.utilityChainAssets().first else { return } let balanceViewModelFactory = createBalanceViewModelFactory(for: xorChainAsset) @@ -151,14 +208,16 @@ final class LiquidityPoolSupplyPresenter { isApproximately: true, usageCase: .detailsCrypto ).value(for: selectedLocale) + DispatchQueue.main.async { self.view?.didReceiveNetworkFee(fee: feeViewModel) } + networkFeeViewModel = feeViewModel checkLoadingState() } - + private func buildBalanceSwapToViewModelFactory( wallet: MetaAccountModel, for chainAsset: ChainAsset? @@ -183,7 +242,7 @@ final class LiquidityPoolSupplyPresenter { ) return balanceViewModelFactory } - + private func runCanXorPayValidation(sendAmount: Decimal) { DataValidationRunner(validators: [ dataValidatingFactory.canPayFeeAndAmount( @@ -194,14 +253,25 @@ final class LiquidityPoolSupplyPresenter { ) ]).runValidation {} } + + private func provideInitialData() { + swapFromChainAsset = chain.chainAssets.first(where: { $0.asset.currencyId == liquidityPair.baseAssetId }) + swapToChainAsset = chain.chainAssets.first(where: { $0.asset.currencyId == liquidityPair.targetAssetId }) + + provideToAssetVewModel() + provideFromAssetVewModel() + + refreshFee() + } } // MARK: - LiquidityPoolSupplyViewOutput + extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { func didTapBackButton() { router.dismiss(view: view) } - + func didTapApyInfo() { var infoText: String var infoTitle: String @@ -215,51 +285,81 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { from: view ) } - - func didTapPreviewButton() { - - } - + + func didTapPreviewButton() {} + func selectFromAmountPercentage(_ percentage: Float) { runLoadingState() swapVariant = .desiredInput swapFromInputResult = .rate(Decimal(Double(percentage))) + + let baseAssetAbsolulteValue = swapFromInputResult?.absoluteValue(from: swapFromBalance.or(.zero)) + let targetAssetAbsoluteValue = baseAssetAbsolulteValue.or(.zero) * baseTargetRate.or(.zero) + swapToInputResult = .absolute(targetAssetAbsoluteValue) + provideFromAssetVewModel() + provideToAssetVewModel() - if swapFromChainAsset == xorChainAsset { + if swapFromChainAsset == chain.utilityChainAssets().first { let inputAmount = swapFromInputResult? .absoluteValue(from: xorBalanceMinusFee) runCanXorPayValidation(sendAmount: inputAmount ?? .zero) } + + refreshFee() } - + func updateFromAmount(_ newValue: Decimal) { runLoadingState() swapVariant = .desiredInput swapFromInputResult = .absolute(newValue) + + let baseAssetAbsolulteValue = swapFromInputResult?.absoluteValue(from: swapFromBalance.or(.zero)) + let targetAssetAbsoluteValue = baseAssetAbsolulteValue.or(.zero) * baseTargetRate.or(.zero) + swapToInputResult = .absolute(targetAssetAbsoluteValue) + provideFromAssetVewModel(updateAmountInput: false) + provideToAssetVewModel() + + refreshFee() } - + func selectToAmountPercentage(_ percentage: Float) { runLoadingState() swapVariant = .desiredOutput swapToInputResult = .rate(Decimal(Double(percentage))) + + let targetAssetAbsoluteValue = swapToInputResult?.absoluteValue(from: swapToBalance.or(.zero)) + let baseAssetAbsolulteValue = targetAssetAbsoluteValue.or(.zero) * baseTargetRate.or(.zero) + swapFromInputResult = .absolute(baseAssetAbsolulteValue) + + provideFromAssetVewModel() provideToAssetVewModel() + + refreshFee() } - + func updateToAmount(_ newValue: Decimal) { runLoadingState() swapVariant = .desiredOutput swapToInputResult = .absolute(newValue) + + let targetAssetAbsoluteValue = swapToInputResult?.absoluteValue(from: swapToBalance.or(.zero)) + let baseAssetAbsolulteValue = targetAssetAbsoluteValue.or(.zero) * baseTargetRate.or(.zero) + swapFromInputResult = .absolute(baseAssetAbsolulteValue) + + provideFromAssetVewModel() provideToAssetVewModel(updateAmountInput: false) + + refreshFee() } - + func didTapSelectFromAsset() { - let showChainAssets = xorChainAsset.chain.chainAssets + let showChainAssets = chain.chainAssets .filter { $0.chainAssetId != swapToChainAsset?.chainAssetId } router.showSelectAsset( from: view, @@ -270,9 +370,9 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { output: self ) } - + func didTapSelectToAsset() { - let showChainAssets = xorChainAsset.chain.chainAssets + let showChainAssets = chain.chainAssets .filter { $0.chainAssetId != swapFromChainAsset?.chainAssetId } router.showSelectAsset( from: view, @@ -283,17 +383,100 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { output: self ) } - + func didLoad(view: LiquidityPoolSupplyViewInput) { self.view = view interactor.setup(with: self) + provideInitialData() } } // MARK: - LiquidityPoolSupplyInteractorOutput -extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyInteractorOutput {} + +extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyInteractorOutput { + func didReceiveDexId(_ dexId: String) { + self.dexId = dexId + refreshFee() + } + + func didReceiveDexIdError(_ error: Error) { + logger.customError(error) + } + + func didReceiveFee(_ fee: BigUInt) { + guard let utilityAsset = chain.utilityAssets().first else { + return + } + + networkFee = Decimal.fromSubstrateAmount(fee, precision: Int16(utilityAsset.precision)) + provideFeeViewModel() + } + + func didReceiveFeeError(_ error: Error) { + logger.customError(error) + } + + func didReceivePricesData(result: Result<[PriceData], Error>) { + switch result { + case let .success(priceData): + prices = priceData + + let baseAssetPrice = prices?.first(where: { $0.priceId == swapFromChainAsset?.asset.priceId }) + let targetAssetPrice = prices?.first(where: { $0.priceId == swapToChainAsset?.asset.priceId }) + + if + let baseAssetPrice = baseAssetPrice, + let targetAssetPrice = targetAssetPrice, + let baseAssetPriceValue = Decimal(string: baseAssetPrice.price), + let targetAssetPriceValue = Decimal(string: targetAssetPrice.price) { + baseTargetRate = baseAssetPriceValue / targetAssetPriceValue + } + case let .failure(error): + prices = [] + logger.error("\(error)") + } + + provideFromAssetVewModel() + provideToAssetVewModel() + } + + func didReceiveAccountInfo(result: Result, for chainAsset: ChainAsset) { + switch result { + case let .success(accountInfo): + if swapFromChainAsset == chainAsset { + swapFromBalance = accountInfo.map { + Decimal.fromSubstrateAmount( + $0.data.sendAvailable, + precision: Int16(chainAsset.asset.precision) + ) + } ?? .zero + provideFromAssetVewModel() + } + if swapToChainAsset == chainAsset { + swapToBalance = accountInfo.map { + Decimal.fromSubstrateAmount( + $0.data.sendAvailable, + precision: Int16(chainAsset.asset.precision) + ) + } ?? .zero + provideToAssetVewModel() + } + if chain.utilityChainAssets().first == chainAsset { + xorBalance = accountInfo.map { + Decimal.fromSubstrateAmount( + $0.data.sendAvailable, + precision: Int16(chainAsset.asset.precision) + ) + } ?? .zero + } + case let .failure(error): + router.present(error: error, from: view, locale: selectedLocale) + } + } +} // MARK: - Localizable + extension LiquidityPoolSupplyPresenter: Localizable { func applyLocalization() {} } @@ -307,7 +490,10 @@ extension LiquidityPoolSupplyPresenter: SelectAssetModuleOutput { didCompleteWith chainAsset: ChainAsset?, contextTag: Int? ) { - view?.didUpdating() + DispatchQueue.main.async { [weak self] in + self?.view?.didUpdating() + } + guard let rawValue = contextTag, let input = InputTag(rawValue: rawValue), let chainAsset = chainAsset @@ -326,6 +512,6 @@ extension LiquidityPoolSupplyPresenter: SelectAssetModuleOutput { runLoadingState() - //TODO : Refresh fee + refreshFee() } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyProtocols.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyProtocols.swift index aa371ff413..7b906e18ca 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyProtocols.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyProtocols.swift @@ -1,3 +1,5 @@ +import SSFModels + typealias LiquidityPoolSupplyModuleCreationResult = ( view: LiquidityPoolSupplyViewInput, input: LiquidityPoolSupplyModuleInput diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyRouter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyRouter.swift index d75e7adbc2..8a3f6bae8a 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyRouter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyRouter.swift @@ -1,4 +1,5 @@ import Foundation +import SSFModels final class LiquidityPoolSupplyRouter: LiquidityPoolSupplyRouterInput { func showSelectAsset( diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift index ef3437b58b..41c9ad65c7 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift @@ -11,25 +11,25 @@ protocol LiquidityPoolSupplyViewOutput: AnyObject { func updateFromAmount(_ newValue: Decimal) func selectToAmountPercentage(_ percentage: Float) func updateToAmount(_ newValue: Decimal) - func didTapSelectFromAsset() - func didTapSelectToAsset() } final class LiquidityPoolSupplyViewController: UIViewController, ViewHolder, HiddableBarWhenPushed { typealias RootViewType = LiquidityPoolSupplyViewLayout var keyboardHandler: FearlessKeyboardHandler? - + private enum Constants { static let delay: CGFloat = 0.7 } - + // MARK: Private properties + private let output: LiquidityPoolSupplyViewOutput - + private var amountFromInputViewModel: IAmountInputViewModel? private var amountToInputViewModel: IAmountInputViewModel? // MARK: - Constructor + init( output: LiquidityPoolSupplyViewOutput, localizationManager: LocalizationManagerProtocol? @@ -45,6 +45,7 @@ final class LiquidityPoolSupplyViewController: UIViewController, ViewHolder, Hid } // MARK: - Life cycle + override func loadView() { view = LiquidityPoolSupplyViewLayout() } @@ -56,9 +57,9 @@ final class LiquidityPoolSupplyViewController: UIViewController, ViewHolder, Hid configure() addEndEditingTapGesture(for: rootView.contentView) } - + // MARK: - Private methods - + private func configure() { navigationController?.setNavigationBarHidden(true, animated: true) @@ -70,25 +71,18 @@ final class LiquidityPoolSupplyViewController: UIViewController, ViewHolder, Hid rootView.swapFromInputView.textField.inputAccessoryView = accessoryView updatePreviewButton() } - + private func updatePreviewButton() { let isEnabled = amountFromInputViewModel?.isValid == true && amountToInputViewModel?.isValid == true rootView.previewButton.set(enabled: isEnabled) } - + private func setupActions() { rootView.backButton.addTarget( self, action: #selector(handleTapBackButton), for: .touchUpInside ) - rootView.swapFromInputView.selectHandler = { [weak self] in - self?.output.didTapSelectFromAsset() - } - rootView.swapToInputView.selectHandler = { [weak self] in - self?.output.didTapSelectToAsset() - } - let tapMinReceiveInfo = UITapGestureRecognizer( target: self, action: #selector(handleTapApyInfo) @@ -102,17 +96,13 @@ final class LiquidityPoolSupplyViewController: UIViewController, ViewHolder, Hid for: .touchUpInside ) } - + // MARK: - Private actions @objc private func handleTapBackButton() { output.didTapBackButton() } - @objc private func handleTapMarketButton() { - output.didTapMarketButton() - } - @objc private func handleTapApyInfo() { output.didTapApyInfo() } @@ -123,14 +113,56 @@ final class LiquidityPoolSupplyViewController: UIViewController, ViewHolder, Hid } // MARK: - LiquidityPoolSupplyViewInput -extension LiquidityPoolSupplyViewController: LiquidityPoolSupplyViewInput {} + +extension LiquidityPoolSupplyViewController: LiquidityPoolSupplyViewInput { + func didReceiveSwapFrom(viewModel: AssetBalanceViewModelProtocol?) { + rootView.bindSwapFrom(assetViewModel: viewModel) + } + + func didReceiveSwapTo(viewModel: AssetBalanceViewModelProtocol?) { + rootView.bindSwapTo(assetViewModel: viewModel) + } + + func didReceiveSwapFrom(amountInputViewModel: IAmountInputViewModel?) { + amountFromInputViewModel = amountInputViewModel + amountInputViewModel?.observable.remove(observer: self) + amountInputViewModel?.observable.add(observer: self) + rootView.swapFromInputView.inputFieldText = amountInputViewModel?.displayAmount + updatePreviewButton() + } + + func didReceiveSwapTo(amountInputViewModel: IAmountInputViewModel?) { + amountToInputViewModel = amountInputViewModel + amountInputViewModel?.observable.remove(observer: self) + amountInputViewModel?.observable.add(observer: self) + rootView.swapToInputView.inputFieldText = amountInputViewModel?.displayAmount + updatePreviewButton() + } + + func didReceiveNetworkFee(fee: BalanceViewModelProtocol?) { + rootView.bind(fee: fee) + updatePreviewButton() + } + + func setButtonLoadingState(isLoading: Bool) { + rootView.previewButton.set(loading: isLoading) + } + + func didUpdating() { + DispatchQueue.main.async { + self.rootView.previewButton.set(enabled: false) + } + } +} // MARK: - Localizable + extension LiquidityPoolSupplyViewController: Localizable { func applyLocalization() { rootView.locale = selectedLocale } } + // MARK: - AmountInputAccessoryViewDelegate extension LiquidityPoolSupplyViewController: AmountInputAccessoryViewDelegate { @@ -168,15 +200,8 @@ extension LiquidityPoolSupplyViewController: UITextFieldDelegate { } func textFieldDidBeginEditing(_ textField: UITextField) { - let swapFromIsFirstResponder = textField == rootView.swapFromInputView.textField - rootView.swapFromInputView.set(highlighted: swapFromIsFirstResponder, animated: false) - - if textField == rootView.swapToInputView.textField, amountToInputViewModel != nil { - let swapToIsFirstResponder = textField == rootView.swapToInputView.textField - rootView.swapToInputView.set(highlighted: swapToIsFirstResponder, animated: false) - } else if textField == rootView.swapToInputView.textField { - rootView.swapToInputView.textField.resignFirstResponder() - output.didTapSelectToAsset() + if textField == rootView.swapToInputView.textField { + rootView.swapFromInputView.textField.resignFirstResponder() } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift index b359e95b18..4bb287eb59 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift @@ -41,8 +41,8 @@ final class LiquidityPoolSupplyViewLayout: UIView { return view }() - let swapFromInputView = SelectableAmountInputView(type: .swapSend) - let swapToInputView = SelectableAmountInputView(type: .swapReceive) + let swapFromInputView = AmountInputViewV2() + let swapToInputView = AmountInputViewV2() let switchSwapButton: UIButton = { let button = UIButton() button.setImage(R.image.iconSwitch(), for: .normal) @@ -105,10 +105,18 @@ final class LiquidityPoolSupplyViewLayout: UIView { } func bindSwapFrom(assetViewModel: AssetBalanceViewModelProtocol?) { + guard let assetViewModel else { + return + } + swapFromInputView.bind(viewModel: assetViewModel) } func bindSwapTo(assetViewModel: AssetBalanceViewModelProtocol?) { + guard let assetViewModel else { + return + } + swapToInputView.bind(viewModel: assetViewModel) } diff --git a/fearless/Modules/Profile/ProfileInteractor.swift b/fearless/Modules/Profile/ProfileInteractor.swift index 99c7427b06..becbfd27b9 100644 --- a/fearless/Modules/Profile/ProfileInteractor.swift +++ b/fearless/Modules/Profile/ProfileInteractor.swift @@ -2,6 +2,7 @@ import Foundation import SoraKeystore import IrohaCrypto import RobinHood +import SSFModels enum ProfileInteractorError: Error { case noSelectedAccount diff --git a/fearless/Modules/Profile/ProfilePresenter.swift b/fearless/Modules/Profile/ProfilePresenter.swift index b93f281d99..2b43263a61 100644 --- a/fearless/Modules/Profile/ProfilePresenter.swift +++ b/fearless/Modules/Profile/ProfilePresenter.swift @@ -1,6 +1,7 @@ import Foundation import SoraFoundation import SoraKeystore +import SSFModels final class ProfilePresenter { private weak var view: ProfileViewProtocol? diff --git a/fearless/Modules/Profile/ProfileProtocol.swift b/fearless/Modules/Profile/ProfileProtocol.swift index b0ac47f2d6..a75c26d286 100644 --- a/fearless/Modules/Profile/ProfileProtocol.swift +++ b/fearless/Modules/Profile/ProfileProtocol.swift @@ -1,4 +1,5 @@ import Foundation +import SSFModels protocol ProfileViewProtocol: ControllerBackedProtocol { func didReceive(state: ProfileViewState) diff --git a/fearless/Modules/SelectCurrency/SelectCurrencyInteractor.swift b/fearless/Modules/SelectCurrency/SelectCurrencyInteractor.swift index 721b76b0f3..f92e9bb45d 100644 --- a/fearless/Modules/SelectCurrency/SelectCurrencyInteractor.swift +++ b/fearless/Modules/SelectCurrency/SelectCurrencyInteractor.swift @@ -1,6 +1,7 @@ import UIKit import SoraKeystore import RobinHood +import SSFModels final class SelectCurrencyInteractor { // MARK: - Private properties diff --git a/fearless/Modules/SelectCurrency/SelectCurrencyPresenter.swift b/fearless/Modules/SelectCurrency/SelectCurrencyPresenter.swift index d8c25f488d..1b05b69287 100644 --- a/fearless/Modules/SelectCurrency/SelectCurrencyPresenter.swift +++ b/fearless/Modules/SelectCurrency/SelectCurrencyPresenter.swift @@ -1,5 +1,6 @@ import Foundation import SoraFoundation +import SSFModels final class SelectCurrencyPresenter { // MARK: Private properties diff --git a/fearless/Modules/SelectCurrency/SelectCurrencyProtocols.swift b/fearless/Modules/SelectCurrency/SelectCurrencyProtocols.swift index 9867b9588e..986cc065e7 100644 --- a/fearless/Modules/SelectCurrency/SelectCurrencyProtocols.swift +++ b/fearless/Modules/SelectCurrency/SelectCurrencyProtocols.swift @@ -1,3 +1,5 @@ +import SSFModels + typealias SelectCurrencyModuleCreationResult = (view: SelectCurrencyViewInput, input: SelectCurrencyModuleInput) protocol SelectCurrencyViewInput: ControllerBackedProtocol { diff --git a/fearless/Modules/SelectCurrency/viewModel/SelectCurrencyViewModelFactory.swift b/fearless/Modules/SelectCurrency/viewModel/SelectCurrencyViewModelFactory.swift index 69ab733fb0..85efd3173f 100644 --- a/fearless/Modules/SelectCurrency/viewModel/SelectCurrencyViewModelFactory.swift +++ b/fearless/Modules/SelectCurrency/viewModel/SelectCurrencyViewModelFactory.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import SSFModels protocol SelectCurrencyViewModelFactoryProtocol { func buildViewModel( From 99a4b18cfc0320eda7522a19dab268bc082aaa34 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Thu, 30 May 2024 15:30:12 +0500 Subject: [PATCH 010/115] dependencies update --- Podfile.lock | 7 +------ fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index 0fd03383ec..90f8a86a24 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -78,7 +78,6 @@ PODS: - SwiftFormat/CLI (0.47.13) - SwiftLint (0.54.0) - SwiftyBeaver (2.0.0) - - XNetworking (0.0.37) DEPENDENCIES: - Charts (~> 4.1.0) @@ -98,7 +97,6 @@ DEPENDENCIES: - SwiftFormat/CLI (~> 0.47.13) - SwiftLint - SwiftyBeaver - - XNetworking (from `https://raw.githubusercontent.com/soramitsu/x-networking/0.0.37/AppCommonNetworking/XNetworking/XNetworking.podspec`) SPEC REPOS: https://github.com/CocoaPods/Specs.git: @@ -129,8 +127,6 @@ EXTERNAL SOURCES: SoraKeystore: :git: https://github.com/soramitsu/keystore-iOS.git :tag: 1.0.1 - XNetworking: - :podspec: https://raw.githubusercontent.com/soramitsu/x-networking/0.0.37/AppCommonNetworking/XNetworking/XNetworking.podspec CHECKOUT OPTIONS: MediaView: @@ -161,8 +157,7 @@ SPEC CHECKSUMS: SwiftFormat: 73573b89257437c550b03d934889725fbf8f75e5 SwiftLint: c1de071d9d08c8aba837545f6254315bc900e211 SwiftyBeaver: 014b0c12065026b731bac80305294f27d63e27f6 - XNetworking: 516d982bd74ff208222381d27e07052a5d897aaf -PODFILE CHECKSUM: 1684027b944c28b30cfce215940039c95ff4a95d +PODFILE CHECKSUM: 934ddb815cb022338f1abd75110a1a634cde7340 COCOAPODS: 1.15.2 diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index 245a49fc6d..adddc62f73 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -133,7 +133,7 @@ "location" : "https://github.com/soramitsu/shared-features-spm.git", "state" : { "branch" : "update-for-fearless-asset-management", - "revision" : "dd3ffa9016d20d9f3695d76b976df4c857ec1efb" + "revision" : "f1f4bfc22cc7782d8f48f89072227c9f3ae3c46b" } }, { From 2396dac8395d372e960e2bb840f7d7dc9f9853a3 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Fri, 31 May 2024 13:03:11 +0500 Subject: [PATCH 011/115] FLW-4635 New bridge route SORA <-> Liberland. Local data updated --- fearless.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 303 ---- fearless/Resources/chains.json | 1430 +++++++++++++++-- 3 files changed, 1298 insertions(+), 437 deletions(-) delete mode 100644 fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index 6a5f772203..abff3ebf00 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -19464,7 +19464,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/soramitsu/shared-features-spm.git"; requirement = { - branch = "update-for-fearless-asset-management"; + branch = "feature/xcm-routes"; kind = branch; }; }; diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index adddc62f73..0000000000 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,303 +0,0 @@ -{ - "originHash" : "48bf9205e83b67ed7d81b2a0bcc8ef081189084205112c031b7823572f561e20", - "pins" : [ - { - "identity" : "appauth-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/openid/AppAuth-iOS.git", - "state" : { - "revision" : "c89ed571ae140f8eb1142735e6e23d7bb8c34cb2", - "version" : "1.7.5" - } - }, - { - "identity" : "bigint", - "kind" : "remoteSourceControl", - "location" : "https://github.com/attaswift/BigInt.git", - "state" : { - "revision" : "0ed110f7555c34ff468e72e1686e59721f2b0da6", - "version" : "5.3.0" - } - }, - { - "identity" : "cryptoswift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", - "state" : { - "revision" : "c9c3df6ab812de32bae61fc0cd1bf6d45170ebf0", - "version" : "1.8.2" - } - }, - { - "identity" : "fearless-starscream", - "kind" : "remoteSourceControl", - "location" : "https://github.com/soramitsu/fearless-starscream", - "state" : { - "revision" : "3e1de9baeab87de379e0cb01c64d5db18fbf130f", - "version" : "4.0.12" - } - }, - { - "identity" : "google-api-objectivec-client-for-rest", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/google-api-objectivec-client-for-rest.git", - "state" : { - "revision" : "d46fb27cf61e08285a727c18a2ae0dbc20d91b2f", - "version" : "3.5.4" - } - }, - { - "identity" : "googlesignin-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleSignIn-iOS", - "state" : { - "revision" : "a7965d134c5d3567026c523e0a8a583f73b62b0d", - "version" : "7.1.0" - } - }, - { - "identity" : "gtm-session-fetcher", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/gtm-session-fetcher.git", - "state" : { - "revision" : "0382ca27f22fb3494cf657d8dc356dc282cd1193", - "version" : "3.4.1" - } - }, - { - "identity" : "gtmappauth", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GTMAppAuth.git", - "state" : { - "revision" : "5d7d66f647400952b1758b230e019b07c0b4b22a", - "version" : "4.1.1" - } - }, - { - "identity" : "nimble", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Quick/Nimble", - "state" : { - "revision" : "e9d769113660769a4d9dd3afb855562c0b7ae7b0", - "version" : "7.3.4" - } - }, - { - "identity" : "promisekit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/mxcl/PromiseKit.git", - "state" : { - "revision" : "8a98e31a47854d3180882c8068cc4d9381bf382d", - "version" : "6.22.1" - } - }, - { - "identity" : "qrcode", - "kind" : "remoteSourceControl", - "location" : "https://github.com/WalletConnect/QRCode", - "state" : { - "revision" : "263f280d2c8144adfb0b6676109846cfc8dd552b", - "version" : "14.3.1" - } - }, - { - "identity" : "quick", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Quick/Quick", - "state" : { - "revision" : "f2b5a06440ea87eba1a167cab37bf6496646c52e", - "version" : "1.3.4" - } - }, - { - "identity" : "reachability.swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ashleymills/Reachability.swift", - "state" : { - "revision" : "7cbd73f46a7dfaeca079e18df7324c6de6d1834a", - "version" : "5.2.3" - } - }, - { - "identity" : "secp256k1.swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Boilertalk/secp256k1.swift.git", - "state" : { - "revision" : "cd187c632fb812fd93711a9f7e644adb7e5f97f0", - "version" : "0.1.7" - } - }, - { - "identity" : "shared-features-spm", - "kind" : "remoteSourceControl", - "location" : "https://github.com/soramitsu/shared-features-spm.git", - "state" : { - "branch" : "update-for-fearless-asset-management", - "revision" : "f1f4bfc22cc7782d8f48f89072227c9f3ae3c46b" - } - }, - { - "identity" : "swift-atomics", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-atomics.git", - "state" : { - "revision" : "cd142fd2f64be2100422d658e7411e39489da985", - "version" : "1.2.0" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections.git", - "state" : { - "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", - "version" : "1.1.0" - } - }, - { - "identity" : "swift-http-types", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-http-types", - "state" : { - "revision" : "9bee2fdb79cc740081abd8ebd80738063d632286", - "version" : "1.1.0" - } - }, - { - "identity" : "swift-nio", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-nio.git", - "state" : { - "revision" : "359c461e5561d22c6334828806cc25d759ca7aa6", - "version" : "2.65.0" - } - }, - { - "identity" : "swift-nio-extras", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-nio-extras.git", - "state" : { - "revision" : "a3b640d7dc567225db7c94386a6e71aded1bfa63", - "version" : "1.22.0" - } - }, - { - "identity" : "swift-nio-http2", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-nio-http2.git", - "state" : { - "revision" : "c6afe04165c865faaa687b42c32ed76dfcc91076", - "version" : "1.31.0" - } - }, - { - "identity" : "swift-nio-ssl", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-nio-ssl.git", - "state" : { - "revision" : "7c381eb6083542b124a6c18fae742f55001dc2b5", - "version" : "2.26.0" - } - }, - { - "identity" : "swift-nio-transport-services", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-nio-transport-services.git", - "state" : { - "revision" : "38ac8221dd20674682148d6451367f89c2652980", - "version" : "1.21.0" - } - }, - { - "identity" : "swift-qrcode-generator", - "kind" : "remoteSourceControl", - "location" : "https://github.com/dagronf/swift-qrcode-generator", - "state" : { - "revision" : "5ca09b6a2ad190f94aa3d6ddef45b187f8c0343b", - "version" : "1.0.3" - } - }, - { - "identity" : "swift-system", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-system.git", - "state" : { - "revision" : "f9266c85189c2751589a50ea5aec72799797e471", - "version" : "1.3.0" - } - }, - { - "identity" : "swiftimagereadwrite", - "kind" : "remoteSourceControl", - "location" : "https://github.com/dagronf/SwiftImageReadWrite", - "state" : { - "revision" : "5596407d1cf61b953b8e658fa8636a471df3c509", - "version" : "1.1.6" - } - }, - { - "identity" : "swiftybeaver", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SwiftyBeaver/SwiftyBeaver.git", - "state" : { - "revision" : "8cba041db09596183331d123f337d0eb2e6e8e91", - "version" : "2.1.1" - } - }, - { - "identity" : "swime", - "kind" : "remoteSourceControl", - "location" : "https://github.com/sendyhalim/Swime", - "state" : { - "revision" : "4e538834483059ceefaaad8cdb3abe0d7d1c5146", - "version" : "3.1.0" - } - }, - { - "identity" : "tweetnacl-swiftwrap", - "kind" : "remoteSourceControl", - "location" : "https://github.com/bitmark-inc/tweetnacl-swiftwrap", - "state" : { - "revision" : "f8fd111642bf2336b11ef9ea828510693106e954", - "version" : "1.1.0" - } - }, - { - "identity" : "walletconnectswiftv2", - "kind" : "remoteSourceControl", - "location" : "https://github.com/WalletConnect/WalletConnectSwiftV2", - "state" : { - "revision" : "58d2b49eeac5cf94432e2647b9107577c156a25c", - "version" : "1.9.9" - } - }, - { - "identity" : "web3.swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/bnsports/Web3.swift.git", - "state" : { - "revision" : "a526779488e5fe2fa993d9614f11f57b00cc1858", - "version" : "7.7.7" - } - }, - { - "identity" : "websocket-kit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/vapor/websocket-kit", - "state" : { - "revision" : "4232d34efa49f633ba61afde365d3896fc7f8740", - "version" : "2.15.0" - } - }, - { - "identity" : "xxhash-swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/daisuke-t-jp/xxHash-Swift", - "state" : { - "revision" : "e86a07ab4867f81481d430e1370a5ec97b6e3359", - "version" : "1.1.1" - } - } - ], - "version" : 3 -} diff --git a/fearless/Resources/chains.json b/fearless/Resources/chains.json index aabf9f6345..6c71ca2159 100644 --- a/fearless/Resources/chains.json +++ b/fearless/Resources/chains.json @@ -1,16 +1,16 @@ [{ "disabled": false, "chainId": "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3", - "rank": 3, + "rank": 8, "name": "Polkadot", "externalApi": { "staking": { "type": "subsquid", - "url": "https://squid.subsquid.io/fearless-x-polkadot/v/v1/graphql" + "url": "https://squid.subsquid.io/fearless-polkadot/v/v3/graphql" }, "history": { "type": "subsquid", - "url": "https://squid.subsquid.io/fearless-x-polkadot/v/v1/graphql" + "url": "https://squid.subsquid.io/fearless-polkadot/v/v3/graphql" }, "crowdloans": { "type": "github", @@ -100,6 +100,17 @@ "symbol": "DOT" } ] + }, + { + "chainId": "7e4e32d0feafd4f9c9414b0be86373f9a1efa904809b683453a9af6856d38ad5", + "assets": [ + { + "id": "99e66d4f-00cd-4d73-bd1b-3adadcacffb2", + "symbol": "DOT" + } + + ], + "bridgeParachainId": "e92d165ad41e41e215d09713788173aecfdbe34d3bed29409d33a2ef03980738" } ] }, @@ -126,16 +137,17 @@ { "disabled": false, "chainId": "b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe", - "rank": 4, + "rank": 9, "name": "Kusama", + "identityChain": "c1af4cb4eb3918e5db15086c0cc5ec17fb334f728b7c65dd44bfe1e174ff8b3f", "externalApi": { "staking": { "type": "subsquid", - "url": "https://squid.subsquid.io/fearless-x-kusama/v/v1/graphql" + "url": "https://squid.subsquid.io/fearless-kusama-2/v/v3/graphql" }, "history": { "type": "subsquid", - "url": "https://squid.subsquid.io/fearless-x-kusama/v/v1/graphql" + "url": "https://squid.subsquid.io/fearless-kusama-2/v/v3/graphql" }, "crowdloans": { "type": "github", @@ -247,16 +259,9 @@ { "disabled": false, "chainId": "e143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e", + "rank": 108, "name": "Westend", "externalApi": { - "staking": { - "type": "subquery", - "url": "https://api.subquery.network/sq/soramitsu/fearless-wallet-westend" - }, - "history": { - "type": "subquery", - "url": "https://api.subquery.network/sq/soramitsu/fearless-wallet-westend" - }, "crowdloans": { "type": "github", "url": "https://raw.githubusercontent.com/soramitsu/fearless-utils/master/crowdloan/westend.json" @@ -447,7 +452,10 @@ } ], "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/Assethub.svg", - "addressPrefix": 2 + "addressPrefix": 2, + "options": [ + "utilityFeePayment" + ] }, { "disabled": false, @@ -494,6 +502,26 @@ "priceId": "tether", "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/USDT.svg", "color": "26A17B" + }, + { + "id": "8f79aa5a-9f31-442c-ac96-01ff80b105e0", + "type": "assets", + "name": "dot is $ded", + "symbol": "ded", + "precision": 10, + "currencyId": "30", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/DED.svg", + "color": "FE0186" + }, + { + "id": "44587704-7da8-45c3-9541-be7b81de76ee", + "type": "assets", + "name": "pink", + "symbol": "pink", + "precision": 10, + "currencyId": "23", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/PINK.svg", + "color": "FF0066" } ], "xcm": { @@ -563,7 +591,10 @@ } ], "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/Assethub.svg", - "addressPrefix": 0 + "addressPrefix": 0, + "options": [ + "utilityFeePayment" + ] }, { "disabled": false, @@ -789,6 +820,17 @@ "symbol": "ACA" } ] + }, + { + "chainId": "7e4e32d0feafd4f9c9414b0be86373f9a1efa904809b683453a9af6856d38ad5", + "assets": [ + { + "id": "7528bb5b-2aa9-4a70-a4ed-3476aec0f87d", + "symbol": "ACA" + } + + ], + "bridgeParachainId": "e92d165ad41e41e215d09713788173aecfdbe34d3bed29409d33a2ef03980738" } ] }, @@ -815,7 +857,10 @@ } ], "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/acala.svg", - "addressPrefix": 10 + "addressPrefix": 10, + "options": [ + "utilityFeePayment" + ] }, { "disabled": false, @@ -1296,12 +1341,16 @@ } ], "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/Karura.svg", - "addressPrefix": 8 + "addressPrefix": 8, + "options": [ + "utilityFeePayment" + ] }, { "disabled": false, "chainId": "401a1f9dca3da46f5c4091016c8a2f26dcea05865116b286f60f668207d1474b", "parentId": "b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe", + "rank": 11, "name": "Moonriver", "paraId": "2023", "externalApi": { @@ -2684,10 +2733,7 @@ ] }, - "nodes": [{ - "url": "wss://rpc.parallel.fi", - "name": "Parallel node" - }, + "nodes": [ { "url": "wss://parallel-rpc.dwellir.com", "name": "Dwellir node" @@ -2760,6 +2806,7 @@ "disabled": false, "chainId": "fe58ea77779b7abda7da4ec526d14db9b1e9cd40a217c34892af80a9b332b76d", "parentId": "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3", + "rank": 12, "paraId": "2004", "name": "Moonbeam", "externalApi": { @@ -2888,6 +2935,16 @@ "priceId": "pha", "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/PHA.svg", "color": "DAFE6F" + }, + { + "id": "6fed1ff5-3aa5-4d94-b07a-22dfc0770fc0", + "type": "assets", + "name": "pink", + "symbol": "xcpink", + "precision": 10, + "currencyId": "64174511183114006009298114091987195453", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/PINK.svg", + "color": "FF0066" } ], "xcm": { @@ -2986,6 +3043,7 @@ { "disabled": false, "chainId": "91bc6e169807aaa54802737e1c504b2577d4fafedd5a02c10293b1cd60e39527", + "rank": 112, "name": "Moonbase Alpha", "externalApi": { "staking": { @@ -4728,6 +4786,7 @@ { "disabled": false, "chainId": "3266816be9fa51b32cfea58d3e33ca77246bc9618595a4300e44c8856a8d8a17", + "rank": 100, "name": "SORA test", "externalApi": { "history": { @@ -4736,7 +4795,7 @@ }, "staking": { "type": "sora", - "url": "https://squid.subsquid.io/sora-stage/v/v4/graphql" + "url": "https://squid.subsquid.io/sora-stage/v/v5/graphql" } }, "xcm": { @@ -4919,27 +4978,62 @@ "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/KSM.svg", "color": "FFFFFF", "type": "soraAsset" - } - ], - "nodes": [{ - "url": "wss://ws.framenode-1.s1.stg1.sora2.soramitsu.co.jp", - "name": "Sora Stage Node #1" }, { - "url": "wss://ws.framenode-2.s1.stg1.sora2.soramitsu.co.jp", - "name": "Sora Stage Node #2" + "id": "ada3b18e-1912-4f96-ad3b-4d0e1b1d1d0a", + "symbol": "tbcd", + "name": "sora tbc dollar", + "currencyId": "0x02000a0000000000000000000000000000000000000000000000000000000000", + "precision": 18, + "icon": "https://raw.githubusercontent.com/soramitsu/fearless-utils/master/icons/tokens/coloured/TBCD.svg", + "color":"6D8954", + "type": "soraAsset", + "isNative": true + }, + { + "id": "eface91d-b2a8-49d2-88e8-640586bda477", + "name": "polkadot", + "symbol": "dot", + "currencyId": "0x0003b1dbee890acfb1b3bc12d1bb3b4295f52755423f84d1751b2545cebf000b", + "precision": 18, + "priceId": "polkadot", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/DOT.svg", + "color": "FF0066", + "type": "soraAsset" }, { - "url": "wss://ws.framenode-3.s2.stg1.sora2.soramitsu.co.jp", - "name": "Sora Stage Node #3" + "id": "191c31de-62b1-41e4-aad3-15a5be1b4cd4", + "name": "dai", + "symbol": "dai", + "currencyId": "0x0200060000000000000000000000000000000000000000000000000000000000", + "precision": 18, + "priceId": "dai", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/DAI.svg", + "color": "F9AF1A", + "type": "soraAsset" }, { - "url": "wss://ws.framenode-4.s2.stg1.sora2.soramitsu.co.jp", - "name": "Sora Stage Node #4" + "id": "3ab2c884-6c6e-4f92-b87a-a013c80210af", + "name": "ethereum", + "symbol": "eth", + "currencyId": "0x0200070000000000000000000000000000000000000000000000000000000000", + "precision": 18, + "priceId": "ethereum", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/ETH.svg", + "color": "627EEA", + "type": "soraAsset" + } + + ], + "nodes": [ + + { + "url": "wss://ws.framenode-7.s4.stg1.sora2.soramitsu.co.jp", + "name": "Sora Stage #7" }, { - "url": "wss://ws.framenode-5.s3.stg1.sora2.soramitsu.co.jp", - "name": "Sora Stage Node #5" + "url": "wss://ws.framenode-8.s5.stg1.sora2.soramitsu.co.jp", + "name": "Sora Stage #8" }, { "url": "wss://ws.framenode-1.r0.dev.sora2.soramitsu.co.jp", @@ -4969,11 +5063,11 @@ "externalApi": { "history": { "type": "sora", - "url": "https://squid.subsquid.io/sora/v/v4/graphql" + "url": "https://squid.subsquid.io/sora/v/v5/graphql" }, "staking": { "type" : "sora", - "url": "https://squid.subsquid.io/sora/v/v4/graphql" + "url": "https://squid.subsquid.io/sora/v/v5/graphql" }, "explorers": [{ "type": "subscan", @@ -5199,6 +5293,71 @@ "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/KSM.svg", "color": "FFFFFF", "type": "soraAsset" + }, + { + "id": "cd092a5a-4eb6-4318-9f11-4bf8454d67a2", + "name": "polkadot", + "symbol": "dot", + "currencyId": "0x0003b1dbee890acfb1b3bc12d1bb3b4295f52755423f84d1751b2545cebf000b", + "precision": 18, + "priceId": "polkadot", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/DOT.svg", + "color": "FF0066", + "type": "soraAsset" + }, + { + "id": "fb88fa55-b8c8-4ff1-afa8-f72a86a238a4", + "name": "acala", + "symbol": "aca", + "currencyId": "0x001ddbe1a880031da72f7ea421260bec635fa7d1aa72593d5412795408b6b2ba", + "precision": 18, + "priceId": "acala", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/ACA.svg", + "color": "FFFFFF", + "type": "soraAsset" + }, + { + "id": "acc32ee0-8fdc-4743-91e1-f70cc4f3069b", + "name": "astar", + "symbol": "astr", + "currencyId": "0x009dd037fcb32f4fe17c513abd4641a2ece844d106e30788124f0c0acc6e748e", + "precision": 18, + "priceId": "astar", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/ASTR.svg", + "color": "0AE2FF", + "type": "soraAsset" + }, + { + "id": "0ef3afdc-cdd3-47cc-bd52-817c54ae65b5", + "name": "liberland merit", + "symbol": "llm", + "currencyId": "0x0200000000000000000000000000010000000000000000000000000000000000", + "precision": 18, + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/LLM.svg", + "color": "EFB900", + "type": "soraAsset" + }, + { + "id": "1c3b4fcb-5a5f-4319-9dce-d178006eb9bf", + "name": "liberland dollar", + "symbol": "lld", + "currencyId": "0x00073edd278e1bd6a7f9d0b27d4f3e93b73c8f0832b58a4df13c69611a99f156", + "precision": 18, + "priceId": "liberland-lld", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/LLD.svg", + "color": "00437F", + "type": "soraAsset" + }, + { + "id": "a543b9a2-f85a-4974-92fc-435d6bc418e5", + "name": "toncoin", + "symbol": "toncoin", + "currencyId": "0x00e8c8923623335128807857dfa38f9212e9803394a2c976e97245d7063bbe10", + "precision": 18, + "priceId": "the-open-network", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/TON.svg", + "color": "0098EA", + "type": "soraAsset" } ], "xcm": { @@ -5207,6 +5366,26 @@ { "id": "5416b261-a759-4ba6-bc83-ea79a83c5101", "symbol": "KSM" + }, + { + "id": "cd092a5a-4eb6-4318-9f11-4bf8454d67a2", + "symbol": "DOT" + }, + { + "id": "fb88fa55-b8c8-4ff1-afa8-f72a86a238a4", + "symbol": "ACA" + }, + { + "id": "a6b83d39-a488-4b34-8352-280705a792ea", + "symbol": "LLD" + }, + { + "id": "b774c386-5cce-454a-a845-1ec0381538ec", + "symbol": "XOR" + }, + { + "id": "0ef3afdc-cdd3-47cc-bd52-817c54ae65b5", + "symbol": "LLM" } ], "availableDestinations": [ @@ -5218,6 +5397,41 @@ "symbol": "KSM" } ] + }, + { + "chainId": "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3", + "assets": [ + { + "id": "cd092a5a-4eb6-4318-9f11-4bf8454d67a2", + "symbol": "DOT" + } + ] + }, + { + "chainId": "fc41b9bd8ef8fe53d58c7ea67c794c7ec9a73daf05e6d54b14ff6342c99ba64c", + "assets": [ + { + "id": "fb88fa55-b8c8-4ff1-afa8-f72a86a238a4", + "symbol": "ACA" + } + ] + }, + { + "chainId": "6bd89e052d67a45bb60a9a23e8581053d5e0d619f15cb9865946937e690c42d6", + "assets": [ + { + "id": "a6b83d39-a488-4b34-8352-280705a792ea", + "symbol": "LLD" + }, + { + "id": "b774c386-5cce-454a-a845-1ec0381538ec", + "symbol": "XOR" + }, + { + "id": "0ef3afdc-cdd3-47cc-bd52-817c54ae65b5", + "symbol": "LLM" + } + ] } ] }, @@ -5519,6 +5733,7 @@ "disabled": false, "chainId": "5d3c298622d5634ed019bf61ea4b71655030015bde9beb0d6a24743714462c86", "parentId": "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3", + "rank": 10, "paraId": "2094", "name": "Pendulum", "externalApi": { @@ -5618,6 +5833,30 @@ "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/SORA.svg", "addressPrefix": 420 }, + { + "disabled": false, + "chainId": "e92d165ad41e41e215d09713788173aecfdbe34d3bed29409d33a2ef03980738", + "parentId": "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3", + "paraId": "2025", + "name": "SORA Polkadot parachain", + "assets": [{ + "id": "a7c696d7-42ce-4ed6-a036-4f0f76386c49", + "name": "sora", + "symbol": "xor", + "precision": 18, + "priceId": "sora", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/XOR.svg", + "color": "EE2233", + "isUtility": true, + "type": "normal" + }], + "nodes": [{ + "url": "wss://ws.parachain-collator-1.pc1.sora2.soramitsu.co.jp", + "name": "Soramitsu node" + }], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/SORA.svg", + "addressPrefix": 81 + }, { "disabled": false, "chainId": "1", @@ -5823,7 +6062,19 @@ "color": "243579", "type": "normal", "ethereumType": "erc20" - } + }, + { + "isUtility": false, + "id": "0x236501327e701692a281934230AF0b6BE8Df3353", + "name": "fluence", + "symbol": "flt", + "precision": 18, + "priceId": "fluence-2", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/FLT.svg", + "color": "FFFFFF", + "type": "normal", + "ethereumType": "erc20" + } ], "nodes": [ { @@ -5846,6 +6097,7 @@ { "disabled": false, "chainId": "5", + "rank": 101, "name": "Ethereum Goerli", "externalApi": { "explorers": [{ @@ -5913,6 +6165,7 @@ "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/ETH.svg", "addressPrefix": 0, "options": [ + "testnet", "ethereum", "utilityFeePayment" ] @@ -6105,6 +6358,7 @@ { "disabled": false, "chainId": "97", + "rank": 102, "name": "BNB Smart Chain Testnet", "externalApi": { "history": { @@ -6161,8 +6415,9 @@ "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/bnbchain.svg", "addressPrefix": 0, "options": [ - "ethereum", - "utilityFeePayment" + "testnet", + "ethereum", + "utilityFeePayment" ] }, { @@ -6171,8 +6426,8 @@ "name": "Sepolia", "externalApi": { "history": { - "type": "alchemy", - "url": "https://eth-sepolia.g.alchemy.com/v2/16bz4S4bROJsWH-eDpiJ21cx_c-KAAaP" + "type": "etherscan", + "url": "https://api-sepolia.etherscan.io/api" }, "explorers": [{ "type": "etherscan", @@ -6223,13 +6478,15 @@ "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/ETH.svg", "addressPrefix": 0, "options": [ - "ethereum", - "utilityFeePayment" + "testnet", + "ethereum", + "utilityFeePayment" ] }, { "disabled": false, "chainId": "137", + "rank": 3, "name": "Polygon", "externalApi": { "explorers": [ @@ -6254,7 +6511,7 @@ "name": "polygon", "symbol": "matic", "precision": 18, - "priceId": "matic", + "priceId": "matic-network", "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/MATIC.svg", "color": "8247E5", "type": "normal", @@ -6395,6 +6652,7 @@ { "disabled": false, "chainId": "80001", + "rank": 103, "name": "Polygon mumbai testnet", "externalApi": { "explorers": [ @@ -6448,27 +6706,16 @@ { "disabled": false, "chainId": "6408de7737c59c238890533af25896a2c20608d8b380bb01029acb392781063e", + "rank": 109, "name": "Rococo", "externalApi": { - "staking": { - "type": "subquery", - "url": "https://api.subquery.network/sq/soramitsu/fearless-wallet-westend" - }, - "history": { - "type": "subquery", - "url": "https://api.subquery.network/sq/soramitsu/fearless-wallet-westend" - }, - "crowdloans": { - "type": "github", - "url": "https://raw.githubusercontent.com/soramitsu/fearless-utils/master/crowdloan/westend.json" - }, "explorers": [{ "type": "subscan", "types": [ "extrinsic", "account" ], - "url": "https://westend.subscan.io/{type}/{value}" + "url": "https://rococo.subscan.io/{type}/{value}" }] }, "xcm": { @@ -6620,26 +6867,29 @@ ], "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/Enjin.svg", - "addressPrefix": 9030 + "addressPrefix": 9030, + "options": [ + "testnet" + ] }, { "disabled": false, "chainId": "42161", - "rank": 13, + "rank": 4, "name": "Arbitrum One", "externalApi": { "history": { - "type": "etherscan", - "url": "https://api.arbiscan.io/api" + "type": "oklink", + "url": "https://www.oklink.com/api/v5/explorer/address/transaction-list?chainShortName=ARBITRUM" }, "explorers": [ { - "type": "etherscan", + "type": "oklink", "types": [ "tx", "address" ], - "url": "https://arbiscan.io/{type}/{value}" + "url": "https://www.okx.com/web3/explorer/arbitrum/{type}/{value}?channelID=flessw" } ] }, @@ -6690,21 +6940,21 @@ { "disabled": false, "chainId": "10", - "rank": 14, + "rank": 5, "name": "OP Mainnet", "externalApi": { "history": { - "type": "etherscan", - "url": "https://api-optimistic.etherscan.io/api" + "type": "oklink", + "url": "https://www.oklink.com/api/v5/explorer/address/transaction-list?chainShortName=OP" }, "explorers": [ { - "type": "etherscan", + "type": "oklink", "types": [ "tx", "address" ], - "url": "https://optimistic.etherscan.io/{type}/{value}" + "url": "https://www.okx.com/web3/explorer/optimism/{type}/{value}?channelID=flessw" } ] }, @@ -6762,21 +7012,21 @@ { "disabled": false, "chainId": "43114", - "rank": 16, + "rank": 6, "name": "Avalanche C-Chain", "externalApi": { "history": { - "type": "etherscan", - "url": "https://api.snowtrace.io/api" + "type": "oklink", + "url": "https://www.oklink.com/api/v5/explorer/address/transaction-list?chainShortName=AVAXC" }, "explorers": [ { - "type": "etherscan", + "type": "oklink", "types": [ "tx", "address" ], - "url": "https://snowtrace.io/{type}/{value}" + "url": "https://www.okx.com/web3/explorer/avax/{type}/{value}?channelID=flessw" } ] }, @@ -6814,48 +7064,116 @@ { "disabled": false, "chainId": "195", - "rank": 18, - "name": "X1 testnet", + "rank": 130, + "name": "X Layer Testnet", "externalApi": { - "history": { - "type": "oklink", - "url": "https://www.oklink.com/api/v5/explorer/address/transaction-list?chainShortName=X1_TEST" - }, - "explorers": [ - { - "type": "oklink", - "types": [ - "tx", - "address" - ], - "url": "https://www.oklink.com/x1-test/{type}/{value}" - } - ] + "history": { + "type": "oklink", + "url": "https://www.oklink.com/api/v5/explorer/address/transaction-list?chainShortName=XLAYER_TESTNET" }, - "assets": [ - { - "isUtility": true, - "id": "0db45dec-9e88-4044-a358-3888f85e9a9e", + "explorers": [ + { + "type": "oklink", + "types": [ + "tx", + "address" + ], + "url": "https://www.okx.com/explorer/xlayer-test/{type}/{value}" + } + ] + }, + "assets": [ + { + "isUtility": true, + "id": "a61b1842-30c0-431d-9bbc-fc3370562455", + "name": "okb", + "symbol": "okb", + "precision": 18, + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/XLOKB.svg", + "color": "FFFFFF", + "type": "normal", + "ethereumType": "normal" + } + ], + "nodes": [ + { + "url": "https://testrpc.xlayer.tech/", + "name": "X Layer Test Tech https node" + }, + { + "url": "wss://testws.xlayer.tech", + "name": "X Layer Test Tech wss node" + }, + { + "url": "https://xlayertestrpc.okx.com/", + "name": "X Layer Test OKX https node" + }, + { + "url": "wss://xlayertestws.okx.com", + "name": "X Layer Test OKX wss node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/xlayerchain.svg", + "addressPrefix": 0, + "options": [ + "ethereum", + "utilityFeePayment" + ] + }, + { + "disabled": false, + "chainId": "196", + "rank": 13, + "name": "X Layer Mainnet", + "externalApi": { + "history": { + "type": "oklink", + "url": "https://www.oklink.com/api/v5/explorer/address/transaction-list?chainShortName=XLAYER" + }, + "explorers": [ + { + "type": "oklink", + "types": [ + "tx", + "address" + ], + "url": "https://www.okx.com/web3/explorer/xlayer-test/{type}/{value}" + } + ] + }, + "assets": [ + { + "isUtility": true, + "id": "dcf40e8d-f041-45b8-8cc8-e5b95bedb86e", "name": "okb", "symbol": "okb", "precision": 18, - "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/OKTC.svg", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/XLOKB.svg", "color": "FFFFFF", + "priceId": "okb", "type": "normal", "ethereumType": "normal" } ], "nodes": [ { - "url": "https://testrpc.x1.tech/", - "name": "X1 https node" + "url": "https://rpc.xlayer.tech/", + "name": "X Layer Tech https node" }, { - "url": "https://x1testrpc.okx.com/", - "name": "X1 https node" + "url": "wss://ws.xlayer.tech/", + "name": "X Layer Tech wss node" + }, + { + "url": "https://xlayerrpc.okx.com/", + "name": "X Layer OKX https node" + }, + { + "url": "wss://xlayerws.okx.com/", + "name": "X Layer OKX wss node" } ], - "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/Okxchain.svg", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/xlayerchain.svg", "addressPrefix": 0, "options": [ "ethereum", @@ -6865,24 +7183,24 @@ { "disabled": false, "chainId": "1101", - "rank": 20, + "rank": 7, "name": "Polygon zkEVM", "externalApi": { "history": { - "type": "etherscan", - "url": "https://api-zkevm.polygonscan.com/api" + "type": "oklink", + "url": "https://www.oklink.com/api/v5/explorer/address/transaction-list?chainShortName=POLYGON_ZKEVM" + }, + "explorers": [ + { + "type": "oklink", + "types": [ + "tx", + "address" + ], + "url": "https://www.okx.com/web3/explorer/polygon-zkevm/{type}/{value}?channelID=flessw" + } + ] }, - "explorers": [ - { - "type": "etherscan", - "types": [ - "tx", - "address" - ], - "url": "https://zkevm.polygonscan.com/{type}/{value}" - } - ] - }, "assets": [ { "isUtility": true, @@ -6941,7 +7259,7 @@ { "disabled": false, "chainId": "7001", - "rank": 17, + "rank": 14, "name": "ZetaChain Testnet", "assets": [ { @@ -6954,26 +7272,14 @@ "color": "235643", "type": "normal", "ethereumType": "normal" - }, - { - "isUtility": false, - "id": "0x48f80608B672DC30DC7e3dbBd0343c5F02C738Eb", - "name": "MATIC-mumbai_testnet", - "symbol": "matic", - "precision": 18, - "priceId": "tMATIC", - "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/MATIC.svg", - "color": "8247E5", - "type": "normal", - "ethereumType": "erc20" } ], "externalApi": { "history": { - "type": "zeta", - "url": "https://zetachain-athens-3.blockscout.com/api/v2/addresses/" + "type": "zeta", + "url": "https://zetachain-athens-3.blockscout.com/api/v2/addresses/" } - }, + }, "nodes": [ { "url": "https://rpc.ankr.com/zetachain_evm_athens_testnet", @@ -7023,6 +7329,7 @@ "explorers": [{ "type": "reef", "types": [ + "transfer", "extrinsic", "account" ], @@ -7035,7 +7342,864 @@ } ], - "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/white/reefchain.svg", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/reefchain.svg", + "addressPrefix": 42 + }, + { + "disabled": false, + "chainId": "f3c7ad88f6a80f366c4be216691411ef0622e8b809b1046ea297ef106058d4eb", + "name": "Manta Parachain", + "assets": [{ + "id": "0c41f3da-3c42-413a-aca3-bfb19b717df7", + "name": "manta", + "symbol": "manta", + "precision": 18, + "priceId": "manta-network", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/MANTA.svg", + "color": "29CCB9", + "isUtility": true, + "type": "normal" + }], + "externalApi": { + "explorers": [{ + "type": "subscan", + "types": [ + "extrinsic", + "account" + ], + "url": "https://manta.subscan.io/{type}/{value}" + }] + }, + "nodes": [{ + "url": "wss://ws.manta.systems", + "name": "Manta Community node" + } + + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/mantachain.svg", + "addressPrefix": 77 + }, + { + "disabled": false, + "chainId": "169", + "name": "Manta Pacific Mainnet", + "assets": [ + { + "isUtility": true, + "id": "af44954d-2be1-4021-ae0b-f05d8180400a", + "name": "ethereum", + "symbol": "eth", + "precision": 18, + "priceId": "ethereum", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/ETH.svg", + "color":"627EEA", + "type": "normal", + "ethereumType": "normal" + }, + { + "isUtility": false, + "id": "0x95CeF13441Be50d20cA4558CC0a27B601aC544E5", + "name": "manta", + "symbol": "manta", + "precision": 18, + "priceId": "manta-network", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/MANTA.svg", + "color": "8247E5", + "type": "normal", + "ethereumType": "erc20" + } + ], + "externalApi": { + "history": { + "type": "zeta", + "url": "https://pacific-explorer.manta.network/api/v2/addresses/" + } + }, + "nodes": [ + { + "url": "https://pacific-rpc.manta.network/http", + "name": "Manta https node" + }, + { + "url": "https://1rpc.io/manta", + "name": "1RPC https node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/mantachain.svg", + "addressPrefix": 0, + "options": [ + "ethereum", + "utilityFeePayment" + ] + }, + { + "disabled": true, + "chainId": "d8761d3c88f26dc12875c00d3165f7d67243d56fc85b4cf19937601a7916e5a9", + "name": "Enjin Relaychain", + "externalApi": { + "explorers": [{ + "type": "subscan", + "types": [ + "extrinsic", + "account" + ], + "url": "https://enjin.subscan.io/{type}/{value}" + }] + }, + "assets": [{ + "id": "43748d94-90ba-41b2-8732-326cd943a501", + "name": "enjin coin", + "symbol": "enj", + "precision": 18, + "priceId": "enjincoin", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/ENJ.svg", + "color": "5A27ED", + "isUtility": true, + "type": "normal" + }], + "nodes": [{ + "url": "wss://rpc.relay.blockchain.enjin.io", + "name": "Enjin Foundation node" + } + + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/Enjin.svg", + "addressPrefix": 2135 + }, + { + "disabled": false, + "chainId": "6bd89e052d67a45bb60a9a23e8581053d5e0d619f15cb9865946937e690c42d6", + "name": "Liberland", + "assets": [ + { + "id": "a6b83d39-a488-4b34-8352-280705a792ea", + "name": "liberland lld", + "symbol": "lld", + "precision": 12, + "priceId": "liberland-lld", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/LLD.svg", + "color": "00437F", + "isUtility": true, + "type": "normal" + }, + { + "id": "30b43eb9-36b4-4b40-bf72-330d2e20ee86", + "name": "liberland merit", + "symbol": "llm", + "precision": 12, + "currencyId": "1", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/LLM.svg", + "color": "EFB900", + "type": "assets" + }, + { + "id": "2e7179c9-4308-420e-a654-43c92d119717", + "name": "sora xor", + "symbol": "xor", + "precision": 12, + "currencyId": "774441749", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/XOR.svg", + "color": "EE2233", + "type": "assets" + } + ], + "xcm": { + "xcmVersion": "v3", + "availableAssets": [ + { + "id": "a6b83d39-a488-4b34-8352-280705a792e", + "symbol": "LLD" + }, + { + "id": "30b43eb9-36b4-4b40-bf72-330d2e20ee86", + "symbol": "LLM" + }, + { + "id": "2e7179c9-4308-420e-a654-43c92d119717", + "symbol": "XOR" + } + ], + "availableDestinations": [ + { + "chainId": "7e4e32d0feafd4f9c9414b0be86373f9a1efa904809b683453a9af6856d38ad5", + "assets": [ + { + "id": "a6b83d39-a488-4b34-8352-280705a792e", + "symbol": "LLD" + }, + { + "id": "30b43eb9-36b4-4b40-bf72-330d2e20ee86", + "symbol": "LLM" + }, + { + "id": "2e7179c9-4308-420e-a654-43c92d119717", + "symbol": "XOR" + } + + ] + } + ] + }, + "nodes": [ + { + "url": "wss://mainnet.liberland.org", + "name": "Liberland node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/liberland.svg", "addressPrefix": 42 - } + }, + { + "disabled": false, + "chainId": "248", + "rank": 15, + "name": "Oasys Mainnet", + "externalApi": { + "history": { + "type": "etherscan", + "url": "https://explorer.oasys.games/api" + }, + "explorers": [ + { + "type": "etherscan", + "types": [ + "tx", + "address" + ], + "url": "https://explorer.oasys.games/{type}/{value}" + } + ] + }, + "assets": [ + { + "isUtility": true, + "id": "11d2ece1-ecae-4596-aae4-ee9db83a5e2a", + "name": "oasys", + "symbol": "oas", + "precision": 18, + "priceId": "oasys", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/OAS.svg", + "color": "00A84F", + "type": "normal", + "ethereumType": "normal" + } + ], + "nodes": [ + { + "url": "https://oasys.blockpi.network/v1/rpc/public/", + "name": "Blockpi https node" + }, + { + "url": "https://rpc.mainnet.oasys.games/", + "name": "Oasys https node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/oasys.svg", + "addressPrefix": 0, + "options": [ + "ethereum", + "utilityFeePayment" + ] + }, + { + "disabled": false, + "chainId": "29548", + "rank": 150, + "name": "MCH Verse Mainnet", + "externalApi": { + "history": { + "type": "etherscan", + "url": "https://explorer.oasys.mycryptoheroes.net/api" + }, + "explorers": [ + { + "type": "etherscan", + "types": [ + "tx", + "address" + ], + "url": "https://explorer.oasys.mycryptoheroes.net/{type}/{value}" + } + ] + }, + "assets": [ + { + "isUtility": true, + "id": "dad38228-7e66-46cb-b8f8-bee5a738a18a", + "name": "oasys", + "symbol": "oas", + "precision": 18, + "priceId": "oasys", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/OAS.svg", + "color": "00A84F", + "type": "normal", + "ethereumType": "normal" + } + ], + "nodes": [ + { + "url": "https://rpc.oasys.mycryptoheroes.net/", + "name": "MCH https node" + }, + { + "url": "wss://ws.oasys.mycryptoheroes.net/", + "name": "MCH wss node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/mchverse.svg", + "addressPrefix": 0, + "options": [ + "ethereum", + "utilityFeePayment" + ] + }, + { + "disabled": false, + "chainId": "2400", + "rank": 151, + "name": "TCG Verse Mainnet", + "externalApi": { + "history": { + "type": "etherscan", + "url": "https://explorer.tcgverse.xyz/api" + }, + "explorers": [ + { + "type": "etherscan", + "types": [ + "tx", + "address" + ], + "url": "https://explorer.tcgverse.xyz/{type}/{value}" + } + ] + }, + "assets": [ + { + "isUtility": true, + "id": "e2661f7b-3916-464d-8ea2-5650b6c7de94", + "name": "oasys", + "symbol": "oas", + "precision": 18, + "priceId": "oasys", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/OAS.svg", + "color": "00A84F", + "type": "normal", + "ethereumType": "normal" + } + ], + "nodes": [ + { + "url": "https://rpc.tcgverse.xyz/", + "name": "TCG https node" + }, + { + "url": "wss://ws-rpc.tcgverse.xyz/", + "name": "TCG wss node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/tcgverse.svg", + "addressPrefix": 0, + "options": [ + "ethereum", + "utilityFeePayment" + ] + }, + { + "disabled": false, + "chainId": "19011", + "rank": 152, + "name": "HOME Verse Mainnet", + "externalApi": { + "history": { + "type": "etherscan", + "url": "https://explorer.oasys.homeverse.games/api" + }, + "explorers": [ + { + "type": "etherscan", + "types": [ + "tx", + "address" + ], + "url": "https://explorer.oasys.homeverse.games/{type}/{value}" + } + ] + }, + "assets": [ + { + "isUtility": true, + "id": "943d4fe9-76e5-48bd-9220-4fcda4e3b8e4", + "name": "oasys", + "symbol": "oas", + "precision": 18, + "priceId": "oasys", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/OAS.svg", + "color": "00A84F", + "type": "normal", + "ethereumType": "normal" + } + ], + "nodes": [ + { + "url": "https://rpc.mainnet.oasys.homeverse.games/", + "name": "HOME https node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/homeverse.svg", + "addressPrefix": 0, + "options": [ + "ethereum", + "utilityFeePayment" + ] + }, + { + "disabled": false, + "chainId": "5555", + "rank": 153, + "name": "Chain Verse Mainnet", + "externalApi": { + "history": { + "type": "etherscan", + "url": "https://explorer.chainverse.info/api" + }, + "explorers": [ + { + "type": "etherscan", + "types": [ + "tx", + "address" + ], + "url": "https://explorer.chainverse.info/{type}/{value}" + } + ] + }, + "assets": [ + { + "isUtility": true, + "id": "f1eeb4f0-d87d-41ef-8e23-af5a535b793c", + "name": "oasys", + "symbol": "oas", + "precision": 18, + "priceId": "oasys", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/OAS.svg", + "color": "00A84F", + "type": "normal", + "ethereumType": "normal" + } + ], + "nodes": [ + { + "url": "https://rpc.chainverse.info/", + "name": "Chain https node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/chainverse.svg", + "addressPrefix": 0, + "options": [ + "ethereum", + "utilityFeePayment" + ] + }, + { + "disabled": false, + "chainId": "7225878", + "rank": 154, + "name": "Saakuru Verse Mainnet", + "externalApi": { + "history": { + "type": "etherscan", + "url": "https://explorer.saakuru.network/api" + }, + "explorers": [ + { + "type": "etherscan", + "types": [ + "tx", + "address" + ], + "url": "https://explorer.saakuru.network/{type}/{value}" + } + ] + }, + "assets": [ + { + "isUtility": true, + "id": "5e15fa7a-b398-49a6-8459-da4b4f660f32", + "name": "oasys", + "symbol": "oas", + "precision": 18, + "priceId": "oasys", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/OAS.svg", + "color": "00A84F", + "type": "normal", + "ethereumType": "normal" + } + ], + "nodes": [ + { + "url": "https://rpc.saakuru.network/", + "name": "Saakuru https node" + }, + { + "url": "wss://ws.saakuru.network/", + "name": "Saakuru wss node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/saakuruverse.svg", + "addressPrefix": 0, + "options": [ + "ethereum", + "utilityFeePayment" + ] + }, + { + "disabled": false, + "chainId": "50005", + "rank": 155, + "name": "Yooldo Verse Mainnet", + "externalApi": { + "history": { + "type": "etherscan", + "url": "https://explorer.yooldo-verse.xyz/api" + }, + "explorers": [ + { + "type": "etherscan", + "types": [ + "tx", + "address" + ], + "url": "https://explorer.yooldo-verse.xyz/{type}/{value}" + } + ] + }, + "assets": [ + { + "isUtility": true, + "id": "648d069c-c32f-4fbc-af23-14f77a9158aa", + "name": "oasys", + "symbol": "oas", + "precision": 18, + "priceId": "oasys", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/OAS.svg", + "color": "00A84F", + "type": "normal", + "ethereumType": "normal" + } + ], + "nodes": [ + { + "url": "https://rpc.yooldo-verse.xyz/", + "name": "Yooldo https node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/yooldoverse.svg", + "addressPrefix": 0, + "options": [ + "ethereum", + "utilityFeePayment" + ] + }, + { + "disabled": false, + "chainId": "30", + "rank": 16, + "name": "Rootstock Mainnet", + "externalApi": { + "history": { + "type": "zeta", + "url": "https://rootstock.blockscout.com//api/v2/addresses/" + } + }, + "assets": [ + { + "isUtility": true, + "id": "defb1fb8-8cf1-49b1-8dd3-b8dac33394a4", + "name": "rootstock rsk rbtc", + "symbol": "rbtc", + "precision": 18, + "priceId": "rootstock", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/RBTC.svg", + "color": "", + "type": "normal", + "ethereumType": "normal" + } + ], + "nodes": [ + { + "url": "https://public-node.rsk.co/", + "name": "Public https node" + }, + { + "url": "https://mycrypto.rsk.co/", + "name": "Mycrypto https node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/rootstock.svg", + "addressPrefix": 0, + "options": [ + "ethereum", + "utilityFeePayment" + ] + }, + { + "disabled": false, + "chainId": "50dd5d206917bf10502c68fb4d18a59fc8aa31586f4e8856b493e43544aa82aa", + "name": "XX network", + "externalApi": { + "history": { + "type": "subquery", + "url": "https://api.subquery.network/sq/nova-wallet/nova-wallet-xx-network" + } + }, + "assets": [{ + "id": "526dca29-63ff-4683-88d6-852d1455b17b", + "name": "xx network", + "symbol": "xx", + "precision": 9, + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/XX.svg", + "priceId": "xxcoin", + "color": "0AC1C7", + "isUtility": true, + "type": "normal" + }], + "nodes": [ + { + "url": "wss://xxnetwork-rpc.dwellir.com", + "name": "Dwellir node" + }, + { + "url": "wss://rpc.xx.network", + "name": "xx Foundation node #1" + }, + { + "url": "wss://rpc-hetzner.xx.network", + "name": "xx Foundation node #2" + }, + { + "url": "wss://rpc-do.xx.network", + "name": "xx Foundation node #3" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/xxnetwork.svg", + "addressPrefix": 55 + }, + { + "disabled": false, + "chainId": "6660", + "name": "Latest Testnet", + "externalApi": { + "history": { + "type": "zeta", + "url": "https://testscan.latestchain.io/api/v2/addresses/" + } + }, + "assets": [ + { + "isUtility": true, + "id": "1a31227d-c6fe-4c78-a3d6-353be70e56a6", + "name": "latest", + "symbol": "latest", + "precision": 18, + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/LATEST.svg", + "color": "FC81EA", + "type": "normal", + "ethereumType": "normal" + } + + ], + "nodes": [ + { + "url": "https://testnet-rpc.latestchain.io", + "name": "Latest https node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/latestchain.svg", + "addressPrefix": 0, + "options": [ + "testnet", + "ethereum", + "utilityFeePayment" + ] + }, + { + "disabled": false, + "chainId": "9630", + "name": "Latest Mainnet", + "externalApi": { + "history": { + "type": "zeta", + "url": "https://scan.latestchain.io/api/v2/addresses/" + } + }, + "assets": [ + { + "isUtility": true, + "id": "e7094e62-d86d-4c08-8497-9ebc4c9011ea", + "name": "latest", + "symbol": "latest", + "precision": 18, + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/LATEST.svg", + "color": "FC81EA", + "type": "normal", + "ethereumType": "normal" + } + + ], + "nodes": [ + { + "url": "https://mainnet-rpc.latestchain.io", + "name": "Latest https node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/latest.svg", + "addressPrefix": 0, + "options": [ + "ethereum", + "utilityFeePayment" + ] + }, + { + "disabled": false, + "chainId": "72778", + "name": "CAGA Ankara Testnet", + "externalApi": { + "history": { + "type": "etherscan", + "url": "https://explorer.ankara-cagacrypto.com/api" + } + }, + "assets": [ + { + "isUtility": true, + "id": "a9270ef5-379a-4228-8d72-e6efb2b5f7b4", + "name": "caga", + "symbol": "caga", + "precision": 18, + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/CAGA.svg", + "color": "FFFFFF", + "type": "normal", + "ethereumType": "normal" + } + + ], + "nodes": [ + { + "url": "wss://wss.ankara-cagacrypto.com", + "name": "Caga wss node" + }, + { + "url": "https://www.ankara-cagacrypto.com", + "name": "Caga https node" + } + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/cagachain.svg", + "addressPrefix": 0, + "options": [ + "testnet", + "ethereum", + "utilityFeePayment" + ] + }, + { + "disabled": false, + "chainId": "6f09966420b2608d1947ccfb0f2a362450d1fc7fd902c29b67c906eaa965a7ae", + "name": "Avail Goldberg Testnet", + "externalApi": { + "explorers": [{ + "type": "subscan", + "types": [ + "extrinsic", + "account" + ], + "url": "https://avail-testnet.subscan.io/{type}/{value}" + }] + }, + "assets": [ + { + "id": "72778e65-53b6-4cb4-bb4c-c029121eb494", + "name": "avail token", + "symbol": "avl", + "precision": 18, + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/AVL.svg", + "color": "56E5FF", + "isUtility": true, + "type": "normal" + } + ], + "nodes": [{ + "url": "wss://goldberg-testnet-rpc.avail.tools/ws", + "name": "Avail Goldberg Testnet node" + } + + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/Avail.svg", + "addressPrefix": 42, + "options": [ + "checkAppId" + ] + }, + { + "disabled": false, + "chainId": "0614f7b74a2e47f7c8d8e2a5335be84bdde9402a43f5decdec03200a87c8b943", + "name": "Analog Testnet", + "assets": [ + { + "id": "9a8799db-a479-4a73-ae81-3b637c8624d8", + "name": "tanlog", + "symbol": "tanlog", + "precision": 12, + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/TANLOG.svg", + "color": "9A74F7", + "isUtility": true, + "type": "normal" + } + ], + "nodes": [{ + "url": "wss://rpc.testnet.analog.one", + "name": "Analog Testnet node" + } + + ], + "options": [ + "testnet" + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/Analog.svg", + "addressPrefix": 12850 + }, + { + "disabled": false, + "chainId": "c1af4cb4eb3918e5db15086c0cc5ec17fb334f728b7c65dd44bfe1e174ff8b3f", + "parentId": "b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe", + "name": "Kusama People", + "assets": [ + { + "id": "b54f9075-6364-4ad9-8ac2-ed8a604491fd", + "name": "kusama", + "symbol": "ksm", + "precision": 12, + "priceId": "kusama", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/KSM.svg", + "color": "FFFFFF", + "isUtility": true, + "type": "normal" + } + ], + "nodes": [ + { + "url": "wss://kusama-people-rpc.polkadot.io", + "name": "Parity node" + }, + { + "url": "wss://ksm-rpc.stakeworld.io/people", + "name": "Stakeworld node" + } + ], + "options": [ + "identityChain" + ], + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/chains/white/People.svg", + "addressPrefix": 2 + } ] From a9086f3e8cb24750acace784223c00b0e1402aae Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Fri, 31 May 2024 15:03:32 +0500 Subject: [PATCH 012/115] FLW-4635 New bridge route SORA <-> Liberland --- .../xcshareddata/swiftpm/Package.resolved | 303 ++++++++++++++++++ fearless/Common/Model/Chain.swift | 6 +- .../Protocols/BaseErrorPresentable.swift | 10 +- .../CrossChain/CrossChainPresenter.swift | 3 +- .../SendDataValidatingFactory.swift | 37 ++- fearless/en.lproj/Localizable.strings | 1 + fearless/id.lproj/Localizable.strings | 1 + fearless/ja.lproj/Localizable.strings | 1 + fearless/pt-PT.lproj/Localizable.strings | 1 + fearless/ru.lproj/Localizable.strings | 1 + fearless/tr.lproj/Localizable.strings | 1 + fearless/vi.lproj/Localizable.strings | 1 + fearless/zh-Hans.lproj/Localizable.strings | 1 + 13 files changed, 356 insertions(+), 11 deletions(-) create mode 100644 fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000000..009139898b --- /dev/null +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,303 @@ +{ + "originHash" : "48bf9205e83b67ed7d81b2a0bcc8ef081189084205112c031b7823572f561e20", + "pins" : [ + { + "identity" : "appauth-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/openid/AppAuth-iOS.git", + "state" : { + "revision" : "c89ed571ae140f8eb1142735e6e23d7bb8c34cb2", + "version" : "1.7.5" + } + }, + { + "identity" : "bigint", + "kind" : "remoteSourceControl", + "location" : "https://github.com/attaswift/BigInt.git", + "state" : { + "revision" : "0ed110f7555c34ff468e72e1686e59721f2b0da6", + "version" : "5.3.0" + } + }, + { + "identity" : "cryptoswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", + "state" : { + "revision" : "c9c3df6ab812de32bae61fc0cd1bf6d45170ebf0", + "version" : "1.8.2" + } + }, + { + "identity" : "fearless-starscream", + "kind" : "remoteSourceControl", + "location" : "https://github.com/soramitsu/fearless-starscream", + "state" : { + "revision" : "3e1de9baeab87de379e0cb01c64d5db18fbf130f", + "version" : "4.0.12" + } + }, + { + "identity" : "google-api-objectivec-client-for-rest", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/google-api-objectivec-client-for-rest.git", + "state" : { + "revision" : "d46fb27cf61e08285a727c18a2ae0dbc20d91b2f", + "version" : "3.5.4" + } + }, + { + "identity" : "googlesignin-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleSignIn-iOS", + "state" : { + "revision" : "a7965d134c5d3567026c523e0a8a583f73b62b0d", + "version" : "7.1.0" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "0382ca27f22fb3494cf657d8dc356dc282cd1193", + "version" : "3.4.1" + } + }, + { + "identity" : "gtmappauth", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GTMAppAuth.git", + "state" : { + "revision" : "5d7d66f647400952b1758b230e019b07c0b4b22a", + "version" : "4.1.1" + } + }, + { + "identity" : "nimble", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Quick/Nimble", + "state" : { + "revision" : "e9d769113660769a4d9dd3afb855562c0b7ae7b0", + "version" : "7.3.4" + } + }, + { + "identity" : "promisekit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mxcl/PromiseKit.git", + "state" : { + "revision" : "8a98e31a47854d3180882c8068cc4d9381bf382d", + "version" : "6.22.1" + } + }, + { + "identity" : "qrcode", + "kind" : "remoteSourceControl", + "location" : "https://github.com/WalletConnect/QRCode", + "state" : { + "revision" : "263f280d2c8144adfb0b6676109846cfc8dd552b", + "version" : "14.3.1" + } + }, + { + "identity" : "quick", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Quick/Quick", + "state" : { + "revision" : "f2b5a06440ea87eba1a167cab37bf6496646c52e", + "version" : "1.3.4" + } + }, + { + "identity" : "reachability.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ashleymills/Reachability.swift", + "state" : { + "revision" : "7cbd73f46a7dfaeca079e18df7324c6de6d1834a", + "version" : "5.2.3" + } + }, + { + "identity" : "secp256k1.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Boilertalk/secp256k1.swift.git", + "state" : { + "revision" : "cd187c632fb812fd93711a9f7e644adb7e5f97f0", + "version" : "0.1.7" + } + }, + { + "identity" : "shared-features-spm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/soramitsu/shared-features-spm.git", + "state" : { + "branch" : "feature/xcm-routes", + "revision" : "8f5bd406c0cb8dad1c058f8de1ed5f1159a22078" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "cd142fd2f64be2100422d658e7411e39489da985", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types", + "state" : { + "revision" : "9bee2fdb79cc740081abd8ebd80738063d632286", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "359c461e5561d22c6334828806cc25d759ca7aa6", + "version" : "2.65.0" + } + }, + { + "identity" : "swift-nio-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-extras.git", + "state" : { + "revision" : "a3b640d7dc567225db7c94386a6e71aded1bfa63", + "version" : "1.22.0" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2.git", + "state" : { + "revision" : "c6afe04165c865faaa687b42c32ed76dfcc91076", + "version" : "1.31.0" + } + }, + { + "identity" : "swift-nio-ssl", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-ssl.git", + "state" : { + "revision" : "7c381eb6083542b124a6c18fae742f55001dc2b5", + "version" : "2.26.0" + } + }, + { + "identity" : "swift-nio-transport-services", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-transport-services.git", + "state" : { + "revision" : "38ac8221dd20674682148d6451367f89c2652980", + "version" : "1.21.0" + } + }, + { + "identity" : "swift-qrcode-generator", + "kind" : "remoteSourceControl", + "location" : "https://github.com/dagronf/swift-qrcode-generator", + "state" : { + "revision" : "5ca09b6a2ad190f94aa3d6ddef45b187f8c0343b", + "version" : "1.0.3" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "f9266c85189c2751589a50ea5aec72799797e471", + "version" : "1.3.0" + } + }, + { + "identity" : "swiftimagereadwrite", + "kind" : "remoteSourceControl", + "location" : "https://github.com/dagronf/SwiftImageReadWrite", + "state" : { + "revision" : "5596407d1cf61b953b8e658fa8636a471df3c509", + "version" : "1.1.6" + } + }, + { + "identity" : "swiftybeaver", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SwiftyBeaver/SwiftyBeaver.git", + "state" : { + "revision" : "8cba041db09596183331d123f337d0eb2e6e8e91", + "version" : "2.1.1" + } + }, + { + "identity" : "swime", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sendyhalim/Swime", + "state" : { + "revision" : "4e538834483059ceefaaad8cdb3abe0d7d1c5146", + "version" : "3.1.0" + } + }, + { + "identity" : "tweetnacl-swiftwrap", + "kind" : "remoteSourceControl", + "location" : "https://github.com/bitmark-inc/tweetnacl-swiftwrap", + "state" : { + "revision" : "f8fd111642bf2336b11ef9ea828510693106e954", + "version" : "1.1.0" + } + }, + { + "identity" : "walletconnectswiftv2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/WalletConnect/WalletConnectSwiftV2", + "state" : { + "revision" : "58d2b49eeac5cf94432e2647b9107577c156a25c", + "version" : "1.9.9" + } + }, + { + "identity" : "web3.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/bnsports/Web3.swift.git", + "state" : { + "revision" : "a526779488e5fe2fa993d9614f11f57b00cc1858", + "version" : "7.7.7" + } + }, + { + "identity" : "websocket-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/websocket-kit", + "state" : { + "revision" : "4232d34efa49f633ba61afde365d3896fc7f8740", + "version" : "2.15.0" + } + }, + { + "identity" : "xxhash-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/daisuke-t-jp/xxHash-Swift", + "state" : { + "revision" : "e86a07ab4867f81481d430e1370a5ec97b6e3359", + "version" : "1.1.1" + } + } + ], + "version" : 3 +} diff --git a/fearless/Common/Model/Chain.swift b/fearless/Common/Model/Chain.swift index d97d5c7f37..0017d690a7 100644 --- a/fearless/Common/Model/Chain.swift +++ b/fearless/Common/Model/Chain.swift @@ -15,6 +15,7 @@ enum Chain: String, Codable, CaseIterable { case equilibrium = "Equilibrium" case reef = "Reef Mainnet" case scuba = "Reef Scuba Testnet" + case liberland = "Liberland" init?(rawValue: String) { switch rawValue { @@ -30,6 +31,7 @@ enum Chain: String, Codable, CaseIterable { case Self.equilibrium.rawValue: self = .equilibrium case Self.reef.rawValue: self = .reef case Self.scuba.rawValue: self = .scuba + case Self.liberland.rawValue: self = .liberland #if F_DEV case "Polkatrain": self = .polkadot @@ -53,6 +55,7 @@ enum Chain: String, Codable, CaseIterable { case Self.equilibrium.genesisHash: self = .equilibrium case Self.reef.genesisHash: self = .reef case Self.scuba.genesisHash: self = .scuba + case Self.liberland.genesisHash: self = .liberland default: return nil } } @@ -72,13 +75,14 @@ enum Chain: String, Codable, CaseIterable { case .equilibrium: return "89d3ec46d2fb43ef5a9713833373d5ea666b092fa8fd68fbc34596036571b907" case .reef: return "7834781d38e4798d548e34ec947d19deea29df148a7bf32484b7b24dacf8d4b7" case .scuba: return "b414a8602b2251fa538d38a9322391500bd0324bc7ac6048845d57c37dd83fe6" + case .liberland: return "6bd89e052d67a45bb60a9a23e8581053d5e0d619f15cb9865946937e690c42d6" } } var erasPerDay: Int { switch self { case .polkadot, .ternoa, .equilibrium, .reef, .scuba: return 1 - case .kusama, .westend, .rococo, .moonbeam, .soraMain, .soraTest: return 4 + case .kusama, .westend, .rococo, .moonbeam, .soraMain, .soraTest, .liberland: return 4 case .moonriver, .moonbaseAlpha: return 12 } } diff --git a/fearless/Common/Protocols/BaseErrorPresentable.swift b/fearless/Common/Protocols/BaseErrorPresentable.swift index edebe67081..cb5758a175 100644 --- a/fearless/Common/Protocols/BaseErrorPresentable.swift +++ b/fearless/Common/Protocols/BaseErrorPresentable.swift @@ -31,7 +31,8 @@ protocol BaseErrorPresentable { func presentSoraBridgeLowAmountError( from view: ControllerBackedProtocol, originChainId: ChainModel.Id, - locale: Locale + locale: Locale, + assetAmount: String ) func presentWarning( for title: String, @@ -225,17 +226,16 @@ extension BaseErrorPresentable where Self: SheetAlertPresentable & ErrorPresenta func presentSoraBridgeLowAmountError( from view: ControllerBackedProtocol, originChainId: ChainModel.Id, - locale: Locale + locale: Locale, + assetAmount: String ) { let originKnownChain = Chain(chainId: originChainId) let message: String? switch originKnownChain { case .kusama: message = R.string.localizable.soraBridgeLowAmountAlert(preferredLanguages: locale.rLanguages) - case .polkadot, .soraMain: - message = R.string.localizable.soraBridgeLowAmauntPolkadotAlert(preferredLanguages: locale.rLanguages) default: - message = nil + message = R.string.localizable.xcmLowAmauntAssetSymbolAlert(assetAmount, preferredLanguages: locale.rLanguages) } let title = R.string.localizable.commonAttention(preferredLanguages: locale.rLanguages) diff --git a/fearless/Modules/CrossChain/CrossChainPresenter.swift b/fearless/Modules/CrossChain/CrossChainPresenter.swift index 8462d11272..32c452c3ba 100644 --- a/fearless/Modules/CrossChain/CrossChainPresenter.swift +++ b/fearless/Modules/CrossChain/CrossChainPresenter.swift @@ -359,7 +359,8 @@ final class CrossChainPresenter { originCHainId: selectedOriginChainModel.chainId, destChainId: selectedDestChainModel?.chainId, amount: inputAmountDecimal, - locale: selectedLocale + locale: selectedLocale, + asset: selectedAmountChainAsset.asset ) let soraBridgeAmountLessFeeViolated = dataValidatingFactory.soraBridgeAmountLessFeeViolated( diff --git a/fearless/Modules/Send/Validators/SendDataValidatingFactory.swift b/fearless/Modules/Send/Validators/SendDataValidatingFactory.swift index 3e92354b5b..295b984666 100644 --- a/fearless/Modules/Send/Validators/SendDataValidatingFactory.swift +++ b/fearless/Modules/Send/Validators/SendDataValidatingFactory.swift @@ -231,17 +231,21 @@ class SendDataValidatingFactory: NSObject { originCHainId: ChainModel.Id, destChainId: ChainModel.Id?, amount: Decimal, - locale: Locale + locale: Locale, + asset: AssetModel ) -> DataValidating { ErrorConditionViolation(onError: { [weak self] in - guard let view = self?.view else { + guard let self, let view = self.view, let destChainId else { return } - self?.basePresentable.presentSoraBridgeLowAmountError( + let assetAmount = self.minAssetAmount(originCHainId: originCHainId, destChainId: destChainId) + + self.basePresentable.presentSoraBridgeLowAmountError( from: view, originChainId: originCHainId, - locale: locale + locale: locale, + assetAmount: assetAmount ) }, preservesCondition: { guard let destChainId = destChainId else { @@ -255,6 +259,10 @@ class SendDataValidatingFactory: NSObject { return amount >= 0.05 case (.polkadot, .soraMain), (.soraMain, .polkadot): return amount >= 1.1 + case (.liberland, .soraMain): + return amount >= 1.0 && asset.symbol.lowercased() == "lld" + case (.soraMain, .liberland): + return amount >= 1.0 && asset.symbol.lowercased() == "lld" default: return true } @@ -299,4 +307,25 @@ class SendDataValidatingFactory: NSObject { } } } + + private func minAssetAmount( + originCHainId: ChainModel.Id, + destChainId: ChainModel.Id + ) -> String { + let originKnownChain = Chain(chainId: originCHainId) + let destKnownChain = Chain(chainId: destChainId) + + switch (originKnownChain, destKnownChain) { + case (.kusama, .soraMain): + return "0.05 KSM" + case (.polkadot, .soraMain), (.soraMain, .polkadot): + return "1.1 DOT" + case (.liberland, .soraMain): + return "1.0 LLD" + case (.soraMain, .liberland): + return "1.0 LLD" + default: + return "" + } + } } diff --git a/fearless/en.lproj/Localizable.strings b/fearless/en.lproj/Localizable.strings index 9683143aab..2948bdd748 100644 --- a/fearless/en.lproj/Localizable.strings +++ b/fearless/en.lproj/Localizable.strings @@ -1271,3 +1271,4 @@ To find out more, contact %@"; "wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%s) on the destination account will be less than the minimal balance (%s). Please increase the amount by %s"; "nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; "wallet.all.assets.hidden" = "You have hidden all assets"; +"xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; diff --git a/fearless/id.lproj/Localizable.strings b/fearless/id.lproj/Localizable.strings index 800bd0f9b7..8b24f35b52 100644 --- a/fearless/id.lproj/Localizable.strings +++ b/fearless/id.lproj/Localizable.strings @@ -1255,3 +1255,4 @@ To find out more, contact %@"; "wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%s) on the destination account will be less than the minimal balance (%s). Please increase the amount by %s"; "nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; "wallet.all.assets.hidden" = "You have hidden all assets"; +"xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; diff --git a/fearless/ja.lproj/Localizable.strings b/fearless/ja.lproj/Localizable.strings index d17e73ecf0..8927551d50 100644 --- a/fearless/ja.lproj/Localizable.strings +++ b/fearless/ja.lproj/Localizable.strings @@ -1140,3 +1140,4 @@ "wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%s) on the destination account will be less than the minimal balance (%s). Please increase the amount by %s"; "nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; "wallet.all.assets.hidden" = "You have hidden all assets"; +"xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; diff --git a/fearless/pt-PT.lproj/Localizable.strings b/fearless/pt-PT.lproj/Localizable.strings index 5c81839032..6032be88fa 100644 --- a/fearless/pt-PT.lproj/Localizable.strings +++ b/fearless/pt-PT.lproj/Localizable.strings @@ -1276,3 +1276,4 @@ To find out more, contact %@"; "wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%s) on the destination account will be less than the minimal balance (%s). Please increase the amount by %s"; "nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; "wallet.all.assets.hidden" = "You have hidden all assets"; +"xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; diff --git a/fearless/ru.lproj/Localizable.strings b/fearless/ru.lproj/Localizable.strings index 6c9f3eadf6..e37d11b9e5 100644 --- a/fearless/ru.lproj/Localizable.strings +++ b/fearless/ru.lproj/Localizable.strings @@ -1269,3 +1269,4 @@ Euro cash"; "wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%s) on the destination account will be less than the minimal balance (%s). Please increase the amount by %s"; "nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; "wallet.all.assets.hidden" = "You have hidden all assets"; +"xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; diff --git a/fearless/tr.lproj/Localizable.strings b/fearless/tr.lproj/Localizable.strings index 547ff66661..153d24e036 100644 --- a/fearless/tr.lproj/Localizable.strings +++ b/fearless/tr.lproj/Localizable.strings @@ -1144,3 +1144,4 @@ ait olduğundan emin olun."; "wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%s) on the destination account will be less than the minimal balance (%s). Please increase the amount by %s"; "nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; "wallet.all.assets.hidden" = "You have hidden all assets"; +"xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; diff --git a/fearless/vi.lproj/Localizable.strings b/fearless/vi.lproj/Localizable.strings index e8dc890aa1..e746b4dd57 100644 --- a/fearless/vi.lproj/Localizable.strings +++ b/fearless/vi.lproj/Localizable.strings @@ -1269,3 +1269,4 @@ Giữ, stake hoặc cung cấp thanh khoản cho XOR trị giá ít nhất €%@ "wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%s) on the destination account will be less than the minimal balance (%s). Please increase the amount by %s"; "nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; "wallet.all.assets.hidden" = "You have hidden all assets"; +"xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; diff --git a/fearless/zh-Hans.lproj/Localizable.strings b/fearless/zh-Hans.lproj/Localizable.strings index 105b414f2a..61655f1d71 100644 --- a/fearless/zh-Hans.lproj/Localizable.strings +++ b/fearless/zh-Hans.lproj/Localizable.strings @@ -1267,3 +1267,4 @@ "wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%s) on the destination account will be less than the minimal balance (%s). Please increase the amount by %s"; "nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; "wallet.all.assets.hidden" = "You have hidden all assets"; +"xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; From ada531f9cdb89420eb514b575c8312f5e203b704 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Tue, 4 Jun 2024 12:41:47 +0500 Subject: [PATCH 013/115] llm min amount fix --- .../Send/Validators/SendDataValidatingFactory.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/fearless/Modules/Send/Validators/SendDataValidatingFactory.swift b/fearless/Modules/Send/Validators/SendDataValidatingFactory.swift index 295b984666..19becc2619 100644 --- a/fearless/Modules/Send/Validators/SendDataValidatingFactory.swift +++ b/fearless/Modules/Send/Validators/SendDataValidatingFactory.swift @@ -260,9 +260,15 @@ class SendDataValidatingFactory: NSObject { case (.polkadot, .soraMain), (.soraMain, .polkadot): return amount >= 1.1 case (.liberland, .soraMain): - return amount >= 1.0 && asset.symbol.lowercased() == "lld" + guard asset.symbol.lowercased() == "lld" else { + return true + } + return amount >= 1.0 case (.soraMain, .liberland): - return amount >= 1.0 && asset.symbol.lowercased() == "lld" + guard asset.symbol.lowercased() == "lld" else { + return true + } + return amount >= 1.0 default: return true } From 39f478daf584ebc90fc1d5b1e0192e6d2c0ff768 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Tue, 4 Jun 2024 13:46:35 +0500 Subject: [PATCH 014/115] [#FLW-4637] New bridge route SORA <-> Acala --- fearless.xcodeproj/project.pbxproj | 207 ------------------ .../xcshareddata/swiftpm/Package.resolved | 2 +- fearless/Common/Model/Chain.swift | 89 -------- .../SendDataValidatingFactory.swift | 4 + .../Staking/Model/RewardDestination.swift | 17 -- 5 files changed, 5 insertions(+), 314 deletions(-) delete mode 100644 fearless/Common/Model/Chain.swift diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index abff3ebf00..38ee57e282 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -228,35 +228,6 @@ 07F44F072BE4C7FF00570143 /* Web3PromiseKit in Frameworks */ = {isa = PBXBuildFile; productRef = 07F44F062BE4C7FF00570143 /* Web3PromiseKit */; }; 07F67CA32ACFC4910044047D /* WalletConnectCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F67CA22ACFC4910044047D /* WalletConnectCoordinator.swift */; }; 07F67CA52ACFEADB0044047D /* BigUInt+EthereumQuantity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F67CA42ACFEADB0044047D /* BigUInt+EthereumQuantity.swift */; }; - 07F6C4BE2BDA3E5C00E4C7B4 /* IrohaCrypto in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4BD2BDA3E5C00E4C7B4 /* IrohaCrypto */; }; - 07F6C4C02BDA3E5C00E4C7B4 /* keccak in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4BF2BDA3E5C00E4C7B4 /* keccak */; }; - 07F6C4C22BDA3E5C00E4C7B4 /* RobinHood in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4C12BDA3E5C00E4C7B4 /* RobinHood */; }; - 07F6C4C42BDA3E5C00E4C7B4 /* SoraKeystore in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4C32BDA3E5C00E4C7B4 /* SoraKeystore */; }; - 07F6C4C62BDA3E5C00E4C7B4 /* SSFAccountManagment in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4C52BDA3E5C00E4C7B4 /* SSFAccountManagment */; }; - 07F6C4C82BDA3E5C00E4C7B4 /* SSFAccountManagmentStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4C72BDA3E5C00E4C7B4 /* SSFAccountManagmentStorage */; }; - 07F6C4CA2BDA3E5C00E4C7B4 /* SSFAssetManagment in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4C92BDA3E5C00E4C7B4 /* SSFAssetManagment */; }; - 07F6C4CC2BDA3E5C00E4C7B4 /* SSFChainConnection in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4CB2BDA3E5C00E4C7B4 /* SSFChainConnection */; }; - 07F6C4CE2BDA3E5C00E4C7B4 /* SSFChainRegistry in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4CD2BDA3E5C00E4C7B4 /* SSFChainRegistry */; }; - 07F6C4D02BDA3E5C00E4C7B4 /* SSFCloudStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4CF2BDA3E5C00E4C7B4 /* SSFCloudStorage */; }; - 07F6C4D22BDA3E5C00E4C7B4 /* SSFCrypto in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4D12BDA3E5C00E4C7B4 /* SSFCrypto */; }; - 07F6C4D42BDA3E5C00E4C7B4 /* SSFEraKit in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4D32BDA3E5C00E4C7B4 /* SSFEraKit */; }; - 07F6C4D62BDA3E5C00E4C7B4 /* SSFExtrinsicKit in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4D52BDA3E5C00E4C7B4 /* SSFExtrinsicKit */; }; - 07F6C4D82BDA3E5C00E4C7B4 /* SSFHelpers in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4D72BDA3E5C00E4C7B4 /* SSFHelpers */; }; - 07F6C4DA2BDA3E5C00E4C7B4 /* SSFKeyPair in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4D92BDA3E5C00E4C7B4 /* SSFKeyPair */; }; - 07F6C4DC2BDA3E5C00E4C7B4 /* SSFLogger in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4DB2BDA3E5C00E4C7B4 /* SSFLogger */; }; - 07F6C4DE2BDA3E5C00E4C7B4 /* SSFModels in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4DD2BDA3E5C00E4C7B4 /* SSFModels */; }; - 07F6C4E02BDA3E5C00E4C7B4 /* SSFNetwork in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4DF2BDA3E5C00E4C7B4 /* SSFNetwork */; }; - 07F6C4E22BDA3E5C00E4C7B4 /* SSFPolkaswap in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4E12BDA3E5C00E4C7B4 /* SSFPolkaswap */; }; - 07F6C4E42BDA3E5C00E4C7B4 /* SSFPools in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4E32BDA3E5C00E4C7B4 /* SSFPools */; }; - 07F6C4E62BDA3E5C00E4C7B4 /* SSFPoolsStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4E52BDA3E5C00E4C7B4 /* SSFPoolsStorage */; }; - 07F6C4E82BDA3E5C00E4C7B4 /* SSFQRService in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4E72BDA3E5C00E4C7B4 /* SSFQRService */; }; - 07F6C4EA2BDA3E5C00E4C7B4 /* SSFRuntimeCodingService in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4E92BDA3E5C00E4C7B4 /* SSFRuntimeCodingService */; }; - 07F6C4EC2BDA3E5C00E4C7B4 /* SSFSigner in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4EB2BDA3E5C00E4C7B4 /* SSFSigner */; }; - 07F6C4EE2BDA3E5C00E4C7B4 /* SSFSingleValueCache in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4ED2BDA3E5C00E4C7B4 /* SSFSingleValueCache */; }; - 07F6C4F02BDA3E5D00E4C7B4 /* SSFStorageQueryKit in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4EF2BDA3E5D00E4C7B4 /* SSFStorageQueryKit */; }; - 07F6C4F22BDA3E5D00E4C7B4 /* SSFTransferService in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4F12BDA3E5D00E4C7B4 /* SSFTransferService */; }; - 07F6C4F42BDA3E5D00E4C7B4 /* SSFUtils in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4F32BDA3E5D00E4C7B4 /* SSFUtils */; }; - 07F6C4F62BDA3E5D00E4C7B4 /* SSFXCM in Frameworks */ = {isa = PBXBuildFile; productRef = 07F6C4F52BDA3E5D00E4C7B4 /* SSFXCM */; }; 07F817F72AD4051A00B87358 /* Interaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F817F62AD4051A00B87358 /* Interaction.swift */; }; 07F817FB2AD4173100B87358 /* WalletConnectCoordinatorRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F817FA2AD4173100B87358 /* WalletConnectCoordinatorRouter.swift */; }; 07F817FD2AD5242D00B87358 /* WalletConnectDefaultCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F817FC2AD5242D00B87358 /* WalletConnectDefaultCoordinator.swift */; }; @@ -566,7 +537,6 @@ 842349C52624E98C0066ACFE /* MultiAddress+Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842349C42624E98C0066ACFE /* MultiAddress+Query.swift */; }; 8423ADD026B2C38600057EDD /* ImportantFlowViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8423ADCF26B2C38600057EDD /* ImportantFlowViewFactory.swift */; }; 8423ADD226B2C9D000057EDD /* ImportantViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8423ADD126B2C9D000057EDD /* ImportantViewProtocol.swift */; }; - 8423B0E0251A759000B8687C /* Chain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8423B0DF251A759000B8687C /* Chain.swift */; }; 8423B0E8251B2DAD00B8687C /* SubstrateOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8423B0E7251B2DAD00B8687C /* SubstrateOperationFactory.swift */; }; 8424308D265B1814003E07EC /* CrowdloanOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8424308C265B1814003E07EC /* CrowdloanOperationFactory.swift */; }; 84243095265B1888003E07EC /* CrowdloanMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84243094265B1888003E07EC /* CrowdloanMetadata.swift */; }; @@ -3650,7 +3620,6 @@ 842349C42624E98C0066ACFE /* MultiAddress+Query.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MultiAddress+Query.swift"; sourceTree = ""; }; 8423ADCF26B2C38600057EDD /* ImportantFlowViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportantFlowViewFactory.swift; sourceTree = ""; }; 8423ADD126B2C9D000057EDD /* ImportantViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportantViewProtocol.swift; sourceTree = ""; }; - 8423B0DF251A759000B8687C /* Chain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Chain.swift; sourceTree = ""; }; 8423B0E7251B2DAD00B8687C /* SubstrateOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubstrateOperationFactory.swift; sourceTree = ""; }; 8424308C265B1814003E07EC /* CrowdloanOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloanOperationFactory.swift; sourceTree = ""; }; 84243094265B1888003E07EC /* CrowdloanMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloanMetadata.swift; sourceTree = ""; }; @@ -6169,73 +6138,44 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 07F6C4D22BDA3E5C00E4C7B4 /* SSFCrypto in Frameworks */, FA6ECE452BF3625E00481B2B /* SSFCloudStorage in Frameworks */, FA6ECE612BF3625E00481B2B /* SSFSigner in Frameworks */, FA6ECE512BF3625E00481B2B /* SSFLogger in Frameworks */, - 07F6C4D82BDA3E5C00E4C7B4 /* SSFHelpers in Frameworks */, FA6ECE592BF3625E00481B2B /* SSFPools in Frameworks */, FA6ECE412BF3625E00481B2B /* SSFChainConnection in Frameworks */, FA6ECE692BF3625E00481B2B /* SSFUtils in Frameworks */, FA6ECE5D2BF3625E00481B2B /* SSFQRService in Frameworks */, - 07F6C4C82BDA3E5C00E4C7B4 /* SSFAccountManagmentStorage in Frameworks */, FA6ECE652BF3625E00481B2B /* SSFStorageQueryKit in Frameworks */, - 07F6C4EC2BDA3E5C00E4C7B4 /* SSFSigner in Frameworks */, FA6ECE472BF3625E00481B2B /* SSFCrypto in Frameworks */, FA6ECE3B2BF3625E00481B2B /* SSFAccountManagment in Frameworks */, FA6ECE3F2BF3625E00481B2B /* SSFAssetManagment in Frameworks */, - 07F6C4DC2BDA3E5C00E4C7B4 /* SSFLogger in Frameworks */, - 07F6C4E02BDA3E5C00E4C7B4 /* SSFNetwork in Frameworks */, FA72546F2AC2F12D00EC47A6 /* Web3Wallet in Frameworks */, FA6ECE372BF3625E00481B2B /* IrohaCrypto in Frameworks */, - 07F6C4DA2BDA3E5C00E4C7B4 /* SSFKeyPair in Frameworks */, - 07F6C4DE2BDA3E5C00E4C7B4 /* SSFModels in Frameworks */, FA6ECE672BF3625E00481B2B /* SSFTransferService in Frameworks */, - 07F6C4F02BDA3E5D00E4C7B4 /* SSFStorageQueryKit in Frameworks */, - 07F6C4E22BDA3E5C00E4C7B4 /* SSFPolkaswap in Frameworks */, FA6ECE572BF3625E00481B2B /* SSFPolkaswap in Frameworks */, - 07F6C4EE2BDA3E5C00E4C7B4 /* SSFSingleValueCache in Frameworks */, - 07F6C4C42BDA3E5C00E4C7B4 /* SoraKeystore in Frameworks */, 07F44F072BE4C7FF00570143 /* Web3PromiseKit in Frameworks */, - 07F6C4D02BDA3E5C00E4C7B4 /* SSFCloudStorage in Frameworks */, - 07F6C4D42BDA3E5C00E4C7B4 /* SSFEraKit in Frameworks */, FA6ECE5B2BF3625E00481B2B /* SSFPoolsStorage in Frameworks */, - 07F6C4E82BDA3E5C00E4C7B4 /* SSFQRService in Frameworks */, FA6ECE6B2BF3625E00481B2B /* SSFXCM in Frameworks */, - 07F6C4E62BDA3E5C00E4C7B4 /* SSFPoolsStorage in Frameworks */, FA6ECE432BF3625E00481B2B /* SSFChainRegistry in Frameworks */, FA72546B2AC2F12D00EC47A6 /* WalletConnectNetworking in Frameworks */, 07F44F032BE4C7FF00570143 /* Web3 in Frameworks */, - 07F6C4CE2BDA3E5C00E4C7B4 /* SSFChainRegistry in Frameworks */, FA6ECE6D2BF3625E00481B2B /* SoraKeystore in Frameworks */, - 07F6C4F42BDA3E5D00E4C7B4 /* SSFUtils in Frameworks */, FA8FD1882AF4BEDD00354482 /* Swime in Frameworks */, - 07F6C4F22BDA3E5D00E4C7B4 /* SSFTransferService in Frameworks */, FA6ECE5F2BF3625E00481B2B /* SSFRuntimeCodingService in Frameworks */, FA7254672AC2F12D00EC47A6 /* WalletConnect in Frameworks */, FA6ECE532BF3625E00481B2B /* SSFModels in Frameworks */, FA6ECE552BF3625E00481B2B /* SSFNetwork in Frameworks */, FA72546D2AC2F12D00EC47A6 /* WalletConnectPairing in Frameworks */, FA7254692AC2F12D00EC47A6 /* WalletConnectAuth in Frameworks */, - 07F6C4F62BDA3E5D00E4C7B4 /* SSFXCM in Frameworks */, - 07F6C4C22BDA3E5C00E4C7B4 /* RobinHood in Frameworks */, - 07F6C4CC2BDA3E5C00E4C7B4 /* SSFChainConnection in Frameworks */, FA6ECE632BF3625E00481B2B /* SSFSingleValueCache in Frameworks */, 07F44F052BE4C7FF00570143 /* Web3ContractABI in Frameworks */, FA6ECE4B2BF3625E00481B2B /* SSFExtrinsicKit in Frameworks */, FA6ECE392BF3625E00481B2B /* RobinHood in Frameworks */, - 07F6C4C02BDA3E5C00E4C7B4 /* keccak in Frameworks */, FA6ECE6F2BF3625E00481B2B /* keccak in Frameworks */, FA6ECE3D2BF3625E00481B2B /* SSFAccountManagmentStorage in Frameworks */, C5AFED6C37C2C29E9903D136 /* Pods_fearlessAll_fearless.framework in Frameworks */, - 07F6C4BE2BDA3E5C00E4C7B4 /* IrohaCrypto in Frameworks */, - 07F6C4EA2BDA3E5C00E4C7B4 /* SSFRuntimeCodingService in Frameworks */, - 07F6C4E42BDA3E5C00E4C7B4 /* SSFPools in Frameworks */, - 07F6C4CA2BDA3E5C00E4C7B4 /* SSFAssetManagment in Frameworks */, FA6ECE492BF3625E00481B2B /* SSFEraKit in Frameworks */, - 07F6C4C62BDA3E5C00E4C7B4 /* SSFAccountManagment in Frameworks */, FA6ECE4D2BF3625E00481B2B /* SSFHelpers in Frameworks */, - 07F6C4D62BDA3E5C00E4C7B4 /* SSFExtrinsicKit in Frameworks */, FA6ECE4F2BF3625E00481B2B /* SSFKeyPair in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -9304,7 +9244,6 @@ 842876B024AE059700D91AD8 /* AboutData.swift */, 84113B6B255B2835009BD21A /* AccountCreateError.swift */, 843C49DC24DF397800B71DDA /* AccountImportSource.swift */, - 8423B0DF251A759000B8687C /* Chain.swift */, 8463A72C25E3A8E1003B8160 /* ChainStorageDecodedItem.swift */, 843910AF253ED36C00E3C217 /* ChainStorageItem.swift */, 84893C0624DA890F008F6A3F /* CommonError.swift */, @@ -15471,35 +15410,6 @@ FA72546C2AC2F12D00EC47A6 /* WalletConnectPairing */, FA72546E2AC2F12D00EC47A6 /* Web3Wallet */, FA8FD1872AF4BEDD00354482 /* Swime */, - 07F6C4BD2BDA3E5C00E4C7B4 /* IrohaCrypto */, - 07F6C4BF2BDA3E5C00E4C7B4 /* keccak */, - 07F6C4C12BDA3E5C00E4C7B4 /* RobinHood */, - 07F6C4C32BDA3E5C00E4C7B4 /* SoraKeystore */, - 07F6C4C52BDA3E5C00E4C7B4 /* SSFAccountManagment */, - 07F6C4C72BDA3E5C00E4C7B4 /* SSFAccountManagmentStorage */, - 07F6C4C92BDA3E5C00E4C7B4 /* SSFAssetManagment */, - 07F6C4CB2BDA3E5C00E4C7B4 /* SSFChainConnection */, - 07F6C4CD2BDA3E5C00E4C7B4 /* SSFChainRegistry */, - 07F6C4CF2BDA3E5C00E4C7B4 /* SSFCloudStorage */, - 07F6C4D12BDA3E5C00E4C7B4 /* SSFCrypto */, - 07F6C4D32BDA3E5C00E4C7B4 /* SSFEraKit */, - 07F6C4D52BDA3E5C00E4C7B4 /* SSFExtrinsicKit */, - 07F6C4D72BDA3E5C00E4C7B4 /* SSFHelpers */, - 07F6C4D92BDA3E5C00E4C7B4 /* SSFKeyPair */, - 07F6C4DB2BDA3E5C00E4C7B4 /* SSFLogger */, - 07F6C4DD2BDA3E5C00E4C7B4 /* SSFModels */, - 07F6C4DF2BDA3E5C00E4C7B4 /* SSFNetwork */, - 07F6C4E12BDA3E5C00E4C7B4 /* SSFPolkaswap */, - 07F6C4E32BDA3E5C00E4C7B4 /* SSFPools */, - 07F6C4E52BDA3E5C00E4C7B4 /* SSFPoolsStorage */, - 07F6C4E72BDA3E5C00E4C7B4 /* SSFQRService */, - 07F6C4E92BDA3E5C00E4C7B4 /* SSFRuntimeCodingService */, - 07F6C4EB2BDA3E5C00E4C7B4 /* SSFSigner */, - 07F6C4ED2BDA3E5C00E4C7B4 /* SSFSingleValueCache */, - 07F6C4EF2BDA3E5D00E4C7B4 /* SSFStorageQueryKit */, - 07F6C4F12BDA3E5D00E4C7B4 /* SSFTransferService */, - 07F6C4F32BDA3E5D00E4C7B4 /* SSFUtils */, - 07F6C4F52BDA3E5D00E4C7B4 /* SSFXCM */, 07F44F022BE4C7FF00570143 /* Web3 */, 07F44F042BE4C7FF00570143 /* Web3ContractABI */, 07F44F062BE4C7FF00570143 /* Web3PromiseKit */, @@ -16036,7 +15946,6 @@ FA7A4C802A1F937A0051FB4D /* RelaychainRewardCalculatorEngine.swift in Sources */, 846CD25B265709A800A2E4B6 /* StorageKeySuffixMapper.swift in Sources */, 076D9D4D29431C71002762E3 /* SwapQuoteAmountFactory.swift in Sources */, - 8423B0E0251A759000B8687C /* Chain.swift in Sources */, 07F2B75728A3A4B800280C38 /* ChainCollectionView.swift in Sources */, 8473D40D2657E9DC00B394B2 /* MultiSigner.swift in Sources */, FA17B4B527E858CB006E0735 /* JsonLoadingFailedAlertConfig.swift in Sources */, @@ -19502,122 +19411,6 @@ package = 07F44F012BE4C7FF00570143 /* XCRemoteSwiftPackageReference "Web3.swift" */; productName = Web3PromiseKit; }; - 07F6C4BD2BDA3E5C00E4C7B4 /* IrohaCrypto */ = { - isa = XCSwiftPackageProductDependency; - productName = IrohaCrypto; - }; - 07F6C4BF2BDA3E5C00E4C7B4 /* keccak */ = { - isa = XCSwiftPackageProductDependency; - productName = keccak; - }; - 07F6C4C12BDA3E5C00E4C7B4 /* RobinHood */ = { - isa = XCSwiftPackageProductDependency; - productName = RobinHood; - }; - 07F6C4C32BDA3E5C00E4C7B4 /* SoraKeystore */ = { - isa = XCSwiftPackageProductDependency; - productName = SoraKeystore; - }; - 07F6C4C52BDA3E5C00E4C7B4 /* SSFAccountManagment */ = { - isa = XCSwiftPackageProductDependency; - productName = SSFAccountManagment; - }; - 07F6C4C72BDA3E5C00E4C7B4 /* SSFAccountManagmentStorage */ = { - isa = XCSwiftPackageProductDependency; - productName = SSFAccountManagmentStorage; - }; - 07F6C4C92BDA3E5C00E4C7B4 /* SSFAssetManagment */ = { - isa = XCSwiftPackageProductDependency; - productName = SSFAssetManagment; - }; - 07F6C4CB2BDA3E5C00E4C7B4 /* SSFChainConnection */ = { - isa = XCSwiftPackageProductDependency; - productName = SSFChainConnection; - }; - 07F6C4CD2BDA3E5C00E4C7B4 /* SSFChainRegistry */ = { - isa = XCSwiftPackageProductDependency; - productName = SSFChainRegistry; - }; - 07F6C4CF2BDA3E5C00E4C7B4 /* SSFCloudStorage */ = { - isa = XCSwiftPackageProductDependency; - productName = SSFCloudStorage; - }; - 07F6C4D12BDA3E5C00E4C7B4 /* SSFCrypto */ = { - isa = XCSwiftPackageProductDependency; - productName = SSFCrypto; - }; - 07F6C4D32BDA3E5C00E4C7B4 /* SSFEraKit */ = { - isa = XCSwiftPackageProductDependency; - productName = SSFEraKit; - }; - 07F6C4D52BDA3E5C00E4C7B4 /* SSFExtrinsicKit */ = { - isa = XCSwiftPackageProductDependency; - productName = SSFExtrinsicKit; - }; - 07F6C4D72BDA3E5C00E4C7B4 /* SSFHelpers */ = { - isa = XCSwiftPackageProductDependency; - productName = SSFHelpers; - }; - 07F6C4D92BDA3E5C00E4C7B4 /* SSFKeyPair */ = { - isa = XCSwiftPackageProductDependency; - productName = SSFKeyPair; - }; - 07F6C4DB2BDA3E5C00E4C7B4 /* SSFLogger */ = { - isa = XCSwiftPackageProductDependency; - productName = SSFLogger; - }; - 07F6C4DD2BDA3E5C00E4C7B4 /* SSFModels */ = { - isa = XCSwiftPackageProductDependency; - productName = SSFModels; - }; - 07F6C4DF2BDA3E5C00E4C7B4 /* SSFNetwork */ = { - isa = XCSwiftPackageProductDependency; - productName = SSFNetwork; - }; - 07F6C4E12BDA3E5C00E4C7B4 /* SSFPolkaswap */ = { - isa = XCSwiftPackageProductDependency; - productName = SSFPolkaswap; - }; - 07F6C4E32BDA3E5C00E4C7B4 /* SSFPools */ = { - isa = XCSwiftPackageProductDependency; - productName = SSFPools; - }; - 07F6C4E52BDA3E5C00E4C7B4 /* SSFPoolsStorage */ = { - isa = XCSwiftPackageProductDependency; - productName = SSFPoolsStorage; - }; - 07F6C4E72BDA3E5C00E4C7B4 /* SSFQRService */ = { - isa = XCSwiftPackageProductDependency; - productName = SSFQRService; - }; - 07F6C4E92BDA3E5C00E4C7B4 /* SSFRuntimeCodingService */ = { - isa = XCSwiftPackageProductDependency; - productName = SSFRuntimeCodingService; - }; - 07F6C4EB2BDA3E5C00E4C7B4 /* SSFSigner */ = { - isa = XCSwiftPackageProductDependency; - productName = SSFSigner; - }; - 07F6C4ED2BDA3E5C00E4C7B4 /* SSFSingleValueCache */ = { - isa = XCSwiftPackageProductDependency; - productName = SSFSingleValueCache; - }; - 07F6C4EF2BDA3E5D00E4C7B4 /* SSFStorageQueryKit */ = { - isa = XCSwiftPackageProductDependency; - productName = SSFStorageQueryKit; - }; - 07F6C4F12BDA3E5D00E4C7B4 /* SSFTransferService */ = { - isa = XCSwiftPackageProductDependency; - productName = SSFTransferService; - }; - 07F6C4F32BDA3E5D00E4C7B4 /* SSFUtils */ = { - isa = XCSwiftPackageProductDependency; - productName = SSFUtils; - }; - 07F6C4F52BDA3E5D00E4C7B4 /* SSFXCM */ = { - isa = XCSwiftPackageProductDependency; - productName = SSFXCM; - }; FA6ECE362BF3625E00481B2B /* IrohaCrypto */ = { isa = XCSwiftPackageProductDependency; package = FA6ECE352BF3625E00481B2B /* XCRemoteSwiftPackageReference "shared-features-spm" */; diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index 009139898b..476e199453 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -133,7 +133,7 @@ "location" : "https://github.com/soramitsu/shared-features-spm.git", "state" : { "branch" : "feature/xcm-routes", - "revision" : "8f5bd406c0cb8dad1c058f8de1ed5f1159a22078" + "revision" : "09e015bcf7dafbfc18e45ad424fe48943a765644" } }, { diff --git a/fearless/Common/Model/Chain.swift b/fearless/Common/Model/Chain.swift deleted file mode 100644 index 0017d690a7..0000000000 --- a/fearless/Common/Model/Chain.swift +++ /dev/null @@ -1,89 +0,0 @@ -import Foundation -import IrohaCrypto - -enum Chain: String, Codable, CaseIterable { - case kusama = "Kusama" - case polkadot = "Polkadot" - case westend = "Westend" - case rococo = "Rococo" - case moonbeam = "Moonbeam" - case moonriver = "Moonriver" - case moonbaseAlpha = "Moonbase Alpha" - case soraMain = "SORA Mainnet" - case soraTest = "SORA Test" - case ternoa = "Ternoa Mainnet" - case equilibrium = "Equilibrium" - case reef = "Reef Mainnet" - case scuba = "Reef Scuba Testnet" - case liberland = "Liberland" - - init?(rawValue: String) { - switch rawValue { - case Self.kusama.rawValue: self = .kusama - case Self.polkadot.rawValue: self = .polkadot - case Self.westend.rawValue: self = .westend - case Self.rococo.rawValue: self = .rococo - case Self.moonbeam.rawValue: self = .moonbeam - case Self.moonriver.rawValue: self = .moonriver - case Self.moonbaseAlpha.rawValue: self = .moonbaseAlpha - case Self.soraMain.rawValue: self = .soraMain - case Self.ternoa.rawValue: self = .ternoa - case Self.equilibrium.rawValue: self = .equilibrium - case Self.reef.rawValue: self = .reef - case Self.scuba.rawValue: self = .scuba - case Self.liberland.rawValue: self = .liberland - - #if F_DEV - case "Polkatrain": self = .polkadot - #endif - - default: return nil - } - } - - init?(chainId: String) { - switch chainId { - case Self.kusama.genesisHash: self = .kusama - case Self.polkadot.genesisHash: self = .polkadot - case Self.westend.genesisHash: self = .westend - case Self.rococo.genesisHash: self = .rococo - case Self.moonbeam.genesisHash: self = .moonbeam - case Self.moonriver.genesisHash: self = .moonriver - case Self.moonbaseAlpha.genesisHash: self = .moonbaseAlpha - case Self.soraMain.genesisHash: self = .soraMain - case Self.ternoa.genesisHash: self = .ternoa - case Self.equilibrium.genesisHash: self = .equilibrium - case Self.reef.genesisHash: self = .reef - case Self.scuba.genesisHash: self = .scuba - case Self.liberland.genesisHash: self = .liberland - default: return nil - } - } - - var genesisHash: String { - switch self { - case .polkadot: return "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3" - case .kusama: return "b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe" - case .westend: return "e143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e" - case .rococo: return "1ab7fbd1d7c3532386268ec23fe4ff69f5bb6b3e3697947df3a2ec2786424de3" - case .moonbeam: return "fe58ea77779b7abda7da4ec526d14db9b1e9cd40a217c34892af80a9b332b76d" - case .moonriver: return "401a1f9dca3da46f5c4091016c8a2f26dcea05865116b286f60f668207d1474b" - case .moonbaseAlpha: return "91bc6e169807aaa54802737e1c504b2577d4fafedd5a02c10293b1cd60e39527" - case .soraMain: return "7e4e32d0feafd4f9c9414b0be86373f9a1efa904809b683453a9af6856d38ad5" - case .soraTest: return "3266816be9fa51b32cfea58d3e33ca77246bc9618595a4300e44c8856a8d8a17" - case .ternoa: return "6859c81ca95ef624c9dfe4dc6e3381c33e5d6509e35e147092bfbc780f777c4e" - case .equilibrium: return "89d3ec46d2fb43ef5a9713833373d5ea666b092fa8fd68fbc34596036571b907" - case .reef: return "7834781d38e4798d548e34ec947d19deea29df148a7bf32484b7b24dacf8d4b7" - case .scuba: return "b414a8602b2251fa538d38a9322391500bd0324bc7ac6048845d57c37dd83fe6" - case .liberland: return "6bd89e052d67a45bb60a9a23e8581053d5e0d619f15cb9865946937e690c42d6" - } - } - - var erasPerDay: Int { - switch self { - case .polkadot, .ternoa, .equilibrium, .reef, .scuba: return 1 - case .kusama, .westend, .rococo, .moonbeam, .soraMain, .soraTest, .liberland: return 4 - case .moonriver, .moonbaseAlpha: return 12 - } - } -} diff --git a/fearless/Modules/Send/Validators/SendDataValidatingFactory.swift b/fearless/Modules/Send/Validators/SendDataValidatingFactory.swift index 19becc2619..ae921e0299 100644 --- a/fearless/Modules/Send/Validators/SendDataValidatingFactory.swift +++ b/fearless/Modules/Send/Validators/SendDataValidatingFactory.swift @@ -269,6 +269,10 @@ class SendDataValidatingFactory: NSObject { return true } return amount >= 1.0 + case (.soraMain, .acala): + return amount >= 1.0 + case (.acala, .soraMain): + return amount >= 56.0 default: return true } diff --git a/fearless/Modules/Staking/Model/RewardDestination.swift b/fearless/Modules/Staking/Model/RewardDestination.swift index f8fee3e595..1e1c533fe8 100644 --- a/fearless/Modules/Staking/Model/RewardDestination.swift +++ b/fearless/Modules/Staking/Model/RewardDestination.swift @@ -22,23 +22,6 @@ extension RewardDestination: Equatable where A == AccountAddress { self = .payout(account: address) } } - - @available(*, deprecated, message: "Use init(payee:stashItem:chainFormat:) instead") - init(payee: RewardDestinationArg, stashItem: StashItem, chain _: Chain) throws { - switch payee { - case .staked: - self = .restake - case .stash: - self = .payout(account: stashItem.stash) - case .controller: - self = .payout(account: stashItem.controller) - case let .account(accountId): - let address = try accountId.toAddress(using: ChainFormat.substrate(42)) - self = .payout(account: address) - case let .address(address): - self = .payout(account: address) - } - } } extension RewardDestination where A == ChainAccountResponse { From d294618133c7829593e65719517fc8e32ad7c552 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Wed, 5 Jun 2024 13:44:54 +0500 Subject: [PATCH 015/115] [#FLW-4664] There is wrong balance on the asset --- fearless/Common/ViewModelFactory/ChainAssetListBuilder.swift | 4 ++-- .../Modules/Send/Validators/SendDataValidatingFactory.swift | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/fearless/Common/ViewModelFactory/ChainAssetListBuilder.swift b/fearless/Common/ViewModelFactory/ChainAssetListBuilder.swift index 132e78122b..b194bc3beb 100644 --- a/fearless/Common/ViewModelFactory/ChainAssetListBuilder.swift +++ b/fearless/Common/ViewModelFactory/ChainAssetListBuilder.swift @@ -291,10 +291,10 @@ extension ChainAssetListBuilder { pricesData: [PriceData], wallet: MetaAccountModel ) -> [AssetChainAssets] { - let assetNamesSet: Set = Set(chainAssets.map { $0.asset.symbolUppercased }) + let assetNamesSet: Set = Set(chainAssets.map { $0.asset.normalizedSymbol() }) return assetNamesSet.compactMap { name in - let assetChainAssets = chainAssets.filter { $0.asset.symbolUppercased == name && wallet.fetch(for: $0.chain.accountRequest()) != nil } + let assetChainAssets = chainAssets.filter { $0.asset.normalizedSymbol() == name && wallet.fetch(for: $0.chain.accountRequest()) != nil } let chainAssetsSorted = assetChainAssets.sorted(by: { ca1, ca2 in sortChainAssets(ca1: ca1, ca2: ca2) }) diff --git a/fearless/Modules/Send/Validators/SendDataValidatingFactory.swift b/fearless/Modules/Send/Validators/SendDataValidatingFactory.swift index ae921e0299..d13e59c074 100644 --- a/fearless/Modules/Send/Validators/SendDataValidatingFactory.swift +++ b/fearless/Modules/Send/Validators/SendDataValidatingFactory.swift @@ -334,6 +334,10 @@ class SendDataValidatingFactory: NSObject { return "1.0 LLD" case (.soraMain, .liberland): return "1.0 LLD" + case (.soraMain, .acala): + return "1.0 ACA" + case (.acala, .soraMain): + return "56.0 ACA" default: return "" } From f4a41fddb930ffc7af8656fbe75a9c0bc7e7b233 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Wed, 5 Jun 2024 16:03:30 +0500 Subject: [PATCH 016/115] update package --- fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index 476e199453..740d4d33f8 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -133,7 +133,7 @@ "location" : "https://github.com/soramitsu/shared-features-spm.git", "state" : { "branch" : "feature/xcm-routes", - "revision" : "09e015bcf7dafbfc18e45ad424fe48943a765644" + "revision" : "def77f0a4496d16421aa92e16fb14494f93bfb82" } }, { From a5e3b25689573878948e61348cee505c2e4878d8 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Wed, 5 Jun 2024 16:25:49 +0500 Subject: [PATCH 017/115] [#FLW-4667] We can see test networks on release version --- fearless/Common/Helpers/ChainAssetsFetching.swift | 3 +++ fearless/Common/Helpers/WalletAssetsObserver.swift | 3 +++ .../Modules/AssetManagement/AssetManagementInteractor.swift | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/fearless/Common/Helpers/ChainAssetsFetching.swift b/fearless/Common/Helpers/ChainAssetsFetching.swift index 9e5c36cdef..dec5afd601 100644 --- a/fearless/Common/Helpers/ChainAssetsFetching.swift +++ b/fearless/Common/Helpers/ChainAssetsFetching.swift @@ -35,6 +35,7 @@ final class ChainAssetsFetching: ChainAssetFetchingProtocol { case chainIds([ChainModel.Id]) case supportNfts case enabled(wallet: MetaAccountModel) + case enabledChains var searchText: String? { switch self { @@ -243,6 +244,8 @@ private extension ChainAssetsFetching { .filter { !$0.hidden } .map { $0.assetId } return chainAssets.filter { enabled.contains($0.identifier) } + case .enabledChains: + return chainAssets.filter { !$0.chain.disabled } } } diff --git a/fearless/Common/Helpers/WalletAssetsObserver.swift b/fearless/Common/Helpers/WalletAssetsObserver.swift index 5473241fba..a76e5447ca 100644 --- a/fearless/Common/Helpers/WalletAssetsObserver.swift +++ b/fearless/Common/Helpers/WalletAssetsObserver.swift @@ -76,6 +76,9 @@ final class WalletAssetsObserverImpl: WalletAssetsObserver { changes.forEach { change in switch change { case let .insert(chain): + guard !chain.disabled else { + return + } if let accounts { if accounts.contains(where: { $0.chainId == chain.chainId }) { group.addTask { diff --git a/fearless/Modules/AssetManagement/AssetManagementInteractor.swift b/fearless/Modules/AssetManagement/AssetManagementInteractor.swift index b69370b57e..fafdde5722 100644 --- a/fearless/Modules/AssetManagement/AssetManagementInteractor.swift +++ b/fearless/Modules/AssetManagement/AssetManagementInteractor.swift @@ -96,7 +96,7 @@ extension AssetManagementInteractor: AssetManagementInteractorInput { func getAvailableChainAssets() async throws -> [ChainAsset] { let chainAssets = try await chainAssetFetching.fetchAwait( shouldUseCache: true, - filters: [], + filters: [.enabledChains], sortDescriptors: [] ) fetchPrices(for: chainAssets) From 3ae631fb9afd45eb786daf2a6f332bbfd25ea4f2 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Thu, 6 Jun 2024 10:07:15 +0500 Subject: [PATCH 018/115] [#FLW-4664] There is wrong balance on the asset --- .../WalletBalanceBuilder.swift | 19 +++---------------- .../WalletBalanceSubscriptionAdapter.swift | 4 ++-- .../ChainAssetListBuilder.swift | 14 ++++++++++++++ .../ChainAccount/ChainAccountInteractor.swift | 6 +++++- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/fearless/ApplicationLayer/Services/Balance/WalletBalanceSubscription/WalletBalanceBuilder.swift b/fearless/ApplicationLayer/Services/Balance/WalletBalanceSubscription/WalletBalanceBuilder.swift index aa9900e894..60d20b37dd 100644 --- a/fearless/ApplicationLayer/Services/Balance/WalletBalanceSubscription/WalletBalanceBuilder.swift +++ b/fearless/ApplicationLayer/Services/Balance/WalletBalanceSubscription/WalletBalanceBuilder.swift @@ -23,7 +23,6 @@ final class WalletBalanceBuilder: WalletBalanceBuilderProtocol { let splitedChainAssets = split(chainAssets, for: wallet) let enabledChainAssets = splitedChainAssets.enabled - let disabledChainAssets = splitedChainAssets.disabled let enabledAssetFiatBalanceInfo = countBalance( for: enabledChainAssets, @@ -32,24 +31,12 @@ final class WalletBalanceBuilder: WalletBalanceBuilderProtocol { prices ) - let disabledAssetFiatBalanceInfo = countBalance( - for: disabledChainAssets, - wallet, - accountInfos, - prices - ) - let enabledAssetFiatBalance = enabledAssetFiatBalanceInfo.totalBalance - let disabledAssetFiatBalance = disabledAssetFiatBalanceInfo.totalBalance - let totalFiatValue = enabledAssetFiatBalance + disabledAssetFiatBalance - + let totalFiatValue = enabledAssetFiatBalance let enabledTotalDayChange = enabledAssetFiatBalanceInfo.totalDayChange - let disabledTotalDayChange = disabledAssetFiatBalanceInfo.totalDayChange - let totalDayChange = enabledTotalDayChange + disabledTotalDayChange - + let totalDayChange = enabledTotalDayChange let dayChangePercent = (totalDayChange / totalFiatValue) - - let isLoaded = enabledAssetFiatBalanceInfo.isLoaded && disabledAssetFiatBalanceInfo.isLoaded + let isLoaded = enabledAssetFiatBalanceInfo.isLoaded guard isLoaded else { return nil diff --git a/fearless/ApplicationLayer/Services/Balance/WalletBalanceSubscription/WalletBalanceSubscriptionAdapter.swift b/fearless/ApplicationLayer/Services/Balance/WalletBalanceSubscription/WalletBalanceSubscriptionAdapter.swift index 0f95d9aacd..5f21f5e686 100644 --- a/fearless/ApplicationLayer/Services/Balance/WalletBalanceSubscription/WalletBalanceSubscriptionAdapter.swift +++ b/fearless/ApplicationLayer/Services/Balance/WalletBalanceSubscription/WalletBalanceSubscriptionAdapter.swift @@ -203,7 +203,7 @@ final class WalletBalanceSubscriptionAdapter: WalletBalanceSubscriptionAdapterPr Task { do { async let wallets = self.walletRepository.fetchAll() - async let chainAssets = chainAssetFetcher.fetchAwait(shouldUseCache: false, filters: [], sortDescriptors: []) + async let chainAssets = chainAssetFetcher.fetchAwait(shouldUseCache: false, filters: [.enabledChains], sortDescriptors: []) try await handle(wallets, chainAssets) let accountInfos = try await fetchAccountInfos(wallets: wallets, chainAssets: chainAssets) @@ -383,7 +383,7 @@ extension WalletBalanceSubscriptionAdapter: EventVisitorProtocol { Task { let chainAssets = try await chainAssetFetcher.fetchAwait( shouldUseCache: false, - filters: [], + filters: [.enabledChains], sortDescriptors: [] ) subscribeToAccountInfo(for: wallets, chainAssets) diff --git a/fearless/Common/ViewModelFactory/ChainAssetListBuilder.swift b/fearless/Common/ViewModelFactory/ChainAssetListBuilder.swift index b194bc3beb..85713d993f 100644 --- a/fearless/Common/ViewModelFactory/ChainAssetListBuilder.swift +++ b/fearless/Common/ViewModelFactory/ChainAssetListBuilder.swift @@ -6,6 +6,12 @@ protocol ChainAssetListBuilder { var assetBalanceFormatterFactory: AssetBalanceFormatterFactoryProtocol { get } } +extension ChainAssetListBuilder { + var assetBalanceFormatterFactory: AssetBalanceFormatterFactoryProtocol { + AssetBalanceFormatterFactory() + } +} + struct AssetChainAssets { let chainAssets: [ChainAsset] let mainChainAsset: ChainAsset @@ -344,6 +350,14 @@ extension ChainAssetListBuilder { guard wallet.assetsVisibility.isNotEmpty else { return defaultByPopular(chainAssets: chainAssets) } + let enabled = enabled(chainAssets: chainAssets, for: wallet) + return enabled + } + + func enabled( + chainAssets: [ChainAsset], + for wallet: MetaAccountModel + ) -> [ChainAsset] { let enabledAssetIds: [String] = wallet.assetsVisibility .filter { !$0.hidden } .map { $0.assetId } diff --git a/fearless/Modules/NewWallet/ChainAccount/ChainAccountInteractor.swift b/fearless/Modules/NewWallet/ChainAccount/ChainAccountInteractor.swift index 014bc9fb16..59d22c0ec5 100644 --- a/fearless/Modules/NewWallet/ChainAccount/ChainAccountInteractor.swift +++ b/fearless/Modules/NewWallet/ChainAccount/ChainAccountInteractor.swift @@ -58,7 +58,11 @@ final class ChainAccountInteractor { private func getAvailableChainAssets() { chainAssetFetching.fetch( shouldUseCache: true, - filters: [.assetNames([chainAsset.asset.symbol, "xc\(chainAsset.asset.symbol)"])], + filters: [ + .assetNames([chainAsset.asset.symbol, "xc\(chainAsset.asset.symbol)"]), + .enabledChains, + .enabled(wallet: wallet) + ], sortDescriptors: [] ) { [weak self] result in guard let strongSelf = self else { From 687d6aa38e7dacdae3aefe4799dd8c5587ce5225 Mon Sep 17 00:00:00 2001 From: Alex Lebedko Date: Thu, 6 Jun 2024 16:52:30 +0700 Subject: [PATCH 019/115] supply raw --- Podfile | 1 - Podfile.lock | 7 +- fearless.xcodeproj/project.pbxproj | 92 ++++- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../SoraXNetworkingHistoryOperation.swift | 116 ------ ...Item+WalletRemoteHistoryItemProtocol.swift | 355 ------------------ .../ViewModel/Amount/MoneyPresentable.swift | 6 +- .../SoraHistoryOperationFactory.swift | 218 ----------- .../LiquidityPoolDetailsViewLayout.swift | 4 +- .../LiquidityPoolSupplyAssembly.swift | 7 +- .../LiquidityPoolSupplyInteractor.swift | 23 ++ .../LiquidityPoolSupplyPresenter.swift | 184 ++++++--- .../LiquidityPoolSupplyProtocols.swift | 9 + .../LiquidityPoolSupplyRouter.swift | 15 + .../LiquidityPoolSupplyViewController.swift | 36 +- .../LiquidityPoolSupplyViewLayout.swift | 75 ++-- .../LiquidityPoolSupplyViewModel.swift | 7 + .../LiquidityPoolSupplyViewModelFactory.swift | 39 ++ .../LiquidityPoolSupplyConfirmAssembly.swift | 97 +++++ ...LiquidityPoolSupplyConfirmInteractor.swift | 157 ++++++++ .../LiquidityPoolSupplyConfirmPresenter.swift | 352 +++++++++++++++++ .../LiquidityPoolSupplyConfirmProtocols.swift | 18 + .../LiquidityPoolSupplyConfirmRouter.swift | 28 ++ ...idityPoolSupplyConfirmViewController.swift | 118 ++++++ ...LiquidityPoolSupplyConfirmViewLayout.swift | 201 ++++++++++ .../LiquidityPoolSupplyConfirmViewModel.swift | 6 + ...ityPoolSupplyConfirmViewModelFactory.swift | 100 +++++ ...leLiquidityPoolsListViewModelFactory.swift | 2 +- .../LiquidityPoolSupplyConfirmTests.swift | 16 + 29 files changed, 1470 insertions(+), 823 deletions(-) delete mode 100644 fearless/Common/Network/BlockExplorer/Subquery/Sora/SoraXNetworkingHistoryOperation.swift delete mode 100644 fearless/Common/Network/BlockExplorer/Subquery/Sora/TxHistoryItem+WalletRemoteHistoryItemProtocol.swift delete mode 100644 fearless/CoreLayer/OperationFactory/SoraHistoryOperationFactory.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolSupply/ViewModel/LiquidityPoolSupplyViewModel.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolSupply/ViewModel/LiquidityPoolSupplyViewModelFactory.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmAssembly.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmInteractor.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmPresenter.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmProtocols.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmRouter.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewController.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewLayout.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/ViewModel/LiquidityPoolSupplyConfirmViewModel.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/ViewModel/LiquidityPoolSupplyConfirmViewModelFactory.swift create mode 100644 fearlessTests/Modules/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmTests.swift diff --git a/Podfile b/Podfile index efffe1fc2f..4a23753e0c 100644 --- a/Podfile +++ b/Podfile @@ -20,7 +20,6 @@ abstract_target 'fearlessAll' do pod 'Kingfisher', '7.10.2' , :inhibit_warnings => true pod 'SVGKit' pod 'Charts', '~> 4.1.0' - pod 'XNetworking', :podspec => 'https://raw.githubusercontent.com/soramitsu/x-networking/0.0.37/AppCommonNetworking/XNetworking/XNetworking.podspec' pod 'MediaView', :git => 'https://github.com/bnsports/MediaView.git', :branch => 'dev' pod 'FearlessKeys', '0.1.3' diff --git a/Podfile.lock b/Podfile.lock index 9fa084f207..f7dd9cdf7f 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -79,7 +79,6 @@ PODS: - SwiftFormat/CLI (0.47.13) - SwiftLint (0.54.0) - SwiftyBeaver (2.0.0) - - XNetworking (0.0.37) DEPENDENCIES: - Charts (~> 4.1.0) @@ -100,7 +99,6 @@ DEPENDENCIES: - SwiftFormat/CLI (~> 0.47.13) - SwiftLint - SwiftyBeaver - - XNetworking (from `https://raw.githubusercontent.com/soramitsu/x-networking/0.0.37/AppCommonNetworking/XNetworking/XNetworking.podspec`) SPEC REPOS: https://github.com/cocoapods/Specs.git: @@ -134,8 +132,6 @@ EXTERNAL SOURCES: Starscream: :git: https://github.com/soramitsu/fearless-starscream.git :tag: 4.0.8 - XNetworking: - :podspec: https://raw.githubusercontent.com/soramitsu/x-networking/0.0.37/AppCommonNetworking/XNetworking/XNetworking.podspec CHECKOUT OPTIONS: MediaView: @@ -170,8 +166,7 @@ SPEC CHECKSUMS: SwiftFormat: 73573b89257437c550b03d934889725fbf8f75e5 SwiftLint: c1de071d9d08c8aba837545f6254315bc900e211 SwiftyBeaver: 014b0c12065026b731bac80305294f27d63e27f6 - XNetworking: 516d982bd74ff208222381d27e07052a5d897aaf -PODFILE CHECKSUM: 0770b928833b595ca2513a2a32218039b8f0e068 +PODFILE CHECKSUM: 3e3f33da665e33df253f763ffaeb6830b00dded0 COCOAPODS: 1.15.2 diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index dada13f5eb..b821e7df58 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -18,11 +18,11 @@ 06197BBE4299DC971C42DB92 /* WalletSendConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A687FBDA0912F8727CE0D81 /* WalletSendConfirmPresenter.swift */; }; 06590486EED4050BADDD32C5 /* AccountManagementPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2B676982F60C55530BDD569 /* AccountManagementPresenter.swift */; }; 0678271BE1BA5BBC084F478A /* RecommendedValidatorListWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57C624E71FCE0FFF8EAD5BA9 /* RecommendedValidatorListWireframe.swift */; }; + 06826AA6DBAEA5BBF45BB5CA /* LiquidityPoolSupplyConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4594686D10DBB7627B8C9A12 /* LiquidityPoolSupplyConfirmInteractor.swift */; }; 069C7D4C9648112115B90234 /* NftSendConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20EB6F377A05C11850066B9F /* NftSendConfirmProtocols.swift */; }; 0702B31529701759003519F5 /* AmountInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0702B31429701759003519F5 /* AmountInputViewModel.swift */; }; 0702B31829701864003519F5 /* WalletViewModelObserverContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0702B31729701864003519F5 /* WalletViewModelObserverContainer.swift */; }; 0702B31A29701884003519F5 /* MoneyPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0702B31929701884003519F5 /* MoneyPresentable.swift */; }; - 0702B35229791357003519F5 /* SoraHistoryOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0702B35129791357003519F5 /* SoraHistoryOperationFactory.swift */; }; 0702B355297948C8003519F5 /* SwapTransactionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0702B354297948C8003519F5 /* SwapTransactionViewModel.swift */; }; 0702B35729794BA6003519F5 /* SwapTransactionViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0702B35629794BA6003519F5 /* SwapTransactionViewModelFactory.swift */; }; 07089AF328B63386001566CA /* UIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07089AF228B63386001566CA /* UIButton.swift */; }; @@ -232,6 +232,7 @@ 0EFEBFB39CE4B0A61B6CD914 /* NftSendConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C542733CEFB871FCD23195E /* NftSendConfirmInteractor.swift */; }; 0F3E58FC800ED8722589F89E /* ReferralCrowdloanPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C503100478AB56E903598A78 /* ReferralCrowdloanPresenter.swift */; }; 0FB6781AB0186A1ED474CAD6 /* StakingUnbondConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD348E749EC6A7E3BB069DE /* StakingUnbondConfirmProtocols.swift */; }; + 1029947E04F64B616E252949 /* LiquidityPoolSupplyConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F6B892F62579DE761073CA /* LiquidityPoolSupplyConfirmPresenter.swift */; }; 1062C095BC566A1EA8DE1C06 /* CrowdloanContributionSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C71DEF78B69F017DF460AB7 /* CrowdloanContributionSetupViewController.swift */; }; 10B4951F5E0C515EFBDBC32E /* StakingPoolCreateConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3837CE5CB2D48D8A694A7EE0 /* StakingPoolCreateConfirmPresenter.swift */; }; 134AFD616BE52A1AE290EEF7 /* StakingBalanceFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C67ACC943DA07FC529AE69B4 /* StakingBalanceFlow.swift */; }; @@ -265,6 +266,7 @@ 23398AEC756086FEEEE91E65 /* WalletsManagmentRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B55A4C7E8BD87A7C8634ADD /* WalletsManagmentRouter.swift */; }; 237AD34CD1C2778834D7B330 /* AnalyticsValidatorsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6F8BBBA9EABA266B288333F /* AnalyticsValidatorsViewFactory.swift */; }; 23E30A21620831C600CBC1D6 /* LiquidityPoolDetailsRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28E3B4BA1160B87C435C2AAF /* LiquidityPoolDetailsRouter.swift */; }; + 24E6794278FEC01DFFE7C69C /* LiquidityPoolSupplyConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D430E8B808864B281A62AB43 /* LiquidityPoolSupplyConfirmViewController.swift */; }; 2510425D74B7318818B57B0F /* PolkaswapTransaktionSettingsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC6DBA90336C1C8A40F3601 /* PolkaswapTransaktionSettingsProtocols.swift */; }; 2515863D26C9F862AB800C4C /* ContactsViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D39CEB720F336B3A400477E /* ContactsViewLayout.swift */; }; 25381484F16FB930B8A90CE3 /* SelectValidatorsConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FE5F01FC9364788A91EFA5 /* SelectValidatorsConfirmProtocols.swift */; }; @@ -420,6 +422,7 @@ 6DA57B4E2F3390624BEA03C1 /* StakingPoolCreateConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA8ECADDA809DE7932B7A17C /* StakingPoolCreateConfirmProtocols.swift */; }; 6ECD0116CD39D8F55D246864 /* SelectValidatorsConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B5626189788682A84D4E9D7 /* SelectValidatorsConfirmPresenter.swift */; }; 6FA6FA6944AD6519FB8A2AC0 /* WalletMainContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961642321C9AD1885325A3D7 /* WalletMainContainerViewController.swift */; }; + 6FAC7E8F0DACB3F2AA0BE825 /* LiquidityPoolSupplyConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCCD9A6B753FD1510D3DD311 /* LiquidityPoolSupplyConfirmProtocols.swift */; }; 705F5EEDD70D6941D138D3F9 /* ContactsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E4DF941DE0EDEF99A843A9D /* ContactsInteractor.swift */; }; 709ABA5647D7DFF36EBCE73E /* WarningAlertViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B7FA75791904AF541BE380 /* WarningAlertViewFactory.swift */; }; 70EAB410A0106F22C2183847 /* StakingUnbondSetupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E1CD099F7C30ABE0E8A001 /* StakingUnbondSetupTests.swift */; }; @@ -431,6 +434,7 @@ 7401E7CAEEE6890BE74ACCE1 /* CustomValidatorListViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2956D0C69019DDCDAB2EB34 /* CustomValidatorListViewLayout.swift */; }; 742374EE778D76ABC965E107 /* StakingAmountViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = AB2349A5057312BDB6C65804 /* StakingAmountViewController.xib */; }; 7489BDA1D23D8DF73E7EB9BC /* UsernameSetupWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65AD15693E21C869DE1FDD17 /* UsernameSetupWireframe.swift */; }; + 74A39BC366D85CC5FCA78579 /* LiquidityPoolSupplyConfirmViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 641B699003FF648A380F7FA6 /* LiquidityPoolSupplyConfirmViewLayout.swift */; }; 75F3A0D30C8BC050489F4644 /* WalletOptionRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EC386082DDF4DF1ADDCB49D /* WalletOptionRouter.swift */; }; 76214649137E7061F701FE38 /* WalletMainContainerAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E096A576B747C09B14FD38D /* WalletMainContainerAssembly.swift */; }; 762BB1AC2F45142B6319B59F /* NftSendProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = F55184167D22A33EF7FF77AE /* NftSendProtocols.swift */; }; @@ -1350,6 +1354,7 @@ 96EBE5F57C97C7A7A525E864 /* SwapTransactionDetailProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC265B5F209B038633AE0E3F /* SwapTransactionDetailProtocols.swift */; }; 982BB3FA25BA6AD5443B24C6 /* NetworkIssuesNotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35EAD41DB1444DA38D8C65E2 /* NetworkIssuesNotificationPresenter.swift */; }; 991BF0BF6DD4D4243073E8C9 /* NftSendConfirmAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D9C58C9D53A7EA34E5CB00C /* NftSendConfirmAssembly.swift */; }; + 991DA2DE1E8A45B0A8B187CD /* LiquidityPoolSupplyConfirmAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 112609AE5629962646248BF0 /* LiquidityPoolSupplyConfirmAssembly.swift */; }; 992A5E2E06A9C0E603A10BF7 /* MainNftContainerInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16219B3D0D2A658185C0850 /* MainNftContainerInteractor.swift */; }; 9942034DCB680824831B0AC1 /* AccountCreatePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBBEC474F607DD9F2A0F4FD /* AccountCreatePresenter.swift */; }; 99A4B2A357ADEA45EFF515A5 /* AccountExportPasswordProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC404A4071AF571FAC4C1994 /* AccountExportPasswordProtocols.swift */; }; @@ -1526,6 +1531,7 @@ B15D513381B7626AB90018F0 /* StakingPoolInfoInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA0B7C9F68A6BF3D3A6D8234 /* StakingPoolInfoInteractor.swift */; }; B1CCC5B7BF30F6ACA309B112 /* StakingRedeemViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7F5F9B54BE4234C5682BDE /* StakingRedeemViewController.swift */; }; B2E3219218E3F54EEB7D5C3C /* NftSendViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0A0CC21B0CD2A47B9B28841 /* NftSendViewController.swift */; }; + B40863AA7377BFE5F8A30259 /* LiquidityPoolSupplyConfirmTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A43F0468DEBA7500C6B23AF /* LiquidityPoolSupplyConfirmTests.swift */; }; B478746C8342468ECACE3478 /* StakingRewardDetailsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D3BFF5C921FEB356E2C39A4 /* StakingRewardDetailsTests.swift */; }; B51AD1836313CE26F369ED3F /* CustomValidatorListWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96D540DFC00C25D8F73CFDC3 /* CustomValidatorListWireframe.swift */; }; B58B2D9494687F357821D53D /* AnalyticsValidatorsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F3B726402D4DB25059EF156 /* AnalyticsValidatorsProtocols.swift */; }; @@ -2260,6 +2266,8 @@ FA5137B929AC76EB00560EBA /* GiantsquidHistoryOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA5137B529AC76EB00560EBA /* GiantsquidHistoryOperationFactory.swift */; }; FA5137BA29AC76EB00560EBA /* SubsquidHistoryOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA5137B629AC76EB00560EBA /* SubsquidHistoryOperationFactory.swift */; }; FA5137BB29AC76EB00560EBA /* HistoryOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA5137B729AC76EB00560EBA /* HistoryOperationFactory.swift */; }; + FA53D8902C084ECA00173ADB /* LiquidityPoolSupplyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA53D88F2C084ECA00173ADB /* LiquidityPoolSupplyViewModel.swift */; }; + FA53D8922C08510000173ADB /* LiquidityPoolSupplyViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA53D8912C08510000173ADB /* LiquidityPoolSupplyViewModelFactory.swift */; }; FA584C782AB2BCD500F6F020 /* UniversalMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA584C772AB2BCD500F6F020 /* UniversalMediaView.swift */; }; FA584C7A2AB2BFE300F6F020 /* MediaType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA584C792AB2BFE300F6F020 /* MediaType.swift */; }; FA584C7C2AB3071E00F6F020 /* AlchemyNFTOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA584C7B2AB3071E00F6F020 /* AlchemyNFTOperationFactory.swift */; }; @@ -2509,6 +2517,8 @@ FA8810D02BDCAF260084CC4B /* keccak in Frameworks */ = {isa = PBXBuildFile; productRef = FA8810CF2BDCAF260084CC4B /* keccak */; }; FA8810D32BDCCF7E0084CC4B /* UserLiquidityPoolsListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA8810D22BDCCF7E0084CC4B /* UserLiquidityPoolsListPresenter.swift */; }; FA8810D52BDCD19D0084CC4B /* UserLiquidityPoolsListViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA8810D42BDCD19D0084CC4B /* UserLiquidityPoolsListViewModelFactory.swift */; }; + FA887A452C107A1100CA720F /* LiquidityPoolSupplyConfirmViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA887A442C107A1100CA720F /* LiquidityPoolSupplyConfirmViewModel.swift */; }; + FA887A472C107A4300CA720F /* LiquidityPoolSupplyConfirmViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA887A462C107A4300CA720F /* LiquidityPoolSupplyConfirmViewModelFactory.swift */; }; FA8ED43328FD960F00EBB712 /* YourValidatorListFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA8ED43228FD960F00EBB712 /* YourValidatorListFlow.swift */; }; FA8ED43628FD983A00EBB712 /* YourValidatorListRelaychainStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA8ED43528FD983A00EBB712 /* YourValidatorListRelaychainStrategy.swift */; }; FA8ED43828FD984500EBB712 /* YourValidatorListRelaychainViewModelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA8ED43728FD984500EBB712 /* YourValidatorListRelaychainViewModelState.swift */; }; @@ -3040,11 +3050,9 @@ FAFFAE4029AC84850074AF1F /* AccountManagementPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFFAE3F29AC84850074AF1F /* AccountManagementPresentable.swift */; }; FAFFAE7329AC84B10074AF1F /* RewardOrSlashData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFFAE4229AC84B10074AF1F /* RewardOrSlashData.swift */; }; FAFFAE7429AC84B10074AF1F /* SubqueryLiquidity.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFFAE4529AC84B10074AF1F /* SubqueryLiquidity.swift */; }; - FAFFAE7529AC84B10074AF1F /* SoraXNetworkingHistoryOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFFAE4629AC84B10074AF1F /* SoraXNetworkingHistoryOperation.swift */; }; FAFFAE7629AC84B10074AF1F /* TransactionContextKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFFAE4729AC84B10074AF1F /* TransactionContextKeys.swift */; }; FAFFAE7729AC84B10074AF1F /* ReferralMethodType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFFAE4829AC84B10074AF1F /* ReferralMethodType.swift */; }; FAFFAE7829AC84B10074AF1F /* SubquerySwap.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFFAE4929AC84B10074AF1F /* SubquerySwap.swift */; }; - FAFFAE7929AC84B10074AF1F /* TxHistoryItem+WalletRemoteHistoryItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFFAE4A29AC84B10074AF1F /* TxHistoryItem+WalletRemoteHistoryItemProtocol.swift */; }; FAFFAE7A29AC84B10074AF1F /* SubqueryCreatePoolLiquidity.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFFAE4B29AC84B10074AF1F /* SubqueryCreatePoolLiquidity.swift */; }; FAFFAE7B29AC84B10074AF1F /* SubqueryReferral.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFFAE4C29AC84B10074AF1F /* SubqueryReferral.swift */; }; FAFFAE7C29AC84B10074AF1F /* KmmCallCodingPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFFAE4D29AC84B10074AF1F /* KmmCallCodingPath.swift */; }; @@ -3092,6 +3100,7 @@ FB214D3851B28AA8FF05AA41 /* NftCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 610A7425DF3F20BE0AB05ECD /* NftCollectionViewLayout.swift */; }; FB377FFB8421F09C875E1968 /* NftDetailsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09373C708FE543066B943E45 /* NftDetailsInteractor.swift */; }; FB6B2B551238260D754E036D /* NftSendConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 447D03660EFFD8CF46374D85 /* NftSendConfirmViewController.swift */; }; + FBE4702B7F95484235386631 /* LiquidityPoolSupplyConfirmRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5121DA4DDF11ED6A6659CEB /* LiquidityPoolSupplyConfirmRouter.swift */; }; FC54046997436059EF436983 /* NftSendRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01A85735F958D05CFEFCB4DF /* NftSendRouter.swift */; }; FC9BD9BC4722D8B17B7A7ACC /* MainNftContainerProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0892AD886F2BE499C075C701 /* MainNftContainerProtocols.swift */; }; FE65F897D63CAA8F8AE4B972 /* NftCollectionAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DE7B3D0BE06472153C0A78C /* NftCollectionAssembly.swift */; }; @@ -3129,10 +3138,10 @@ 04806331BF10F63A49326941 /* NetworkInfoWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NetworkInfoWireframe.swift; sourceTree = ""; }; 04EF69DFE142600FF2708A13 /* ControllerAccountConfirmationViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ControllerAccountConfirmationViewController.swift; sourceTree = ""; }; 0533F9E531CDFB721D697769 /* YourValidatorListPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = YourValidatorListPresenter.swift; sourceTree = ""; }; + 06F6B892F62579DE761073CA /* LiquidityPoolSupplyConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyConfirmPresenter.swift; sourceTree = ""; }; 0702B31429701759003519F5 /* AmountInputViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmountInputViewModel.swift; sourceTree = ""; }; 0702B31729701864003519F5 /* WalletViewModelObserverContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletViewModelObserverContainer.swift; sourceTree = ""; }; 0702B31929701884003519F5 /* MoneyPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoneyPresentable.swift; sourceTree = ""; }; - 0702B35129791357003519F5 /* SoraHistoryOperationFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SoraHistoryOperationFactory.swift; sourceTree = ""; }; 0702B354297948C8003519F5 /* SwapTransactionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapTransactionViewModel.swift; sourceTree = ""; }; 0702B35629794BA6003519F5 /* SwapTransactionViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapTransactionViewModelFactory.swift; sourceTree = ""; }; 07089AF228B63386001566CA /* UIButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIButton.swift; sourceTree = ""; }; @@ -3341,6 +3350,7 @@ 0E07F60661001B2C505D9C8B /* SelectMarketPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectMarketPresenter.swift; sourceTree = ""; }; 0F48467D97F9D06F83F70894 /* Pods-fearlessAll-fearless.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-fearlessAll-fearless.dev.xcconfig"; path = "Target Support Files/Pods-fearlessAll-fearless/Pods-fearlessAll-fearless.dev.xcconfig"; sourceTree = ""; }; 0F67BB672040911E4A165446 /* PolkaswapSwapConfirmationViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PolkaswapSwapConfirmationViewLayout.swift; sourceTree = ""; }; + 112609AE5629962646248BF0 /* LiquidityPoolSupplyConfirmAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyConfirmAssembly.swift; sourceTree = ""; }; 1211B723BB656770C4EA5BC2 /* WalletTransactionDetailsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletTransactionDetailsViewFactory.swift; sourceTree = ""; }; 12441689D2AF47D508D16CCF /* WalletSendConfirmViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletSendConfirmViewFactory.swift; sourceTree = ""; }; 1366336078BCA34EFB4C6FF9 /* CrowdloanContributionConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionConfirmInteractor.swift; sourceTree = ""; }; @@ -3433,6 +3443,7 @@ 3837CE5CB2D48D8A694A7EE0 /* StakingPoolCreateConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPoolCreateConfirmPresenter.swift; sourceTree = ""; }; 385FE8691EA37DE9F562B34E /* CrowdloanContributionConfirmTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionConfirmTests.swift; sourceTree = ""; }; 38B543AA1B941C76CB021051 /* Pods-fearlessTests.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-fearlessTests.dev.xcconfig"; path = "Target Support Files/Pods-fearlessTests/Pods-fearlessTests.dev.xcconfig"; sourceTree = ""; }; + 3A43F0468DEBA7500C6B23AF /* LiquidityPoolSupplyConfirmTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyConfirmTests.swift; sourceTree = ""; }; 3A93673EEA8F71E8DDA84557 /* StakingRedeemWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRedeemWireframe.swift; sourceTree = ""; }; 3ABDA8952766DE724CD078D6 /* NodeSelectionPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NodeSelectionPresenter.swift; sourceTree = ""; }; 3B0E2EDF787BF82F16663215 /* NetworkInfoViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NetworkInfoViewController.swift; sourceTree = ""; }; @@ -3460,6 +3471,7 @@ 447D03660EFFD8CF46374D85 /* NftSendConfirmViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftSendConfirmViewController.swift; sourceTree = ""; }; 447FE0DC90DF4B4D13CADE62 /* NodeSelectionInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NodeSelectionInteractor.swift; sourceTree = ""; }; 44FDDC470A6921DC2258939E /* WalletMainContainerViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletMainContainerViewLayout.swift; sourceTree = ""; }; + 4594686D10DBB7627B8C9A12 /* LiquidityPoolSupplyConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyConfirmInteractor.swift; sourceTree = ""; }; 45C8249F3F41DC0FFCF27EFF /* NftSendTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftSendTests.swift; sourceTree = ""; }; 470B64C45E547C25FCCCFC33 /* StakingMainProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingMainProtocols.swift; sourceTree = ""; }; 47759907380BE9300E54DC78 /* ExportMnemonicViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExportMnemonicViewFactory.swift; sourceTree = ""; }; @@ -3536,6 +3548,7 @@ 63B11A7ADEF107B6341C378F /* Pods-fearlessAll-fearlessIntegrationTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-fearlessAll-fearlessIntegrationTests.release.xcconfig"; path = "Target Support Files/Pods-fearlessAll-fearlessIntegrationTests/Pods-fearlessAll-fearlessIntegrationTests.release.xcconfig"; sourceTree = ""; }; 63CCCC63389A70294F816143 /* NodeSelectionProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NodeSelectionProtocols.swift; sourceTree = ""; }; 63F4BE52D0625CD8C21D2460 /* CrowdloanListViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanListViewLayout.swift; sourceTree = ""; }; + 641B699003FF648A380F7FA6 /* LiquidityPoolSupplyConfirmViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyConfirmViewLayout.swift; sourceTree = ""; }; 6503D178156C6407EC848D41 /* LiquidityPoolSupplyRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyRouter.swift; sourceTree = ""; }; 65AD15693E21C869DE1FDD17 /* UsernameSetupWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UsernameSetupWireframe.swift; sourceTree = ""; }; 66FFB904E5A83F2EFBCCBBF8 /* StakingPoolInfoTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPoolInfoTests.swift; sourceTree = ""; }; @@ -4822,9 +4835,11 @@ D101339CC1292531CC4DB0AC /* StakingUnbondSetupInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondSetupInteractor.swift; sourceTree = ""; }; D2FCA2DD3A8898D64CBC9F97 /* AccountManagementViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountManagementViewController.swift; sourceTree = ""; }; D39D54DC9992CF9CB6699AA3 /* StakingAmountViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingAmountViewFactory.swift; sourceTree = ""; }; + D430E8B808864B281A62AB43 /* LiquidityPoolSupplyConfirmViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyConfirmViewController.swift; sourceTree = ""; }; D45B7031E0809CED062C83F8 /* StakingUnbondConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondConfirmPresenter.swift; sourceTree = ""; }; D482753E0D75C5F5E0617998 /* CreateContactInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateContactInteractor.swift; sourceTree = ""; }; D4C391292F22D16427C77CD9 /* SelectValidatorsStartViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectValidatorsStartViewFactory.swift; sourceTree = ""; }; + D5121DA4DDF11ED6A6659CEB /* LiquidityPoolSupplyConfirmRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyConfirmRouter.swift; sourceTree = ""; }; D55EEBFA4A54B3AAF9F52855 /* WarningAlertViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WarningAlertViewLayout.swift; sourceTree = ""; }; D5B7937620F4339EE948EC25 /* AddCustomNodePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddCustomNodePresenter.swift; sourceTree = ""; }; D613E20E96E7BA5B8F4B9799 /* StakingRewardDetailsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRewardDetailsInteractor.swift; sourceTree = ""; }; @@ -4845,6 +4860,7 @@ DC265B5F209B038633AE0E3F /* SwapTransactionDetailProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapTransactionDetailProtocols.swift; sourceTree = ""; }; DC50F2FF6E8EBC00B56CB86D /* PolkaswapSwapConfirmationRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PolkaswapSwapConfirmationRouter.swift; sourceTree = ""; }; DCC952210C0714F32C3AE570 /* CreateContactTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateContactTests.swift; sourceTree = ""; }; + DCCD9A6B753FD1510D3DD311 /* LiquidityPoolSupplyConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyConfirmProtocols.swift; sourceTree = ""; }; DDCB1F95F35D50950D0A021E /* StakingPoolInfoProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPoolInfoProtocols.swift; sourceTree = ""; }; DEC72ACCA21471C6A9580180 /* WalletMainContainerInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletMainContainerInteractor.swift; sourceTree = ""; }; DFBFF377F35B254AB3141100 /* AssetNetworksViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetNetworksViewLayout.swift; sourceTree = ""; }; @@ -5395,6 +5411,8 @@ FA5137B529AC76EB00560EBA /* GiantsquidHistoryOperationFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GiantsquidHistoryOperationFactory.swift; sourceTree = ""; }; FA5137B629AC76EB00560EBA /* SubsquidHistoryOperationFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubsquidHistoryOperationFactory.swift; sourceTree = ""; }; FA5137B729AC76EB00560EBA /* HistoryOperationFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HistoryOperationFactory.swift; sourceTree = ""; }; + FA53D88F2C084ECA00173ADB /* LiquidityPoolSupplyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyViewModel.swift; sourceTree = ""; }; + FA53D8912C08510000173ADB /* LiquidityPoolSupplyViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyViewModelFactory.swift; sourceTree = ""; }; FA584C772AB2BCD500F6F020 /* UniversalMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalMediaView.swift; sourceTree = ""; }; FA584C792AB2BFE300F6F020 /* MediaType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaType.swift; sourceTree = ""; }; FA584C7B2AB3071E00F6F020 /* AlchemyNFTOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlchemyNFTOperationFactory.swift; sourceTree = ""; }; @@ -5614,6 +5632,8 @@ FA8810D12BDCAF350084CC4B /* shared-features-spm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "shared-features-spm"; path = "../shared-features-spm"; sourceTree = ""; }; FA8810D22BDCCF7E0084CC4B /* UserLiquidityPoolsListPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLiquidityPoolsListPresenter.swift; sourceTree = ""; }; FA8810D42BDCD19D0084CC4B /* UserLiquidityPoolsListViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLiquidityPoolsListViewModelFactory.swift; sourceTree = ""; }; + FA887A442C107A1100CA720F /* LiquidityPoolSupplyConfirmViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyConfirmViewModel.swift; sourceTree = ""; }; + FA887A462C107A4300CA720F /* LiquidityPoolSupplyConfirmViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyConfirmViewModelFactory.swift; sourceTree = ""; }; FA8C34B051607218638BA851 /* FiltersProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FiltersProtocols.swift; sourceTree = ""; }; FA8ED43228FD960F00EBB712 /* YourValidatorListFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YourValidatorListFlow.swift; sourceTree = ""; }; FA8ED43528FD983A00EBB712 /* YourValidatorListRelaychainStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YourValidatorListRelaychainStrategy.swift; sourceTree = ""; }; @@ -6152,11 +6172,9 @@ FAFFAE3F29AC84850074AF1F /* AccountManagementPresentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountManagementPresentable.swift; sourceTree = ""; }; FAFFAE4229AC84B10074AF1F /* RewardOrSlashData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RewardOrSlashData.swift; sourceTree = ""; }; FAFFAE4529AC84B10074AF1F /* SubqueryLiquidity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubqueryLiquidity.swift; sourceTree = ""; }; - FAFFAE4629AC84B10074AF1F /* SoraXNetworkingHistoryOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SoraXNetworkingHistoryOperation.swift; sourceTree = ""; }; FAFFAE4729AC84B10074AF1F /* TransactionContextKeys.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionContextKeys.swift; sourceTree = ""; }; FAFFAE4829AC84B10074AF1F /* ReferralMethodType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferralMethodType.swift; sourceTree = ""; }; FAFFAE4929AC84B10074AF1F /* SubquerySwap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubquerySwap.swift; sourceTree = ""; }; - FAFFAE4A29AC84B10074AF1F /* TxHistoryItem+WalletRemoteHistoryItemProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TxHistoryItem+WalletRemoteHistoryItemProtocol.swift"; sourceTree = ""; }; FAFFAE4B29AC84B10074AF1F /* SubqueryCreatePoolLiquidity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubqueryCreatePoolLiquidity.swift; sourceTree = ""; }; FAFFAE4C29AC84B10074AF1F /* SubqueryReferral.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubqueryReferral.swift; sourceTree = ""; }; FAFFAE4D29AC84B10074AF1F /* KmmCallCodingPath.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KmmCallCodingPath.swift; sourceTree = ""; }; @@ -7323,6 +7341,21 @@ path = WalletOption; sourceTree = ""; }; + 3362664DE1DD71BB75722C2D /* LiquidityPoolSupplyConfirm */ = { + isa = PBXGroup; + children = ( + FA887A432C107A0200CA720F /* ViewModel */, + DCCD9A6B753FD1510D3DD311 /* LiquidityPoolSupplyConfirmProtocols.swift */, + D5121DA4DDF11ED6A6659CEB /* LiquidityPoolSupplyConfirmRouter.swift */, + 06F6B892F62579DE761073CA /* LiquidityPoolSupplyConfirmPresenter.swift */, + 4594686D10DBB7627B8C9A12 /* LiquidityPoolSupplyConfirmInteractor.swift */, + D430E8B808864B281A62AB43 /* LiquidityPoolSupplyConfirmViewController.swift */, + 641B699003FF648A380F7FA6 /* LiquidityPoolSupplyConfirmViewLayout.swift */, + 112609AE5629962646248BF0 /* LiquidityPoolSupplyConfirmAssembly.swift */, + ); + path = LiquidityPoolSupplyConfirm; + sourceTree = ""; + }; 3722354DA3C59896C49B5794 /* StakingRewardDetails */ = { isa = PBXGroup; children = ( @@ -7713,6 +7746,7 @@ 9468F26496DF90FEE7016AA3 /* LiquidityPoolsOverview */, 9439C16432098735E8F4C122 /* LiquidityPoolDetails */, 04265CAB3B55A71F49337293 /* LiquidityPoolSupply */, + 749EAA035EECE4D63C56C358 /* LiquidityPoolSupplyConfirm */, ); path = Modules; sourceTree = ""; @@ -7760,6 +7794,14 @@ path = ControllerAccountConfirmation; sourceTree = ""; }; + 749EAA035EECE4D63C56C358 /* LiquidityPoolSupplyConfirm */ = { + isa = PBXGroup; + children = ( + 3A43F0468DEBA7500C6B23AF /* LiquidityPoolSupplyConfirmTests.swift */, + ); + path = LiquidityPoolSupplyConfirm; + sourceTree = ""; + }; 77D9A8552D1961A00A3683BE /* SelectValidatorsConfirm */ = { isa = PBXGroup; children = ( @@ -11968,6 +12010,7 @@ EE3CBEFADE55F44DE05DCEF2 /* LiquidityPoolSupply */ = { isa = PBXGroup; children = ( + FA53D88E2C084EB800173ADB /* ViewModel */, 8FB7091F743BBACC09553298 /* LiquidityPoolSupplyProtocols.swift */, 6503D178156C6407EC848D41 /* LiquidityPoolSupplyRouter.swift */, 31F848482B2AD7D6831B0CCE /* LiquidityPoolSupplyPresenter.swift */, @@ -12579,6 +12622,7 @@ FA1D51D42BCFD410001353E7 /* LiquidityPools */ = { isa = PBXGroup; children = ( + 3362664DE1DD71BB75722C2D /* LiquidityPoolSupplyConfirm */, EE3CBEFADE55F44DE05DCEF2 /* LiquidityPoolSupply */, 478C62A42D572C8647512722 /* LiquidityPoolDetails */, 675669EAE3DAF27168F1B390 /* LiquidityPoolsOverview */, @@ -13483,6 +13527,15 @@ path = Main; sourceTree = ""; }; + FA53D88E2C084EB800173ADB /* ViewModel */ = { + isa = PBXGroup; + children = ( + FA53D88F2C084ECA00173ADB /* LiquidityPoolSupplyViewModel.swift */, + FA53D8912C08510000173ADB /* LiquidityPoolSupplyViewModelFactory.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; FA584C7F2AB308AE00F6F020 /* Etherscan */ = { isa = PBXGroup; children = ( @@ -13915,7 +13968,6 @@ children = ( FA2DF9962A8C97170047F440 /* NFT */, FA6C175629935DC700A55254 /* BlockExplorer */, - 0702B35129791357003519F5 /* SoraHistoryOperationFactory.swift */, FA2FC81C28B3809600CC0A42 /* StakingPoolOperationFactory.swift */, 076D9D462940A24A002762E3 /* Polkaswap */, ); @@ -13977,6 +14029,15 @@ path = StakingAccountSubscription; sourceTree = ""; }; + FA887A432C107A0200CA720F /* ViewModel */ = { + isa = PBXGroup; + children = ( + FA887A442C107A1100CA720F /* LiquidityPoolSupplyConfirmViewModel.swift */, + FA887A462C107A4300CA720F /* LiquidityPoolSupplyConfirmViewModelFactory.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; FA8ED43128FD8D6200EBB712 /* Flows */ = { isa = PBXGroup; children = ( @@ -15542,11 +15603,9 @@ isa = PBXGroup; children = ( FAFFAE4529AC84B10074AF1F /* SubqueryLiquidity.swift */, - FAFFAE4629AC84B10074AF1F /* SoraXNetworkingHistoryOperation.swift */, FAFFAE4729AC84B10074AF1F /* TransactionContextKeys.swift */, FAFFAE4829AC84B10074AF1F /* ReferralMethodType.swift */, FAFFAE4929AC84B10074AF1F /* SubquerySwap.swift */, - FAFFAE4A29AC84B10074AF1F /* TxHistoryItem+WalletRemoteHistoryItemProtocol.swift */, FAFFAE4B29AC84B10074AF1F /* SubqueryCreatePoolLiquidity.swift */, FAFFAE4C29AC84B10074AF1F /* SubqueryReferral.swift */, FAFFAE4D29AC84B10074AF1F /* KmmCallCodingPath.swift */, @@ -16268,6 +16327,7 @@ FA86443E2768439900956D8E /* ContainerProtocols.swift in Sources */, 07F818012AD55D3600B87358 /* WalletConnectSessionCoordinator.swift in Sources */, 84FB1F69252694B600E0242B /* HistoryInfo.swift in Sources */, + FA53D8902C084ECA00173ADB /* LiquidityPoolSupplyViewModel.swift in Sources */, 84C74361251E4B5E009576C6 /* FeeType.swift in Sources */, 8428765224ADDE0200D91AD8 /* ProfilePresenter.swift in Sources */, F4F22987260DC4F300ACFDB8 /* StakingRewardDetailsSimpleLabelViewModel.swift in Sources */, @@ -16972,7 +17032,6 @@ FAD428942A834A8E001D6A16 /* TopViewControllerHelper.swift in Sources */, 84CE69E82566750D00559427 /* ByteLengthProcessor.swift in Sources */, 8467FD4924ED64A5005D486C /* ProfileDetailsTableViewCell.swift in Sources */, - FAFFAE7529AC84B10074AF1F /* SoraXNetworkingHistoryOperation.swift in Sources */, 8494D87A2525350000614D8F /* SubscanStatusData.swift in Sources */, FA2FC7FF28B3807C00CC0A42 /* StakingPoolJoinChoosePoolAssembly.swift in Sources */, 842876A724AE049B00D91AD8 /* SelectionListProtocols.swift in Sources */, @@ -17410,6 +17469,7 @@ FA86442C27674A8600956D8E /* WalletTransactionHistoryViewState.swift in Sources */, AEB99798261310D5005C60A5 /* StoriesProgressBar.swift in Sources */, AEE5FB1626457AA9002B8FDC /* StakingRewardDestSetupPresenter.swift in Sources */, + FA887A452C107A1100CA720F /* LiquidityPoolSupplyConfirmViewModel.swift in Sources */, FA256987274CE5B700875A53 /* SHA256.swift in Sources */, 8468B86E24F63D1400B76BC6 /* AddAccount+OnboardingMainWireframe.swift in Sources */, FAA0137028DA12E3000A5230 /* StakingRedeemConfirmationParachainViewModelState.swift in Sources */, @@ -18054,6 +18114,7 @@ FA004891282CCA400032FF49 /* SelectValidatorsStartParachainStrategy.swift in Sources */, FAFFAEA129AC84D30074AF1F /* LinkDecorator.swift in Sources */, FA5137AD29AC6F2F00560EBA /* PolkaswapDisclaimerViewController.swift in Sources */, + FA887A472C107A4300CA720F /* LiquidityPoolSupplyConfirmViewModelFactory.swift in Sources */, 849632072555CE9D00B8E316 /* ExportSeedData.swift in Sources */, FAD428F52A86567F001D6A16 /* BackupPasswordProtocols.swift in Sources */, FACD429D2A5BE811009975AA /* MultiassetV9MigrationPolicy.swift in Sources */, @@ -18086,7 +18147,6 @@ FA34EEDB2B98723C0042E73E /* OnboardingRouter.swift in Sources */, 84F47D532666F13C00F7647A /* KaruraVerifyInfo.swift in Sources */, FA936BD6286C66B70059B97A /* StakingRebondConfirmationParachainViewModelFactory.swift in Sources */, - FAFFAE7929AC84B10074AF1F /* TxHistoryItem+WalletRemoteHistoryItemProtocol.swift in Sources */, FA38C997275E0AB8005C5577 /* AssetPriceViewModelFactory.swift in Sources */, AE9EF273260A82FB0026910A /* StorySlide.swift in Sources */, FA34EEAE2B9872180042E73E /* NominationPoolsPoolMembersRequest.swift in Sources */, @@ -18159,6 +18219,7 @@ 84B64E3F2704567700914E88 /* RelaychainStakingLocalStorageSubscriber.swift in Sources */, BD571417BD18C711B76E1D62 /* ExportSeedWireframe.swift in Sources */, 8463A73825E3AA47003B8160 /* AccountInfo.swift in Sources */, + FA53D8922C08510000173ADB /* LiquidityPoolSupplyViewModelFactory.swift in Sources */, ABA3D873BBECB7F4BD670872 /* ExportSeedPresenter.swift in Sources */, FA5DD0E3278EFE2500967047 /* WalletTransactionHistoryTableSectionHeader.swift in Sources */, FAA0134728DA12CD000A5230 /* StakingUnbondSetupPoolStrategy.swift in Sources */, @@ -18743,7 +18804,6 @@ 3B9314DE6AFC01CA7EF0DAAA /* SelectValidatorsConfirmFlow.swift in Sources */, 07DFA472289B8D8E0035A8AB /* EducationStoriesViewState.swift in Sources */, 134AFD616BE52A1AE290EEF7 /* StakingBalanceFlow.swift in Sources */, - 0702B35229791357003519F5 /* SoraHistoryOperationFactory.swift in Sources */, FAC0BBE4291D0EB000E6F106 /* SelectAssetProtocols.swift in Sources */, 78314B269F1CF1A499DE5CCB /* StakingBondMoreFlow.swift in Sources */, FA584C882AB3F48200F6F020 /* AlchemyNftFetchingService.swift in Sources */, @@ -18970,6 +19030,13 @@ F31469BD18062A4A008FE39E /* LiquidityPoolSupplyViewController.swift in Sources */, 5E8504507116E0177D70314B /* LiquidityPoolSupplyViewLayout.swift in Sources */, DCE13AA9F3BA0EB54F793017 /* LiquidityPoolSupplyAssembly.swift in Sources */, + 6FAC7E8F0DACB3F2AA0BE825 /* LiquidityPoolSupplyConfirmProtocols.swift in Sources */, + FBE4702B7F95484235386631 /* LiquidityPoolSupplyConfirmRouter.swift in Sources */, + 1029947E04F64B616E252949 /* LiquidityPoolSupplyConfirmPresenter.swift in Sources */, + 06826AA6DBAEA5BBF45BB5CA /* LiquidityPoolSupplyConfirmInteractor.swift in Sources */, + 24E6794278FEC01DFFE7C69C /* LiquidityPoolSupplyConfirmViewController.swift in Sources */, + 74A39BC366D85CC5FCA78579 /* LiquidityPoolSupplyConfirmViewLayout.swift in Sources */, + 991DA2DE1E8A45B0A8B187CD /* LiquidityPoolSupplyConfirmAssembly.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -19143,6 +19210,7 @@ 60C22E112CA857A2EA5A129E /* LiquidityPoolsOverviewTests.swift in Sources */, 6D0E50CEBCB73C23A75A7F46 /* LiquidityPoolDetailsTests.swift in Sources */, D8C33C4064C3B2F30BB478A5 /* LiquidityPoolSupplyTests.swift in Sources */, + B40863AA7377BFE5F8A30259 /* LiquidityPoolSupplyConfirmTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index c5987471bd..6d49df0a6e 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -113,8 +113,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/ashleymills/Reachability.swift", "state" : { - "revision" : "57da4b1270cab7c2228919eabc0e4e1bf93e48ea", - "version" : "5.2.2" + "revision" : "7cbd73f46a7dfaeca079e18df7324c6de6d1834a", + "version" : "5.2.3" } }, { diff --git a/fearless/Common/Network/BlockExplorer/Subquery/Sora/SoraXNetworkingHistoryOperation.swift b/fearless/Common/Network/BlockExplorer/Subquery/Sora/SoraXNetworkingHistoryOperation.swift deleted file mode 100644 index feb9aa678f..0000000000 --- a/fearless/Common/Network/BlockExplorer/Subquery/Sora/SoraXNetworkingHistoryOperation.swift +++ /dev/null @@ -1,116 +0,0 @@ -import RobinHood -import XNetworking -import SSFModels - -final class SoraXNetworkingHistoryOperation: BaseOperation { - private let httpProvider: SoramitsuHttpClientProviderImpl - private let soraNetworkClient: SoramitsuNetworkClient - private let subQueryClient: SubQueryClientForSoraWallet - private let filters: [WalletTransactionHistoryFilter] - private let chainAsset: ChainAsset - private let address: String - private let count: Int - private let page: Int - private let url: URL - - init( - chainAsset: ChainAsset, - filters: [WalletTransactionHistoryFilter], - url: URL, - address: String, - count: Int, - page: Int - ) { - httpProvider = SoramitsuHttpClientProviderImpl() - soraNetworkClient = SoramitsuNetworkClient( - timeout: 60000, - logging: true, - provider: httpProvider - ) - - subQueryClient = SubQueryClientForSoraWalletFactory() - .create( - soramitsuNetworkClient: soraNetworkClient, - baseUrl: url.absoluteString, - pageSize: Int32(count) - ) - self.filters = filters - self.chainAsset = chainAsset - self.url = url - self.address = address - self.count = count - self.page = page - - super.init() - } - - override public func main() { - super.main() - - if isCancelled { - return - } - - if result != nil { - return - } - - let semaphore = DispatchSemaphore(value: 0) - let filter = prepareFilter(for: chainAsset, filters: filters) - - // TODO: delete after kotlin 1.7.0 released, now we should call method from main queue - DispatchQueue.main.async { - self.subQueryClient.getTransactionHistoryPaged( - address: self.address, - networkName: self.chainAsset.chain.name, - page: Int64(self.page), - url: self.url.absoluteString, - filter: filter, - completionHandler: { [self] requestResult, _ in - guard let data = requestResult as? ResultType else { return } - - if self.isCancelled { - return - } - semaphore.signal() - self.result = .success(data) - } - ) - } - - semaphore.wait() - } - - private func prepareFilter( - for chainAsset: ChainAsset, - filters: [WalletTransactionHistoryFilter] - ) -> ((TxHistoryItem) -> KotlinBoolean)? { - let currencyId = chainAsset.asset.currencyId - let filter: ((TxHistoryItem) -> KotlinBoolean)? = { item in - let callPath = KmmCallCodingPath(moduleName: item.module, callName: item.method) - - if callPath.isTransfer { - if !filters.contains(where: { $0.type == .transfer && $0.selected }) { - return KotlinBoolean(value: false) - } else { - let isAssetId = item.data?.first { $0.paramName == "assetId" }?.paramValue == currencyId - return KotlinBoolean(value: isAssetId) - } - } - - if callPath.isSwap { - if !filters.contains(where: { $0.type == .swap && $0.selected }) { - return KotlinBoolean(value: false) - } else { - let isBaseAssetId = item.data?.first { $0.paramName == "baseAssetId" }?.paramValue == currencyId - let isTargetAssetId = item.data?.first { $0.paramName == "targetAssetId" }?.paramValue == currencyId - return KotlinBoolean(value: isBaseAssetId || isTargetAssetId) - } - } - - return KotlinBoolean(value: false) - } - - return currencyId != nil ? filter : nil - } -} diff --git a/fearless/Common/Network/BlockExplorer/Subquery/Sora/TxHistoryItem+WalletRemoteHistoryItemProtocol.swift b/fearless/Common/Network/BlockExplorer/Subquery/Sora/TxHistoryItem+WalletRemoteHistoryItemProtocol.swift deleted file mode 100644 index 703b2ad2df..0000000000 --- a/fearless/Common/Network/BlockExplorer/Subquery/Sora/TxHistoryItem+WalletRemoteHistoryItemProtocol.swift +++ /dev/null @@ -1,355 +0,0 @@ -import Foundation -import XNetworking -import BigInt - -import IrohaCrypto -import SSFUtils -import SSFModels - -extension TxHistoryItem: WalletRemoteHistoryItemProtocol { - var identifier: String { id } - var itemBlockNumber: UInt64 { 0 } - var itemExtrinsicIndex: UInt16 { 0 } - var extrinsicHash: String? { id } - var itemTimestamp: Int64 { - Int64(timestamp) ?? 0 - } - - var label: WalletRemoteHistorySourceLabel { - .extrinsics - } - - func createTransactionForAddress( - _ address: String, - chain: SSFModels.ChainModel, - asset: AssetModel - ) -> AssetTransactionData { - var dict = [String: JSON]() - - for element in data ?? [] { - dict[element.paramName] = JSON.stringValue(element.paramValue) - } - - let json: JSON = .dictionaryValue(dict) - - if let rewardOrSlash = try? json.map(to: SubqueryRewardOrSlash.self) { - return createTransactionForRewardOrSlash(rewardOrSlash, asset: asset) - } - - if let transfer = try? json.map(to: SoraSubqueryTransfer.self) { - return createTransactionForTransfer( - transfer, - address: address, - chain: chain, - asset: asset - ) - } - - if let swap = try? json.map(to: SubquerySwap.self) { - return createTransactionForSwap(swap) - } - - if let liquidity = try? json.map(to: SubqueryLiquidity.self) { - return createTransactionForLiquidity( - liquidity, - asset: asset - ) - } - - if let referral = try? json.map(to: SubqueryReferral.self) { - return createTransactionForReferral( - referral, - address: address, - asset: asset - ) - } - - if let depositLiquidityData = createDepositLiquidityAssetTransaction( - asset: asset - ) { - return depositLiquidityData - } - - return createUnknownAssetTransaction(asset: asset) - } - - // MARK: - Private methods - - private func createUnknownAssetTransaction( - asset: AssetModel - ) -> AssetTransactionData { - AssetTransactionData( - transactionId: identifier, - status: .pending, - assetId: asset.identifier, - peerId: "", - peerFirstName: nil, - peerLastName: nil, - peerName: "", - details: "", - amount: AmountDecimal(value: 0), - fees: [], - timestamp: itemTimestamp, - type: TransactionType.unused.rawValue, - reason: nil, - context: nil - ) - } - - private func createTransactionForTransfer( - _ transfer: SoraSubqueryTransfer, - address: String, - chain: SSFModels.ChainModel, - asset: AssetModel - ) -> AssetTransactionData { - let status = success ? AssetTransactionStatus.commited : AssetTransactionStatus.rejected - - let peerAddress = transfer.sender == address ? transfer.receiver : transfer.sender - - let peerAccountId = try? AddressFactory.accountId( - from: address, - chain: chain - ) - - let amountDecimal = Decimal(string: transfer.amount) ?? .zero - let feeDecimal = Decimal(string: networkFee) ?? .zero - - let fee = AssetTransactionFee( - identifier: asset.identifier, - assetId: asset.identifier, - amount: AmountDecimal(value: feeDecimal), - context: nil - ) - - let type = transfer.sender == address ? TransactionType.outgoing : TransactionType.incoming - - let context: [String: String]? - - if let extrinsicHash = self.extrinsicHash { - context = [TransactionContextKeys.extrinsicHash: extrinsicHash] - } else { - context = nil - } - - return AssetTransactionData( - transactionId: identifier, - status: status, - assetId: transfer.assetId, - peerId: peerAccountId?.toHex() ?? "", - peerFirstName: nil, - peerLastName: nil, - peerName: peerAddress, - details: "", - amount: AmountDecimal(value: amountDecimal), - fees: [fee], - timestamp: itemTimestamp, - type: type.rawValue, - reason: nil, - context: context - ) - } - - private func createTransactionForSwap( - _ swap: SubquerySwap - ) -> AssetTransactionData { - let status: AssetTransactionStatus = success ? .commited : .rejected - let amountDecimal = Decimal(string: swap.targetAssetAmount) ?? .zero - - let feeDecimal = Decimal(string: networkFee) ?? .zero - let fee = AssetTransactionFee( - identifier: swap.targetAssetId, - assetId: swap.targetAssetId, - amount: AmountDecimal(value: feeDecimal), - context: nil - ) - - // Selected market: empty: smart; else first? - return AssetTransactionData( - transactionId: identifier, - status: status, - assetId: swap.targetAssetId, - peerId: swap.baseAssetId, - peerFirstName: nil, - peerLastName: nil, - peerName: swap.selectedMarket, - details: swap.baseAssetAmount, - amount: AmountDecimal(value: amountDecimal), - fees: [fee], - timestamp: itemTimestamp, - type: TransactionType.swap.rawValue, - reason: nil, - context: nil - ) - } - - private func createTransactionForRewardOrSlash( - _ rewardOrSlash: SubqueryRewardOrSlash, - asset: AssetModel - ) -> AssetTransactionData { - let amount = Decimal.fromSubstrateAmount( - BigUInt(string: rewardOrSlash.amount) ?? 0, - precision: Int16(asset.precision) - ) ?? 0.0 - - let type = rewardOrSlash.isReward ? TransactionType.reward.rawValue : TransactionType.slash.rawValue - - let validatorAddress = rewardOrSlash.validator ?? "" - - let context: [String: String]? - - if let era = rewardOrSlash.era { - context = [TransactionContextKeys.era: String(era)] - } else { - context = nil - } - - return AssetTransactionData( - transactionId: identifier, - status: .commited, - assetId: asset.identifier, - peerId: validatorAddress, - peerFirstName: nil, - peerLastName: nil, - peerName: validatorAddress, - details: "", - amount: AmountDecimal(value: amount), - fees: [], - timestamp: itemTimestamp, - type: type, - reason: nil, - context: context - ) - } - - private func createDepositLiquidityAssetTransaction( - asset: AssetModel - ) -> AssetTransactionData? { - var dict = [String: JSON]() - - if let data = nestedData?.first(where: { - $0.module == "poolXYK" && $0.method == "depositLiquidity" - } - ) { - for element in data.data { - dict[element.paramName] = JSON.stringValue(element.paramValue) - } - - let json: JSON = .dictionaryValue(dict) - if let extrinsic = try? json.map(to: SubqueryCreatePoolLiquidity.self) { - return createTransactionForCreateLiquidityPool( - extrinsic, - asset: asset - ) - } - } - return nil - } - - private func createTransactionForLiquidity( - _ liquidity: SubqueryLiquidity, - asset: AssetModel - ) -> AssetTransactionData { - let status: AssetTransactionStatus = success ? .commited : .rejected - let amountDecimal = Decimal(string: liquidity.targetAssetAmount) ?? .zero - let fee = AssetTransactionFee( - identifier: asset.identifier, - assetId: asset.identifier, - amount: AmountDecimal(value: Decimal(string: networkFee) ?? .zero), - context: nil - ) - - return AssetTransactionData( - transactionId: identifier, - status: status, - assetId: liquidity.targetAssetId, - peerId: liquidity.baseAssetId, - peerFirstName: nil, - peerLastName: nil, - peerName: liquidity.baseAssetId, - details: liquidity.baseAssetAmount, - amount: AmountDecimal(value: amountDecimal), - fees: [fee], - timestamp: itemTimestamp, - type: TransactionType.unused.rawValue, - reason: nil, - context: nil - ) - } - - private func createTransactionForCreateLiquidityPool( - _ liquidity: SubqueryCreatePoolLiquidity, - asset: AssetModel - ) -> AssetTransactionData { - let status: AssetTransactionStatus = success ? .commited : .rejected - let amountDecimal = Decimal(string: liquidity.inputADesired) ?? .zero - let fee = AssetTransactionFee( - identifier: asset.identifier, - assetId: asset.identifier, - amount: AmountDecimal(value: Decimal(string: networkFee) ?? .zero), - context: nil - ) - - return AssetTransactionData( - transactionId: identifier, - status: status, - assetId: liquidity.inputAssetA, - peerId: liquidity.inputAssetB, - peerFirstName: nil, - peerLastName: nil, - peerName: liquidity.inputAssetB, - details: liquidity.inputBDesired, - amount: AmountDecimal(value: amountDecimal), - fees: [fee], - timestamp: itemTimestamp, - type: TransactionLiquidityType.deposit.rawValue, - reason: nil, - context: nil - ) - } - - private func createTransactionForReferral( - _ referral: SubqueryReferral, - address: String, - asset: AssetModel - ) -> AssetTransactionData { - let status: AssetTransactionStatus = success ? .commited : .rejected - let amountDecimal = Decimal(string: referral.amount ?? "") ?? .zero - let fee = AssetTransactionFee( - identifier: asset.identifier, - assetId: asset.identifier, - amount: AmountDecimal(value: Decimal(string: networkFee) ?? .zero), - context: nil - ) - - var type = ReferralMethodType(fromRawValue: method) - - if type == .setReferrer, referral.to == address { - type = .setReferral - } - - let context: [String: String]? = [ - TransactionContextKeys.blockHash: blockHash, - TransactionContextKeys.sender: referral.from, - TransactionContextKeys.referral: referral.from, - TransactionContextKeys.referrer: referral.to, - TransactionContextKeys.referralTransactionType: type.rawValue - ] - - return AssetTransactionData( - transactionId: identifier, - status: status, - assetId: asset.identifier, - peerId: referral.to, - peerFirstName: nil, - peerLastName: nil, - peerName: nil, - details: "", - amount: AmountDecimal(value: amountDecimal), - fees: [fee], - timestamp: itemTimestamp, - type: TransactionType.unused.rawValue, - reason: nil, - context: context - ) - } -} diff --git a/fearless/Common/ViewModel/Amount/MoneyPresentable.swift b/fearless/Common/ViewModel/Amount/MoneyPresentable.swift index 7e0772012f..6315a487be 100644 --- a/fearless/Common/ViewModel/Amount/MoneyPresentable.swift +++ b/fearless/Common/ViewModel/Amount/MoneyPresentable.swift @@ -124,9 +124,9 @@ extension MoneyPresentable { ) } - if result.isEmpty { - result = MoneyPresentableConstants.singleZero - } +// if result.isEmpty { +// result = MoneyPresentableConstants.singleZero +// } return result } diff --git a/fearless/CoreLayer/OperationFactory/SoraHistoryOperationFactory.swift b/fearless/CoreLayer/OperationFactory/SoraHistoryOperationFactory.swift deleted file mode 100644 index ac9550362f..0000000000 --- a/fearless/CoreLayer/OperationFactory/SoraHistoryOperationFactory.swift +++ /dev/null @@ -1,218 +0,0 @@ - -import RobinHood -import IrohaCrypto -import SSFUtils -import XNetworking -import BigInt -import SSFModels - -final class SoraHistoryOperationFactory: HistoryOperationFactoryProtocol { - private let txStorage: AnyDataProviderRepository - - init(txStorage: AnyDataProviderRepository) { - self.txStorage = txStorage - } - - // MARK: - Public methods - - func fetchTransactionHistoryOperation( - asset: AssetModel, - chain: SSFModels.ChainModel, - address: String, - filters: [WalletTransactionHistoryFilter], - pagination: Pagination - ) -> CompoundOperationWrapper { - let chainAsset = ChainAsset(chain: chain, asset: asset) - let historyContext = TransactionHistoryContext( - context: pagination.context ?? [:], - defaultRow: pagination.count - ).byApplying(filters: filters) - - let isCompletedString = pagination.context?[TransactionHistoryContext.isComplete] ?? "" - let isCompleted = Bool(isCompletedString) ?? false - guard !isCompleted else { - let pageData = AssetTransactionPageData( - transactions: [], - context: nil - ) - - let operation = BaseOperation() - operation.result = .success(pageData) - return CompoundOperationWrapper(targetOperation: operation) - } - - var cursor = 1 - if let stringCursor = pagination.context?[TransactionHistoryContext.cursor] { - cursor = Int(stringCursor) ?? 1 - } - - let remoteHistoryWrapper: CompoundOperationWrapper - if let baseUrl = chain.externalApi?.history?.url { - remoteHistoryWrapper = createSoraOperationWrapper( - chainAsset: chainAsset, - baseURL: baseUrl, - for: historyContext, - address: address, - count: pagination.count, - cursor: cursor, - filters: filters - ) - } else { - let result = WalletRemoteHistoryData(historyItems: [], context: historyContext) - remoteHistoryWrapper = CompoundOperationWrapper.createWithResult(result) - } - - var dependencies = remoteHistoryWrapper.allOperations - - let localFetchOperation: BaseOperation<[TransactionHistoryItem]>? - - if pagination.context == nil { - let operation = txStorage.fetchAllOperation(with: RepositoryFetchOptions()) - dependencies.append(operation) - - remoteHistoryWrapper.allOperations.forEach { operation.addDependency($0) } - - localFetchOperation = operation - } else { - localFetchOperation = nil - } - - let mergeOperation = createHistoryMergeOperation( - dependingOn: remoteHistoryWrapper, - localOperation: localFetchOperation, - asset: asset, - chain: chain, - address: address - ) - - dependencies.forEach { mergeOperation.addDependency($0) } - - dependencies.append(mergeOperation) - - if pagination.context == nil { - let clearOperation = txStorage.saveOperation({ [] }, { - let mergeResult = try mergeOperation - .extractResultData(throwing: BaseOperationError.parentOperationCancelled) - return mergeResult.identifiersToRemove - }) - - dependencies.append(clearOperation) - clearOperation.addDependency(mergeOperation) - } - - let mapOperation = createHistoryMapOperation( - dependingOn: mergeOperation, - remoteOperation: remoteHistoryWrapper.targetOperation - ) - - dependencies.forEach { mapOperation.addDependency($0) } - - return CompoundOperationWrapper( - targetOperation: mapOperation, - dependencies: dependencies - ) - } - - // MARK: - Private methods - - private func createSoraOperationWrapper( - chainAsset: ChainAsset, - baseURL: URL, - for context: TransactionHistoryContext, - address: String, - count: Int, - cursor: Int, - filters: [WalletTransactionHistoryFilter] - ) -> CompoundOperationWrapper { - let queryOperation = SoraXNetworkingHistoryOperation>( - chainAsset: chainAsset, - filters: filters, - url: baseURL, - address: address, - count: count, - page: cursor - ) - - let mappingOperation = ClosureOperation { - guard let response = try? queryOperation.extractNoCancellableResultData() else { - return WalletRemoteHistoryData(historyItems: [], context: context) - } - - let isCompleted = response.endReached - let items = (response.items as? [WalletRemoteHistoryItemProtocol]) ?? [] - var updatedContext = context - updatedContext.soraCursor = (context.soraCursor ?? 1) + 1 - updatedContext.soraIsComplete = isCompleted - - return WalletRemoteHistoryData( - historyItems: items, - context: updatedContext - ) - } - - mappingOperation.addDependency(queryOperation) - - return CompoundOperationWrapper(targetOperation: mappingOperation, dependencies: [queryOperation]) - } - - private func createHistoryMergeOperation( - dependingOn remoteOperation: CompoundOperationWrapper?, - localOperation: BaseOperation<[TransactionHistoryItem]>?, - asset: AssetModel, - chain: SSFModels.ChainModel, - address: String - ) -> BaseOperation { - ClosureOperation { - let remoteTransactions = try remoteOperation? - .targetOperation - .extractNoCancellableResultData() - .historyItems ?? [] - - if let localTransactions = try localOperation?.extractNoCancellableResultData() - .filter({ $0.sender == address || $0.receiver == address }), - !localTransactions.isEmpty { - let manager = TransactionHistoryMergeManager( - address: address, - chain: chain, - asset: asset - ) - return manager.merge( - subscanItems: remoteTransactions, - localItems: localTransactions - ) - } else { - let transactions: [AssetTransactionData] = remoteTransactions.map { item in - item.createTransactionForAddress( - address, - chain: chain, - asset: asset - ) - } - - return TransactionHistoryMergeResult( - historyItems: transactions, - identifiersToRemove: [] - ) - } - } - } - - private func createHistoryMapOperation( - dependingOn mergeOperation: BaseOperation, - remoteOperation: BaseOperation - ) -> BaseOperation { - ClosureOperation { - let mergeResult = try mergeOperation.extractNoCancellableResultData() - let newHistoryContext = try remoteOperation.extractNoCancellableResultData().context - - let contextDict = [TransactionHistoryContext.isComplete: String(newHistoryContext.soraIsComplete), - TransactionHistoryContext.cursor: String(newHistoryContext.soraCursor ?? 1)] - let context = !newHistoryContext.isComplete ? contextDict : nil - - return AssetTransactionPageData( - transactions: mergeResult.historyItems, - context: context - ) - } - } -} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift index 9ca15fedce..2ae5f9b23e 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift @@ -201,8 +201,8 @@ final class LiquidityPoolDetailsViewLayout: UIView { private static func makeRowView() -> TitleMultiValueView { let view = TitleMultiValueView() - view.titleLabel.font = .p1Paragraph - view.titleLabel.textColor = R.color.colorWhite() + view.titleLabel.font = .h6Title + view.titleLabel.textColor = R.color.colorWhite50() view.valueTop.font = .p1Paragraph view.valueTop.textColor = R.color.colorWhite() view.valueBottom.font = .p2Paragraph diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyAssembly.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyAssembly.swift index 9f8e7c3fcb..a24fb4c07c 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyAssembly.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyAssembly.swift @@ -40,7 +40,7 @@ final class LiquidityPoolSupplyAssembly { let interactor = LiquidityPoolSupplyInteractor(lpOperationService: lpOperationService, lpDataService: lpDataService, liquidityPair: liquidityPair, priceLocalSubscriber: PriceLocalStorageSubscriberImpl.shared, chain: chain, accountInfoSubscriptionAdapter: accountInfoSubscriptionAdapter) let router = LiquidityPoolSupplyRouter() - + let dataValidatingFactory = SendDataValidatingFactory(presentable: router) let presenter = LiquidityPoolSupplyPresenter( interactor: interactor, router: router, @@ -49,7 +49,8 @@ final class LiquidityPoolSupplyAssembly { chain: chain, logger: Logger.shared, wallet: wallet, - dataValidatingFactory: SendDataValidatingFactory(presentable: router) + dataValidatingFactory: dataValidatingFactory, + viewModelFactory: LiquidityPoolSupplyViewModelFactoryDefault() ) let view = LiquidityPoolSupplyViewController( @@ -57,6 +58,8 @@ final class LiquidityPoolSupplyAssembly { localizationManager: localizationManager ) + dataValidatingFactory.view = view + return (view, presenter) } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyInteractor.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyInteractor.swift index 69ab71ae49..3a8a366f25 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyInteractor.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyInteractor.swift @@ -11,6 +11,8 @@ protocol LiquidityPoolSupplyInteractorOutput: AnyObject { func didReceiveFeeError(_ error: Error) func didReceivePricesData(result: Result<[PriceData], Error>) func didReceiveAccountInfo(result: Result, for chainAsset: ChainAsset) + func didReceivePoolAPY(apyInfo: PoolApyInfo?) + func didReceivePoolApyError(error: Error) } final class LiquidityPoolSupplyInteractor { @@ -82,6 +84,7 @@ extension LiquidityPoolSupplyInteractor: LiquidityPoolSupplyInteractorInput { fetchDexId() subscribeToPrices() subscribeToAccountInfo() + fetchApy() } func estimateFee(supplyLiquidityInfo: SupplyLiquidityInfo) { @@ -94,6 +97,26 @@ extension LiquidityPoolSupplyInteractor: LiquidityPoolSupplyInteractorInput { } } } + + func fetchApy() { + guard let reservesId = liquidityPair.reservesId else { + return + } + + Task { + do { + let apy = try await lpDataService.fetchPoolsAPY() + let address = try AddressFactory.address(for: Data(hex: reservesId), chain: chain) + await MainActor.run { + output?.didReceivePoolAPY(apyInfo: apy.first(where: { $0.poolId == address })) + } + } catch { + await MainActor.run { + output?.didReceivePoolApyError(error: error) + } + } + } + } } // MARK: - PriceLocalStorageSubscriber diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift index 0fdbb902b8..1c2acd7c71 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift @@ -3,6 +3,7 @@ import SoraFoundation import SSFModels import SSFPools import BigInt +import SSFPolkaswap protocol LiquidityPoolSupplyViewInput: ControllerBackedProtocol { func didReceiveSwapFrom(viewModel: AssetBalanceViewModelProtocol?) @@ -12,6 +13,8 @@ protocol LiquidityPoolSupplyViewInput: ControllerBackedProtocol { func didReceiveNetworkFee(fee: BalanceViewModelProtocol?) func setButtonLoadingState(isLoading: Bool) func didUpdating() + func didReceiveViewModel(_ viewModel: LiquidityPoolSupplyViewModel) + func didReceiveSwapQuoteReady() } protocol LiquidityPoolSupplyInteractorInput: AnyObject { @@ -38,6 +41,7 @@ final class LiquidityPoolSupplyPresenter { private let logger: LoggerProtocol private let liquidityPair: LiquidityPair private let chain: ChainModel + private let viewModelFactory: LiquidityPoolSupplyViewModelFactory private let wallet: MetaAccountModel private var swapFromChainAsset: ChainAsset? @@ -45,11 +49,12 @@ final class LiquidityPoolSupplyPresenter { private var prices: [PriceData]? private var swapVariant: SwapVariant = .desiredInput + private var apyInfo: PoolApyInfo? private var slippadgeTolerance: Float = Constants.slippadgeTolerance - private var swapFromInputResult: AmountInputResult? - private var swapFromBalance: Decimal? - private var swapToInputResult: AmountInputResult? - private var swapToBalance: Decimal? + private var baseAssetInputResult: AmountInputResult? + private var baseAssetBalance: Decimal? + private var targetAssetInputResult: AmountInputResult? + private var targetAssetBalance: Decimal? private var networkFeeViewModel: BalanceViewModelProtocol? private var networkFee: Decimal? @@ -62,6 +67,22 @@ final class LiquidityPoolSupplyPresenter { private var dexId: String? + private var baseAssetResultAmount: Decimal { + guard let baseAssetInputResult else { + return .zero + } + + return baseAssetInputResult.absoluteValue(from: baseAssetBalance.or(.zero)) + } + + private var targetAssetResultAmount: Decimal { + guard let targetAssetInputResult else { + return .zero + } + + return targetAssetInputResult.absoluteValue(from: targetAssetBalance.or(.zero)) + } + // MARK: - Constructors init( @@ -72,7 +93,8 @@ final class LiquidityPoolSupplyPresenter { chain: ChainModel, logger: LoggerProtocol, wallet: MetaAccountModel, - dataValidatingFactory: SendDataValidatingFactory + dataValidatingFactory: SendDataValidatingFactory, + viewModelFactory: LiquidityPoolSupplyViewModelFactory ) { self.interactor = interactor self.router = router @@ -81,12 +103,24 @@ final class LiquidityPoolSupplyPresenter { self.logger = logger self.wallet = wallet self.dataValidatingFactory = dataValidatingFactory + self.viewModelFactory = viewModelFactory self.localizationManager = localizationManager } // MARK: - Private methods + private func provideViewModel() { + let viewModel = viewModelFactory.buildViewModel( + slippage: Decimal(floatLiteral: Double(slippadgeTolerance)), + apy: apyInfo, + liquidityPair: liquidityPair, + chain: chain + ) + + view?.didReceiveViewModel(viewModel) + } + private func refreshFee() { guard let dexId, @@ -99,8 +133,8 @@ final class LiquidityPoolSupplyPresenter { let baseAssetInfo = PooledAssetInfo(id: liquidityPair.baseAssetId, precision: Int16(baseAsset.precision)) let targetAssetInfo = PooledAssetInfo(id: liquidityPair.targetAssetId, precision: Int16(targetAsset.precision)) - let baseAssetAmount = swapFromInputResult?.absoluteValue(from: swapFromBalance ?? .zero) ?? .zero - let targetAssetAmount = swapToInputResult?.absoluteValue(from: swapToBalance ?? .zero) ?? .zero + let baseAssetAmount = baseAssetInputResult?.absoluteValue(from: baseAssetBalance ?? .zero) ?? .zero + let targetAssetAmount = targetAssetInputResult?.absoluteValue(from: targetAssetBalance ?? .zero) ?? .zero let supplyLiquidityInfo = SupplyLiquidityInfo( dexId: dexId, baseAsset: baseAssetInfo, @@ -126,12 +160,6 @@ final class LiquidityPoolSupplyPresenter { } private func provideFromAssetVewModel(updateAmountInput: Bool = true) { - var balance: Decimal? = swapFromBalance - if swapFromChainAsset == chain.utilityChainAssets().first, let xorBalance = xorBalance, let networkFee = networkFee { - balance = xorBalance - networkFee - } - let inputAmount = swapFromInputResult? - .absoluteValue(from: balance ?? .zero) let balanceViewModelFactory = buildBalanceSwapToViewModelFactory( wallet: wallet, for: swapFromChainAsset @@ -142,13 +170,13 @@ final class LiquidityPoolSupplyPresenter { }) let viewModel = balanceViewModelFactory?.createAssetBalanceViewModel( - inputAmount, - balance: swapFromBalance, + baseAssetResultAmount, + balance: baseAssetBalance, priceData: swapFromPrice ).value(for: selectedLocale) let inputViewModel = balanceViewModelFactory? - .createBalanceInputViewModel(inputAmount) + .createBalanceInputViewModel(baseAssetResultAmount) .value(for: selectedLocale) DispatchQueue.main.async { [weak self] in @@ -163,8 +191,6 @@ final class LiquidityPoolSupplyPresenter { } private func provideToAssetVewModel(updateAmountInput: Bool = true) { - let inputAmount = swapToInputResult? - .absoluteValue(from: swapToBalance ?? .zero) let balanceViewModelFactory = buildBalanceSwapToViewModelFactory( wallet: wallet, for: swapToChainAsset @@ -175,13 +201,13 @@ final class LiquidityPoolSupplyPresenter { }) let viewModel = balanceViewModelFactory?.createAssetBalanceViewModel( - inputAmount, - balance: swapToBalance, + targetAssetResultAmount, + balance: targetAssetBalance, priceData: swapToPrice ).value(for: selectedLocale) let inputViewModel = balanceViewModelFactory? - .createBalanceInputViewModel(inputAmount) + .createBalanceInputViewModel(targetAssetResultAmount) .value(for: selectedLocale) DispatchQueue.main.async { [weak self] in @@ -243,24 +269,17 @@ final class LiquidityPoolSupplyPresenter { return balanceViewModelFactory } - private func runCanXorPayValidation(sendAmount: Decimal) { - DataValidationRunner(validators: [ - dataValidatingFactory.canPayFeeAndAmount( - balanceType: .utility(balance: xorBalance), - feeAndTip: networkFee ?? .zero, - sendAmount: sendAmount, - locale: selectedLocale - ) - ]).runValidation {} - } - private func provideInitialData() { swapFromChainAsset = chain.chainAssets.first(where: { $0.asset.currencyId == liquidityPair.baseAssetId }) swapToChainAsset = chain.chainAssets.first(where: { $0.asset.currencyId == liquidityPair.targetAssetId }) + DispatchQueue.main.async { + self.view?.didReceiveNetworkFee(fee: nil) + self.provideViewModel() + } + provideToAssetVewModel() provideFromAssetVewModel() - refreshFee() } } @@ -286,27 +305,66 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { ) } - func didTapPreviewButton() {} + func didTapPreviewButton() { + let baseAssetFee = swapFromChainAsset?.isUtility == true ? networkFee : .zero + let targetAssetFee = swapToChainAsset?.isUtility == true ? networkFee : .zero + + let validators = [ + dataValidatingFactory.has( + fee: networkFee, + locale: selectedLocale, + onError: { [weak self] in + self?.refreshFee() + } + ), + dataValidatingFactory.canPayFeeAndAmount( + balanceType: .utility(balance: baseAssetBalance), + feeAndTip: baseAssetFee, + sendAmount: baseAssetResultAmount, + locale: selectedLocale + ), + dataValidatingFactory.canPayFeeAndAmount( + balanceType: .utility(balance: targetAssetBalance), + feeAndTip: targetAssetFee, + sendAmount: targetAssetResultAmount, + locale: selectedLocale + ) + ] + + DataValidationRunner(validators: validators).runValidation { [weak self] in + guard let self else { + return + } + + let inputData = LiquidityPoolSupplyConfirmInputData( + baseAssetAmount: self.baseAssetResultAmount, + targetAssetAmount: self.targetAssetResultAmount, + slippageTolerance: Decimal(floatLiteral: Double(self.slippadgeTolerance)) + ) + + self.router.showConfirmation( + chain: self.chain, + wallet: self.wallet, + liquidityPair: self.liquidityPair, + inputData: inputData, + from: self.view + ) + } + } func selectFromAmountPercentage(_ percentage: Float) { runLoadingState() swapVariant = .desiredInput - swapFromInputResult = .rate(Decimal(Double(percentage))) + baseAssetInputResult = .rate(Decimal(Double(percentage))) - let baseAssetAbsolulteValue = swapFromInputResult?.absoluteValue(from: swapFromBalance.or(.zero)) + let baseAssetAbsolulteValue = baseAssetInputResult?.absoluteValue(from: baseAssetBalance.or(.zero)) let targetAssetAbsoluteValue = baseAssetAbsolulteValue.or(.zero) * baseTargetRate.or(.zero) - swapToInputResult = .absolute(targetAssetAbsoluteValue) + targetAssetInputResult = .absolute(targetAssetAbsoluteValue) provideFromAssetVewModel() provideToAssetVewModel() - if swapFromChainAsset == chain.utilityChainAssets().first { - let inputAmount = swapFromInputResult? - .absoluteValue(from: xorBalanceMinusFee) - runCanXorPayValidation(sendAmount: inputAmount ?? .zero) - } - refreshFee() } @@ -314,11 +372,11 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { runLoadingState() swapVariant = .desiredInput - swapFromInputResult = .absolute(newValue) + baseAssetInputResult = .absolute(newValue) - let baseAssetAbsolulteValue = swapFromInputResult?.absoluteValue(from: swapFromBalance.or(.zero)) + let baseAssetAbsolulteValue = baseAssetInputResult?.absoluteValue(from: baseAssetBalance.or(.zero)) let targetAssetAbsoluteValue = baseAssetAbsolulteValue.or(.zero) * baseTargetRate.or(.zero) - swapToInputResult = .absolute(targetAssetAbsoluteValue) + targetAssetInputResult = .absolute(targetAssetAbsoluteValue) provideFromAssetVewModel(updateAmountInput: false) provideToAssetVewModel() @@ -330,11 +388,11 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { runLoadingState() swapVariant = .desiredOutput - swapToInputResult = .rate(Decimal(Double(percentage))) + targetAssetInputResult = .rate(Decimal(Double(percentage))) - let targetAssetAbsoluteValue = swapToInputResult?.absoluteValue(from: swapToBalance.or(.zero)) - let baseAssetAbsolulteValue = targetAssetAbsoluteValue.or(.zero) * baseTargetRate.or(.zero) - swapFromInputResult = .absolute(baseAssetAbsolulteValue) + let targetAssetAbsoluteValue = targetAssetInputResult?.absoluteValue(from: targetAssetBalance.or(.zero)) + let baseAssetAbsolulteValue = targetAssetAbsoluteValue.or(.zero) / baseTargetRate.or(1) + baseAssetInputResult = .absolute(baseAssetAbsolulteValue) provideFromAssetVewModel() provideToAssetVewModel() @@ -346,11 +404,11 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { runLoadingState() swapVariant = .desiredOutput - swapToInputResult = .absolute(newValue) + targetAssetInputResult = .absolute(newValue) - let targetAssetAbsoluteValue = swapToInputResult?.absoluteValue(from: swapToBalance.or(.zero)) - let baseAssetAbsolulteValue = targetAssetAbsoluteValue.or(.zero) * baseTargetRate.or(.zero) - swapFromInputResult = .absolute(baseAssetAbsolulteValue) + let targetAssetAbsoluteValue = targetAssetInputResult?.absoluteValue(from: targetAssetBalance.or(.zero)) + let baseAssetAbsolulteValue = targetAssetAbsoluteValue.or(.zero) / baseTargetRate.or(1) + baseAssetInputResult = .absolute(baseAssetAbsolulteValue) provideFromAssetVewModel() provideToAssetVewModel(updateAmountInput: false) @@ -387,6 +445,9 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { func didLoad(view: LiquidityPoolSupplyViewInput) { self.view = view interactor.setup(with: self) + } + + func handleViewAppeared() { provideInitialData() } } @@ -394,6 +455,15 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { // MARK: - LiquidityPoolSupplyInteractorOutput extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyInteractorOutput { + func didReceivePoolAPY(apyInfo: SSFPolkaswap.PoolApyInfo?) { + self.apyInfo = apyInfo + provideViewModel() + } + + func didReceivePoolApyError(error: Error) { + logger.customError(error) + } + func didReceiveDexId(_ dexId: String) { self.dexId = dexId refreshFee() @@ -430,6 +500,8 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyInteractorOutput { let baseAssetPriceValue = Decimal(string: baseAssetPrice.price), let targetAssetPriceValue = Decimal(string: targetAssetPrice.price) { baseTargetRate = baseAssetPriceValue / targetAssetPriceValue + + view?.didReceiveSwapQuoteReady() } case let .failure(error): prices = [] @@ -444,7 +516,7 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyInteractorOutput { switch result { case let .success(accountInfo): if swapFromChainAsset == chainAsset { - swapFromBalance = accountInfo.map { + baseAssetBalance = accountInfo.map { Decimal.fromSubstrateAmount( $0.data.sendAvailable, precision: Int16(chainAsset.asset.precision) @@ -453,7 +525,7 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyInteractorOutput { provideFromAssetVewModel() } if swapToChainAsset == chainAsset { - swapToBalance = accountInfo.map { + targetAssetBalance = accountInfo.map { Decimal.fromSubstrateAmount( $0.data.sendAvailable, precision: Int16(chainAsset.asset.precision) diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyProtocols.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyProtocols.swift index 7b906e18ca..c5e9bff54d 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyProtocols.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyProtocols.swift @@ -1,4 +1,5 @@ import SSFModels +import SSFPools typealias LiquidityPoolSupplyModuleCreationResult = ( view: LiquidityPoolSupplyViewInput, @@ -14,6 +15,14 @@ protocol LiquidityPoolSupplyRouterInput: AnyObject, AnyDismissable, SheetAlertPr contextTag: Int?, output: SelectAssetModuleOutput ) + + func showConfirmation( + chain: ChainModel, + wallet: MetaAccountModel, + liquidityPair: LiquidityPair, + inputData: LiquidityPoolSupplyConfirmInputData, + from view: ControllerBackedProtocol? + ) } protocol LiquidityPoolSupplyModuleInput: AnyObject {} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyRouter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyRouter.swift index 8a3f6bae8a..425ff95c68 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyRouter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyRouter.swift @@ -1,4 +1,5 @@ import Foundation +import SSFPools import SSFModels final class LiquidityPoolSupplyRouter: LiquidityPoolSupplyRouterInput { @@ -24,4 +25,18 @@ final class LiquidityPoolSupplyRouter: LiquidityPoolSupplyRouterInput { view?.controller.present(module.view.controller, animated: true) } + + func showConfirmation( + chain: ChainModel, + wallet: MetaAccountModel, + liquidityPair: LiquidityPair, + inputData: LiquidityPoolSupplyConfirmInputData, + from view: ControllerBackedProtocol? + ) { + guard let module = LiquidityPoolSupplyConfirmAssembly.configureModule(chain: chain, wallet: wallet, liquidityPair: liquidityPair, inputData: inputData) else { + return + } + + view?.controller.navigationController?.pushViewController(module.view.controller, animated: true) + } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift index 41c9ad65c7..a7998ed7ad 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift @@ -4,6 +4,7 @@ import SnapKit protocol LiquidityPoolSupplyViewOutput: AnyObject { func didLoad(view: LiquidityPoolSupplyViewInput) + func handleViewAppeared() func didTapBackButton() func didTapApyInfo() func didTapPreviewButton() @@ -28,6 +29,9 @@ final class LiquidityPoolSupplyViewController: UIViewController, ViewHolder, Hid private var amountFromInputViewModel: IAmountInputViewModel? private var amountToInputViewModel: IAmountInputViewModel? + private var assetFromViewModel: AssetBalanceViewModelProtocol? + private var assetToViewModel: AssetBalanceViewModelProtocol? + // MARK: - Constructor init( @@ -58,6 +62,14 @@ final class LiquidityPoolSupplyViewController: UIViewController, ViewHolder, Hid addEndEditingTapGesture(for: rootView.contentView) } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if isBeingPresented || isMovingToParent { + output.handleViewAppeared() + } + } + // MARK: - Private methods private func configure() { @@ -66,6 +78,9 @@ final class LiquidityPoolSupplyViewController: UIViewController, ViewHolder, Hid rootView.swapToInputView.textField.delegate = self rootView.swapFromInputView.textField.delegate = self + rootView.swapFromInputView.textField.isUserInteractionEnabled = false + rootView.swapToInputView.textField.isUserInteractionEnabled = false + let locale = localizationManager?.selectedLocale ?? Locale.current let accessoryView = UIFactory.default.createAmountAccessoryView(for: self, locale: locale) rootView.swapFromInputView.textField.inputAccessoryView = accessoryView @@ -73,7 +88,7 @@ final class LiquidityPoolSupplyViewController: UIViewController, ViewHolder, Hid } private func updatePreviewButton() { - let isEnabled = amountFromInputViewModel?.isValid == true && amountToInputViewModel?.isValid == true + let isEnabled = amountFromInputViewModel?.isValid == true && amountToInputViewModel?.isValid == true && assetToViewModel != nil && assetFromViewModel != nil rootView.previewButton.set(enabled: isEnabled) } @@ -83,12 +98,6 @@ final class LiquidityPoolSupplyViewController: UIViewController, ViewHolder, Hid action: #selector(handleTapBackButton), for: .touchUpInside ) - let tapMinReceiveInfo = UITapGestureRecognizer( - target: self, - action: #selector(handleTapApyInfo) - ) - rootView.minMaxReceivedView.titleLabel - .addGestureRecognizer(tapMinReceiveInfo) rootView.previewButton.addTarget( self, @@ -115,12 +124,21 @@ final class LiquidityPoolSupplyViewController: UIViewController, ViewHolder, Hid // MARK: - LiquidityPoolSupplyViewInput extension LiquidityPoolSupplyViewController: LiquidityPoolSupplyViewInput { + func didReceiveSwapQuoteReady() { + rootView.swapFromInputView.textField.isUserInteractionEnabled = true + rootView.swapToInputView.textField.isUserInteractionEnabled = true + } + func didReceiveSwapFrom(viewModel: AssetBalanceViewModelProtocol?) { + assetFromViewModel = viewModel rootView.bindSwapFrom(assetViewModel: viewModel) + updatePreviewButton() } func didReceiveSwapTo(viewModel: AssetBalanceViewModelProtocol?) { + assetToViewModel = viewModel rootView.bindSwapTo(assetViewModel: viewModel) + updatePreviewButton() } func didReceiveSwapFrom(amountInputViewModel: IAmountInputViewModel?) { @@ -153,6 +171,10 @@ extension LiquidityPoolSupplyViewController: LiquidityPoolSupplyViewInput { self.rootView.previewButton.set(enabled: false) } } + + func didReceiveViewModel(_ viewModel: LiquidityPoolSupplyViewModel) { + rootView.bind(viewModel: viewModel) + } } // MARK: - Localizable diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift index 4bb287eb59..6edbcd9a7d 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift @@ -49,21 +49,20 @@ final class LiquidityPoolSupplyViewLayout: UIView { return button }() - let minMaxReceivedView = UIFactory.default.createMultiView() let swapRouteView: TitleMultiValueView = { let view = UIFactory.default.createMultiView() return view }() - let fromPerToPriceView = UIFactory.default.createMultiView() - let toPerFromPriceView = UIFactory.default.createMultiView() + let slippageView = UIFactory.default.createMultiView() + let apyView = UIFactory.default.createMultiView() + let rewardTokenView = UIFactory.default.createMultiView() let networkFeeView = UIFactory.default.createMultiView() private lazy var multiViews = [ - minMaxReceivedView, - swapRouteView, - fromPerToPriceView, - toPerFromPriceView, + slippageView, + apyView, + rewardTokenView, networkFeeView ] @@ -120,31 +119,10 @@ final class LiquidityPoolSupplyViewLayout: UIView { swapToInputView.bind(viewModel: assetViewModel) } - func bindDetails(viewModel: PolkaswapAdjustmentDetailsViewModel?) { - guard let viewModel = viewModel else { - multiViews.forEach { $0.isHidden = true } - return - } - minMaxReceivedView.bindBalance(viewModel: viewModel.minMaxReceiveVieModel) - swapRouteView.valueTop.text = viewModel.route - fromPerToPriceView.titleLabel.text = viewModel.fromPerToTitle - fromPerToPriceView.valueTop.text = viewModel.fromPerToValue - toPerFromPriceView.titleLabel.text = viewModel.toPerFromTitle - toPerFromPriceView.valueTop.text = viewModel.toPerFromValue - multiViews.forEach { $0.isHidden = false } - } - - func bind(swapVariant: SwapVariant) { - var text: String - switch swapVariant { - case .desiredInput: - text = R.string.localizable - .polkaswapMinReceived(preferredLanguages: locale.rLanguages) - case .desiredOutput: - text = R.string.localizable - .polkaswapMaxReceived(preferredLanguages: locale.rLanguages) - } - setInfoImage(for: minMaxReceivedView.titleLabel, text: text) + func bind(viewModel: LiquidityPoolSupplyViewModel) { + slippageView.bind(viewModel: viewModel.slippageViewModel) + apyView.bind(viewModel: viewModel.apyViewModel) + rewardTokenView.bind(viewModel: viewModel.rewardTokenViewModel) } // MARK: - Private methods @@ -183,13 +161,13 @@ final class LiquidityPoolSupplyViewLayout: UIView { contentView.snp.makeConstraints { make in make.top.equalTo(navigationViewContainer.snp.bottom) make.leading.trailing.equalToSuperview() - make.bottom.equalTo(previewButton.snp.top).offset(-UIConstants.bigOffset) + make.bottom.equalTo(previewButton.snp.top).offset(-16) } previewButton.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview().inset(UIConstants.bigOffset) + make.leading.trailing.equalToSuperview().inset(16) make.height.equalTo(UIConstants.actionHeight) - keyboardAdoptableConstraint = make.bottom.equalToSuperview().inset(UIConstants.bigOffset).constraint + keyboardAdoptableConstraint = make.bottom.equalToSuperview().inset(16).constraint } let switchInputsView = createSwitchInputsView() @@ -201,7 +179,7 @@ final class LiquidityPoolSupplyViewLayout: UIView { let feesView = createFeesView() contentView.stackView.addArrangedSubview(feesView) feesView.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview().inset(UIConstants.bigOffset) + make.leading.trailing.equalToSuperview().inset(16) } } @@ -237,27 +215,36 @@ final class LiquidityPoolSupplyViewLayout: UIView { } } + let backgroundView = UIFactory.default.createInfoBackground() let container = UIFactory.default.createVerticalStackView() + backgroundView.addSubview(container) + container.snp.makeConstraints { make in + make.leading.trailing.top.equalToSuperview().inset(16) + make.bottom.equalToSuperview().inset(8) + } + multiViews.forEach { container.addArrangedSubview($0) makeCommonConstraints(for: $0) - $0.isHidden = true $0.titleLabel.isUserInteractionEnabled = true } - return container + return backgroundView } private func createMultiView() -> TitleMultiValueView { let view = TitleMultiValueView() - view.borderView.borderType = .none - view.titleLabel.font = .p2Paragraph - view.titleLabel.textColor = R.color.colorStrokeGray() - view.valueTop.font = .h6Title + view.titleLabel.font = .h6Title + view.titleLabel.textColor = R.color.colorWhite50() + view.valueTop.font = .p1Paragraph view.valueTop.textColor = R.color.colorWhite() view.valueBottom.font = .p2Paragraph view.valueBottom.textColor = R.color.colorStrokeGray() + view.borderView.isHidden = true + view.equalsLabelsWidth = true + view.valueTop.lineBreakMode = .byTruncatingTail + view.valueBottom.lineBreakMode = .byTruncatingMiddle return view } @@ -281,6 +268,10 @@ final class LiquidityPoolSupplyViewLayout: UIView { previewButton.imageWithTitleView?.title = R.string.localizable .commonPreview(preferredLanguages: locale.rLanguages) + + slippageView.titleLabel.text = R.string.localizable.polkaswapSettingsSlippageTitle(preferredLanguages: locale.rLanguages) + apyView.titleLabel.text = "Strategic Bonus APY" + rewardTokenView.titleLabel.text = "Rewards Payout In" } private func setInfoImage(for label: UILabel, text: String) { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/ViewModel/LiquidityPoolSupplyViewModel.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/ViewModel/LiquidityPoolSupplyViewModel.swift new file mode 100644 index 0000000000..b6d62c8a8d --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/ViewModel/LiquidityPoolSupplyViewModel.swift @@ -0,0 +1,7 @@ +import Foundation + +struct LiquidityPoolSupplyViewModel { + let slippageViewModel: TitleMultiValueViewModel? + let apyViewModel: TitleMultiValueViewModel? + let rewardTokenViewModel: TitleMultiValueViewModel? +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/ViewModel/LiquidityPoolSupplyViewModelFactory.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/ViewModel/LiquidityPoolSupplyViewModelFactory.swift new file mode 100644 index 0000000000..e3a3c63591 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/ViewModel/LiquidityPoolSupplyViewModelFactory.swift @@ -0,0 +1,39 @@ +import Foundation +import SSFPolkaswap +import SSFPools +import SSFModels + +protocol LiquidityPoolSupplyViewModelFactory { + func buildViewModel( + slippage: Decimal, + apy: PoolApyInfo?, + liquidityPair: LiquidityPair, + chain: ChainModel + ) -> LiquidityPoolSupplyViewModel +} + +class LiquidityPoolSupplyViewModelFactoryDefault: LiquidityPoolSupplyViewModelFactory { + func buildViewModel( + slippage: Decimal, + apy: PoolApyInfo?, + liquidityPair: LiquidityPair, + chain: ChainModel + ) -> LiquidityPoolSupplyViewModel { + let slippageString = NumberFormatter.percentPlain.stringFromDecimal(slippage) + let slippageViewModel = TitleMultiValueViewModel(title: slippageString, subtitle: nil) + + let apyString = apy?.apy.flatMap { + NumberFormatter.percentAPY.stringFromDecimal($0) + } + let apyViewModel = apyString.flatMap { TitleMultiValueViewModel(title: $0, subtitle: nil) } + + let rewardAssetSymbol = chain.assets.first(where: { $0.currencyId == liquidityPair.rewardAssetId })?.symbol.uppercased() + let rewardTokenViewModel = TitleMultiValueViewModel(title: rewardAssetSymbol, subtitle: nil) + + return LiquidityPoolSupplyViewModel( + slippageViewModel: slippageViewModel, + apyViewModel: apyViewModel, + rewardTokenViewModel: rewardTokenViewModel + ) + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmAssembly.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmAssembly.swift new file mode 100644 index 0000000000..937f66a9d6 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmAssembly.swift @@ -0,0 +1,97 @@ +import UIKit +import SoraFoundation +import SSFPolkaswap +import SSFPools +import SSFModels +import SoraKeystore + +struct LiquidityPoolSupplyConfirmInputData { + let baseAssetAmount: Decimal + let targetAssetAmount: Decimal + let slippageTolerance: Decimal +} + +enum LiquidityPoolSupplyConfirmAssembly { + static func configureModule( + chain: ChainModel, + wallet: MetaAccountModel, + liquidityPair: LiquidityPair, + inputData: LiquidityPoolSupplyConfirmInputData + ) -> LiquidityPoolSupplyConfirmModuleCreationResult? { + guard let response = wallet.fetch(for: chain.accountRequest()) else { + return nil + } + + guard let secretKeyData = try? fetchSecretKey( + for: chain, + metaId: wallet.metaId, + accountResponse: response + ) else { + return nil + } + + let localizationManager = LocalizationManager.shared + let chainRegistry = ChainRegistryFacade.sharedRegistry + let lpDataService = PolkaswapLiquidityPoolServiceAssembly.buildService(for: chain, chainRegistry: chainRegistry) + let signingWrapperData = SigningWrapperData(publicKeyData: response.publicKey, secretKeyData: secretKeyData) + + guard let lpOperationService = try? PolkaswapLiquidityPoolServiceAssembly.buildOperationService( + for: chain, + wallet: wallet.utilsModel, + chainRegistry: chainRegistry, + signingWrapperData: signingWrapperData + ) else { + return nil + } + + let accountInfoSubscriptionAdapter = AccountInfoSubscriptionAdapter( + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, + selectedMetaAccount: wallet + ) + + let interactor = LiquidityPoolSupplyConfirmInteractor( + lpOperationService: lpOperationService, + lpDataService: lpDataService, + liquidityPair: liquidityPair, + priceLocalSubscriber: PriceLocalStorageSubscriberImpl.shared, + chain: chain, + accountInfoSubscriptionAdapter: accountInfoSubscriptionAdapter + ) + let router = LiquidityPoolSupplyConfirmRouter() + + let presenter = LiquidityPoolSupplyConfirmPresenter( + interactor: interactor, + router: router, + localizationManager: localizationManager, + dataValidatingFactory: SendDataValidatingFactory(presentable: router), + logger: Logger.shared, + liquidityPair: liquidityPair, + chain: chain, + inputData: inputData, + wallet: wallet, + viewModelFactory: LiquidityPoolSupplyConfirmViewModelFactoryDefault() + ) + + let view = LiquidityPoolSupplyConfirmViewController( + output: presenter, + localizationManager: localizationManager + ) + + return (view, presenter) + } + + static func fetchSecretKey( + for chain: ChainModel, + metaId: String, + accountResponse: ChainAccountResponse + ) throws -> Data { + let accountId = accountResponse.isChainAccount ? accountResponse.accountId : nil + let tag: String = chain.isEthereumBased + ? KeystoreTagV2.ethereumSecretKeyTagForMetaId(metaId, accountId: accountId) + : KeystoreTagV2.substrateSecretKeyTagForMetaId(metaId, accountId: accountId) + + let keystore = Keychain() + let secretKey = try keystore.fetchKey(for: tag) + return secretKey + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmInteractor.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmInteractor.swift new file mode 100644 index 0000000000..6b27bec5d1 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmInteractor.swift @@ -0,0 +1,157 @@ +import UIKit +import SSFModels +import SSFPolkaswap +import SSFPools +import BigInt + +protocol LiquidityPoolSupplyConfirmInteractorOutput: AnyObject { + func didReceiveDexId(_ dexId: String) + func didReceiveDexIdError(_ error: Error) + func didReceiveFee(_ fee: BigUInt) + func didReceiveFeeError(_ error: Error) + func didReceivePricesData(result: Result<[PriceData], Error>) + func didReceiveAccountInfo(result: Result, for chainAsset: ChainAsset) + func didReceivePoolAPY(apyInfo: PoolApyInfo?) + func didReceivePoolApyError(error: Error) + func didReceiveTransactionHash(_ hash: String) + func didReceiveSubmitError(error: Error) +} + +final class LiquidityPoolSupplyConfirmInteractor { + // MARK: - Private properties + + private weak var output: LiquidityPoolSupplyConfirmInteractorOutput? + private let lpOperationService: PoolsOperationService + private let lpDataService: PolkaswapLiquidityPoolService + private let liquidityPair: LiquidityPair + private let priceLocalSubscriber: PriceLocalStorageSubscriber + private let chain: ChainModel + private let accountInfoSubscriptionAdapter: AccountInfoSubscriptionAdapterProtocol + + private var pricesProvider: AnySingleValueProvider<[PriceData]>? + + init( + lpOperationService: PoolsOperationService, + lpDataService: PolkaswapLiquidityPoolService, + liquidityPair: LiquidityPair, + priceLocalSubscriber: PriceLocalStorageSubscriber, + chain: ChainModel, + accountInfoSubscriptionAdapter: AccountInfoSubscriptionAdapterProtocol + ) { + self.lpOperationService = lpOperationService + self.lpDataService = lpDataService + self.liquidityPair = liquidityPair + self.priceLocalSubscriber = priceLocalSubscriber + self.chain = chain + self.accountInfoSubscriptionAdapter = accountInfoSubscriptionAdapter + } + + private func fetchDexId() { + Task { + do { + let dexId = try await lpDataService.fetchDexId(baseAssetId: liquidityPair.baseAssetId) + output?.didReceiveDexId(dexId) + } catch { + output?.didReceiveDexIdError(error) + } + } + } + + private func subscribeToPrices() { + let chainAssets = chain.chainAssets + pricesProvider = priceLocalSubscriber.subscribeToPrices(for: chainAssets, listener: self) + + guard chainAssets.isNotEmpty else { + output?.didReceivePricesData(result: .success([])) + return + } + pricesProvider = priceLocalSubscriber.subscribeToPrices(for: chainAssets, listener: self) + } + + private func subscribeToAccountInfo() { + let chainAssets = chain.chainAssets + accountInfoSubscriptionAdapter.subscribe( + chainsAssets: chainAssets, + handler: self, + deliveryOn: .main + ) + } +} + +// MARK: - LiquidityPoolSupplyConfirmInteractorInput + +extension LiquidityPoolSupplyConfirmInteractor: LiquidityPoolSupplyConfirmInteractorInput { + func setup(with output: LiquidityPoolSupplyConfirmInteractorOutput) { + self.output = output + fetchDexId() + fetchApy() + subscribeToPrices() + subscribeToAccountInfo() + } + + func estimateFee(supplyLiquidityInfo: SupplyLiquidityInfo) { + Task { + do { + let fee = try await lpOperationService.estimateFee(liquidityOperation: .substrateSupplyLiquidity(supplyLiquidityInfo)) + output?.didReceiveFee(fee) + } catch { + output?.didReceiveFeeError(error) + } + } + } + + func submit(supplyLiquidityInfo: SupplyLiquidityInfo) { + Task { + do { + let hash = try await lpOperationService.submit(liquidityOperation: .substrateSupplyLiquidity(supplyLiquidityInfo)) + await MainActor.run { + output?.didReceiveTransactionHash(hash) + } + } catch { + await MainActor.run { + output?.didReceiveSubmitError(error: error) + } + } + } + } + + func fetchApy() { + guard let reservesId = liquidityPair.reservesId else { + return + } + + Task { + do { + let apy = try await lpDataService.fetchPoolsAPY() + let address = try AddressFactory.address(for: Data(hex: reservesId), chain: chain) + await MainActor.run { + output?.didReceivePoolAPY(apyInfo: apy.first(where: { $0.poolId == address })) + } + } catch { + await MainActor.run { + output?.didReceivePoolApyError(error: error) + } + } + } + } +} + +// MARK: - PriceLocalStorageSubscriber + +extension LiquidityPoolSupplyConfirmInteractor: PriceLocalSubscriptionHandler { + func handlePrices(result: Result<[PriceData], Error>) { + output?.didReceivePricesData(result: result) + } +} + +// MARK: - AccountInfoSubscriptionAdapterHandler + +extension LiquidityPoolSupplyConfirmInteractor: AccountInfoSubscriptionAdapterHandler { + func handleAccountInfo( + result: Result, + accountId _: AccountId, + chainAsset: ChainAsset + ) { + output?.didReceiveAccountInfo(result: result, for: chainAsset) + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmPresenter.swift new file mode 100644 index 0000000000..0d9d993737 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmPresenter.swift @@ -0,0 +1,352 @@ +import Foundation +import SoraFoundation +import SSFPools +import SSFPolkaswap +import SSFModels +import BigInt + +protocol LiquidityPoolSupplyConfirmViewInput: ControllerBackedProtocol { + func didReceiveNetworkFee(fee: BalanceViewModelProtocol?) + func setButtonLoadingState(isLoading: Bool) + func didUpdating() + func didReceiveViewModel(_ viewModel: LiquidityPoolSupplyViewModel) + func didReceiveConfirmationViewModel(_ viewModel: LiquidityPoolSupplyConfirmViewModel?) +} + +protocol LiquidityPoolSupplyConfirmInteractorInput: AnyObject { + func setup(with output: LiquidityPoolSupplyConfirmInteractorOutput) + func estimateFee(supplyLiquidityInfo: SupplyLiquidityInfo) + func submit(supplyLiquidityInfo: SupplyLiquidityInfo) +} + +final class LiquidityPoolSupplyConfirmPresenter { + // MARK: Private properties + + private weak var view: LiquidityPoolSupplyConfirmViewInput? + private let router: LiquidityPoolSupplyConfirmRouterInput + private let interactor: LiquidityPoolSupplyConfirmInteractorInput + private let dataValidatingFactory: SendDataValidatingFactory + private let logger: LoggerProtocol + private let liquidityPair: LiquidityPair + private let chain: ChainModel + private let inputData: LiquidityPoolSupplyConfirmInputData + private let viewModelFactory: LiquidityPoolSupplyConfirmViewModelFactory + + private var apyInfo: PoolApyInfo? + private let wallet: MetaAccountModel + private var swapFromChainAsset: ChainAsset? + private var swapToChainAsset: ChainAsset? + private var prices: [PriceData]? + private var networkFeeViewModel: BalanceViewModelProtocol? + private var networkFee: Decimal? + private var xorBalance: Decimal? + private var xorBalanceMinusFee: Decimal { + (xorBalance ?? 0) - (networkFee ?? 0) + } + + private var swapFromBalance: Decimal? + private var swapToBalance: Decimal? + + private var dexId: String? + + // MARK: - Constructors + + init( + interactor: LiquidityPoolSupplyConfirmInteractorInput, + router: LiquidityPoolSupplyConfirmRouterInput, + localizationManager: LocalizationManagerProtocol, + dataValidatingFactory: SendDataValidatingFactory, + logger: LoggerProtocol, + liquidityPair: LiquidityPair, + chain: ChainModel, + inputData: LiquidityPoolSupplyConfirmInputData, + wallet: MetaAccountModel, + viewModelFactory: LiquidityPoolSupplyConfirmViewModelFactory + ) { + self.interactor = interactor + self.router = router + self.dataValidatingFactory = dataValidatingFactory + self.logger = logger + self.liquidityPair = liquidityPair + self.chain = chain + self.inputData = inputData + self.wallet = wallet + self.viewModelFactory = viewModelFactory + + self.localizationManager = localizationManager + } + + // MARK: - Private methods + + private func refreshFee() { + guard + let dexId, + let baseAsset = chain.assets.first(where: { $0.currencyId == liquidityPair.baseAssetId }), + let targetAsset = chain.assets.first(where: { $0.currencyId == liquidityPair.targetAssetId }) + else { + return + } + + let baseAssetInfo = PooledAssetInfo(id: liquidityPair.baseAssetId, precision: Int16(baseAsset.precision)) + let targetAssetInfo = PooledAssetInfo(id: liquidityPair.targetAssetId, precision: Int16(targetAsset.precision)) + + let supplyLiquidityInfo = SupplyLiquidityInfo( + dexId: dexId, + baseAsset: baseAssetInfo, + targetAsset: targetAssetInfo, + baseAssetAmount: inputData.baseAssetAmount, + targetAssetAmount: inputData.targetAssetAmount, + slippage: inputData.slippageTolerance + ) + + interactor.estimateFee(supplyLiquidityInfo: supplyLiquidityInfo) + } + + private func runLoadingState() { + DispatchQueue.main.async { [weak self] in + self?.view?.setButtonLoadingState(isLoading: true) + } + } + + private func checkLoadingState() { + DispatchQueue.main.async { [weak self] in + self?.view?.setButtonLoadingState(isLoading: false) + } + } + + private func provideFeeViewModel() { + guard let swapFromFee = networkFee, let xorChainAsset = chain.utilityChainAssets().first else { + return + } + let balanceViewModelFactory = createBalanceViewModelFactory(for: xorChainAsset) + let feeViewModel = balanceViewModelFactory.balanceFromPrice( + swapFromFee, + priceData: prices?.first(where: { price in + price.priceId == xorChainAsset.asset.priceId + }), + isApproximately: true, + usageCase: .detailsCrypto + ).value(for: selectedLocale) + + DispatchQueue.main.async { + self.view?.didReceiveNetworkFee(fee: feeViewModel) + } + + networkFeeViewModel = feeViewModel + + checkLoadingState() + } + + private func buildBalanceSwapToViewModelFactory( + wallet: MetaAccountModel, + for chainAsset: ChainAsset? + ) -> BalanceViewModelFactoryProtocol? { + guard let chainAsset = chainAsset else { + return nil + } + let assetInfo = chainAsset.asset + .displayInfo(with: chainAsset.chain.icon) + let balanceViewModelFactory = BalanceViewModelFactory( + targetAssetInfo: assetInfo, + selectedMetaAccount: wallet + ) + return balanceViewModelFactory + } + + private func createBalanceViewModelFactory(for chainAsset: ChainAsset) -> BalanceViewModelFactory { + let assetInfo = chainAsset.asset.displayInfo(with: chainAsset.chain.icon) + let balanceViewModelFactory = BalanceViewModelFactory( + targetAssetInfo: assetInfo, + selectedMetaAccount: wallet + ) + return balanceViewModelFactory + } + + private func runCanXorPayValidation(sendAmount: Decimal) { + DataValidationRunner(validators: [ + dataValidatingFactory.canPayFeeAndAmount( + balanceType: .utility(balance: xorBalance), + feeAndTip: networkFee ?? .zero, + sendAmount: sendAmount, + locale: selectedLocale + ) + ]).runValidation {} + } + + private func provideInitialData() { + swapFromChainAsset = chain.chainAssets.first(where: { $0.asset.currencyId == liquidityPair.baseAssetId }) + swapToChainAsset = chain.chainAssets.first(where: { $0.asset.currencyId == liquidityPair.targetAssetId }) + + refreshFee() + } + + private func provideViewModel() { + let viewModel = viewModelFactory.buildViewModel( + slippage: inputData.slippageTolerance, + apy: apyInfo, + liquidityPair: liquidityPair, + chain: chain + ) + + view?.didReceiveViewModel(viewModel) + } + + private func provideConfirmationViewModel() { + let viewModel = viewModelFactory.buildViewModel( + baseAssetAmount: inputData.baseAssetAmount, + targetAssetAmount: inputData.targetAssetAmount, + liquidityPair: liquidityPair, + chain: chain, + locale: selectedLocale + ) + + view?.didReceiveConfirmationViewModel(viewModel) + } +} + +// MARK: - LiquidityPoolSupplyConfirmViewOutput + +extension LiquidityPoolSupplyConfirmPresenter: LiquidityPoolSupplyConfirmViewOutput { + func didTapBackButton() { + router.dismiss(view: view) + } + + func didTapApyInfo() {} + + func didTapConfirmButton() { + guard + let dexId, + let baseAsset = chain.assets.first(where: { $0.currencyId == liquidityPair.baseAssetId }), + let targetAsset = chain.assets.first(where: { $0.currencyId == liquidityPair.targetAssetId }) + else { + return + } + + let baseAssetInfo = PooledAssetInfo(id: liquidityPair.baseAssetId, precision: Int16(baseAsset.precision)) + let targetAssetInfo = PooledAssetInfo(id: liquidityPair.targetAssetId, precision: Int16(targetAsset.precision)) + + let supplyLiquidityInfo = SupplyLiquidityInfo( + dexId: dexId, + baseAsset: baseAssetInfo, + targetAsset: targetAssetInfo, + baseAssetAmount: inputData.baseAssetAmount, + targetAssetAmount: inputData.targetAssetAmount, + slippage: inputData.slippageTolerance + ) + + interactor.submit(supplyLiquidityInfo: supplyLiquidityInfo) + } + + func didLoad(view: LiquidityPoolSupplyConfirmViewInput) { + self.view = view + interactor.setup(with: self) + } + + func handleViewAppeared() { + DispatchQueue.main.async { [weak self] in + self?.view?.didReceiveNetworkFee(fee: nil) + self?.provideViewModel() + self?.provideConfirmationViewModel() + } + + refreshFee() + } +} + +// MARK: - LiquidityPoolSupplyConfirmInteractorOutput + +extension LiquidityPoolSupplyConfirmPresenter: LiquidityPoolSupplyConfirmInteractorOutput { + func didReceivePoolAPY(apyInfo: SSFPolkaswap.PoolApyInfo?) { + self.apyInfo = apyInfo + provideViewModel() + } + + func didReceivePoolApyError(error: Error) { + logger.customError(error) + } + + func didReceiveDexId(_ dexId: String) { + self.dexId = dexId + refreshFee() + } + + func didReceiveDexIdError(_ error: Error) { + logger.customError(error) + } + + func didReceiveFee(_ fee: BigUInt) { + guard let utilityAsset = chain.utilityAssets().first else { + return + } + + networkFee = Decimal.fromSubstrateAmount(fee, precision: Int16(utilityAsset.precision)) + provideFeeViewModel() + } + + func didReceiveFeeError(_ error: Error) { + logger.customError(error) + } + + func didReceivePricesData(result: Result<[PriceData], Error>) { + switch result { + case let .success(priceData): + prices = priceData + provideFeeViewModel() + case let .failure(error): + prices = [] + logger.error("\(error)") + } + } + + func didReceiveAccountInfo(result: Result, for chainAsset: ChainAsset) { + switch result { + case let .success(accountInfo): + if swapFromChainAsset == chainAsset { + swapFromBalance = accountInfo.map { + Decimal.fromSubstrateAmount( + $0.data.sendAvailable, + precision: Int16(chainAsset.asset.precision) + ) + } ?? .zero + } + if swapToChainAsset == chainAsset { + swapToBalance = accountInfo.map { + Decimal.fromSubstrateAmount( + $0.data.sendAvailable, + precision: Int16(chainAsset.asset.precision) + ) + } ?? .zero + } + if chain.utilityChainAssets().first == chainAsset { + xorBalance = accountInfo.map { + Decimal.fromSubstrateAmount( + $0.data.sendAvailable, + precision: Int16(chainAsset.asset.precision) + ) + } ?? .zero + } + case let .failure(error): + router.present(error: error, from: view, locale: selectedLocale) + } + } + + func didReceiveTransactionHash(_ hash: String) { + guard let utilityChainAsset = chain.utilityChainAssets().first else { + return + } + + router.complete(on: view, title: hash, chainAsset: utilityChainAsset) + } + + func didReceiveSubmitError(error: Error) { + router.present(error: error, from: view, locale: selectedLocale) + } +} + +// MARK: - Localizable + +extension LiquidityPoolSupplyConfirmPresenter: Localizable { + func applyLocalization() {} +} + +extension LiquidityPoolSupplyConfirmPresenter: LiquidityPoolSupplyConfirmModuleInput {} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmProtocols.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmProtocols.swift new file mode 100644 index 0000000000..010b7084bc --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmProtocols.swift @@ -0,0 +1,18 @@ +import SSFModels + +typealias LiquidityPoolSupplyConfirmModuleCreationResult = ( + view: LiquidityPoolSupplyConfirmViewInput, + input: LiquidityPoolSupplyConfirmModuleInput +) + +protocol LiquidityPoolSupplyConfirmRouterInput: AnyObject, BaseErrorPresentable, SheetAlertPresentable, AnyDismissable, AllDonePresentable { + func complete( + on view: ControllerBackedProtocol?, + title: String, + chainAsset: ChainAsset + ) +} + +protocol LiquidityPoolSupplyConfirmModuleInput: AnyObject {} + +protocol LiquidityPoolSupplyConfirmModuleOutput: AnyObject {} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmRouter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmRouter.swift new file mode 100644 index 0000000000..0db7a44e76 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmRouter.swift @@ -0,0 +1,28 @@ +import Foundation +import SSFModels +import SoraUI + +final class LiquidityPoolSupplyConfirmRouter: LiquidityPoolSupplyConfirmRouterInput { + func complete( + on view: ControllerBackedProtocol?, + title: String, + chainAsset: ChainAsset + ) { + let presenter = view?.controller.navigationController?.presentingViewController + + let controller = AllDoneAssembly.configureModule(chainAsset: chainAsset, hashString: title)?.view.controller + controller?.modalPresentationStyle = .custom + + let factory = ModalSheetBlurPresentationFactory( + configuration: ModalSheetPresentationConfiguration.fearlessBlur + ) + controller?.modalTransitioningFactory = factory + + view?.controller.navigationController?.dismiss(animated: true) { + if let presenter = presenter as? ControllerBackedProtocol, + let controller = controller { + presenter.controller.present(controller, animated: true) + } + } + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewController.swift new file mode 100644 index 0000000000..e3cad00513 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewController.swift @@ -0,0 +1,118 @@ +import UIKit +import SoraFoundation + +protocol LiquidityPoolSupplyConfirmViewOutput: AnyObject { + func didLoad(view: LiquidityPoolSupplyConfirmViewInput) + func handleViewAppeared() + func didTapBackButton() + func didTapApyInfo() + func didTapConfirmButton() +} + +final class LiquidityPoolSupplyConfirmViewController: UIViewController, ViewHolder, HiddableBarWhenPushed { + typealias RootViewType = LiquidityPoolSupplyConfirmViewLayout + + // MARK: Private properties + + private let output: LiquidityPoolSupplyConfirmViewOutput + + // MARK: - Constructor + + init( + output: LiquidityPoolSupplyConfirmViewOutput, + localizationManager: LocalizationManagerProtocol? + ) { + self.output = output + super.init(nibName: nil, bundle: nil) + self.localizationManager = localizationManager + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Life cycle + + override func loadView() { + view = LiquidityPoolSupplyConfirmViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + output.didLoad(view: self) + setupActions() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if isBeingPresented || isMovingToParent { + output.handleViewAppeared() + } + } + + // MARK: - Private methods + + private func setupActions() { + rootView.backButton.addTarget( + self, + action: #selector(handleTapBackButton), + for: .touchUpInside + ) + + rootView.confirmButton.addTarget( + self, + action: #selector(handleTapConfirmButton), + for: .touchUpInside + ) + } + + // MARK: - Private actions + + @objc private func handleTapBackButton() { + output.didTapBackButton() + } + + @objc private func handleTapApyInfo() { + output.didTapApyInfo() + } + + @objc private func handleTapConfirmButton() { + output.didTapConfirmButton() + } +} + +// MARK: - LiquidityPoolSupplyConfirmViewInput + +extension LiquidityPoolSupplyConfirmViewController: LiquidityPoolSupplyConfirmViewInput { + func didReceiveNetworkFee(fee: BalanceViewModelProtocol?) { + rootView.bind(feeViewModel: fee) + } + + func setButtonLoadingState(isLoading: Bool) { + rootView.confirmButton.set(loading: isLoading) + } + + func didUpdating() { + DispatchQueue.main.async { + self.rootView.confirmButton.set(enabled: false) + } + } + + func didReceiveViewModel(_ viewModel: LiquidityPoolSupplyViewModel) { + rootView.bind(viewModel: viewModel) + } + + func didReceiveConfirmationViewModel(_ viewModel: LiquidityPoolSupplyConfirmViewModel?) { + rootView.bind(confirmViewModel: viewModel) + } +} + +// MARK: - Localizable + +extension LiquidityPoolSupplyConfirmViewController: Localizable { + func applyLocalization() { + rootView.locale = selectedLocale + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewLayout.swift new file mode 100644 index 0000000000..9b61b05a5c --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewLayout.swift @@ -0,0 +1,201 @@ +import UIKit + +final class LiquidityPoolSupplyConfirmViewLayout: UIView { + private enum Constants { + static let navigationBarHeight: CGFloat = 56.0 + static let backButtonSize = CGSize(width: 32, height: 32) + } + + let navigationViewContainer = UIView() + + let backButton: UIButton = { + let button = UIButton() + button.setImage(R.image.iconBack(), for: .normal) + button.layer.masksToBounds = true + button.backgroundColor = R.color.colorWhite8() + return button + }() + + let titleLabel: UILabel = { + let label = UILabel() + label.font = .h3Title + label.textColor = R.color.colorWhite() + return label + }() + + let contentView: ScrollableContainerView = { + let view = ScrollableContainerView() + view.stackView.isLayoutMarginsRelativeArrangement = true + view.stackView.layoutMargins = UIEdgeInsets( + top: 24.0, + left: 0.0, + bottom: UIConstants.actionHeight + UIConstants.bigOffset * 2, + right: 0.0 + ) + view.stackView.spacing = UIConstants.bigOffset + return view + }() + + let doubleImageView = PolkaswapDoubleSymbolView() + let swapStubTitle: UILabel = { + let label = UILabel() + label.font = .p1Paragraph + label.textColor = R.color.colorStrokeGray() + label.numberOfLines = 3 + label.textAlignment = .center + return label + }() + + let amountsLabel: UILabel = { + let label = UILabel() + label.font = .h3Title + label.textColor = R.color.colorWhite() + label.numberOfLines = 2 + return label + }() + + let infoBackground = UIFactory.default.createInfoBackground() + let infoViewsStackView: UIStackView = { + let stackView = UIFactory.default.createVerticalStackView() + stackView.alignment = .center + stackView.spacing = 12 + return stackView + }() + + let slippageView = UIFactory.default.createConfirmationMultiView() + let swapRouteView: TitleMultiValueView = { + let view = UIFactory.default.createMultiView() + return view + }() + + let apyView = UIFactory.default.createConfirmationMultiView() + let networkFeeView = UIFactory.default.createConfirmationMultiView() + let rewardTokenView = UIFactory.default.createConfirmationMultiView() + + private lazy var multiViews = [ + slippageView, + apyView, + networkFeeView, + rewardTokenView + ] + + let confirmButton: TriangularedButton = { + let button = TriangularedButton() + button.applyEnabledStyle() + return button + }() + + var locale: Locale = .current { + didSet { + applyLocalization() + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = R.color.colorBlack19() + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + backButton.rounded() + } + + func bind(viewModel: LiquidityPoolSupplyViewModel) { + slippageView.bind(viewModel: viewModel.slippageViewModel) + apyView.bind(viewModel: viewModel.apyViewModel) + rewardTokenView.bind(viewModel: viewModel.rewardTokenViewModel) + } + + func bind(confirmViewModel: LiquidityPoolSupplyConfirmViewModel?) { + amountsLabel.attributedText = confirmViewModel?.amountsText + + if let doubleImageViewModel = confirmViewModel?.doubleImageViewViewModel { + doubleImageView.bind(viewModel: doubleImageViewModel) + } + } + + func bind(feeViewModel: BalanceViewModelProtocol?) { + networkFeeView.bindBalance(viewModel: feeViewModel) + } + + // MARK: - Private methods + + private func applyLocalization() { + titleLabel.text = R.string.localizable + .commonPreview(preferredLanguages: locale.rLanguages) + networkFeeView.titleLabel.text = R.string.localizable + .commonNetworkFee(preferredLanguages: locale.rLanguages) + confirmButton.imageWithTitleView?.title = R.string.localizable + .commonConfirm(preferredLanguages: locale.rLanguages) + swapStubTitle.text = "Output is estimated. If the price changes more than 0.5% your transaction will revert." + slippageView.titleLabel.text = R.string.localizable.polkaswapSettingsSlippageTitle(preferredLanguages: locale.rLanguages) + apyView.titleLabel.text = "Strategic Bonus APY" + rewardTokenView.titleLabel.text = "Rewards Payout In" + } + + private func setupLayout() { + func makeCommonConstraints(for view: UIView) { + view.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview() + } + } + + addSubview(navigationViewContainer) + navigationViewContainer.snp.makeConstraints { make in + make.top.leading.trailing.equalToSuperview() + make.height.equalTo(Constants.navigationBarHeight) + } + + navigationViewContainer.addSubview(backButton) + backButton.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.leading.equalTo(UIConstants.bigOffset) + make.size.equalTo(Constants.backButtonSize) + } + + navigationViewContainer.addSubview(titleLabel) + titleLabel.snp.makeConstraints { make in + make.center.equalToSuperview() + } + + addSubview(contentView) + addSubview(confirmButton) + + contentView.snp.makeConstraints { make in + make.top.equalTo(navigationViewContainer.snp.bottom) + make.leading.trailing.equalToSuperview().inset(UIConstants.bigOffset) + make.bottom.equalTo(safeAreaLayoutGuide) + } + + contentView.stackView.addArrangedSubview(infoBackground) + + infoViewsStackView.addArrangedSubview(doubleImageView) + infoViewsStackView.addArrangedSubview(amountsLabel) + infoViewsStackView.addArrangedSubview(swapStubTitle) + + infoBackground.addSubview(infoViewsStackView) + makeCommonConstraints(for: infoBackground) + infoViewsStackView.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(UIConstants.accessoryItemsSpacing) + make.top.bottom.equalToSuperview().inset(UIConstants.defaultOffset) + } + + multiViews.forEach { view in + infoViewsStackView.addArrangedSubview(view) + makeCommonConstraints(for: view) + } + + confirmButton.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(UIConstants.bigOffset) + make.bottom.equalToSuperview().inset(UIConstants.bigOffset) + make.height.equalTo(UIConstants.actionHeight) + } + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/ViewModel/LiquidityPoolSupplyConfirmViewModel.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/ViewModel/LiquidityPoolSupplyConfirmViewModel.swift new file mode 100644 index 0000000000..67e53f2691 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/ViewModel/LiquidityPoolSupplyConfirmViewModel.swift @@ -0,0 +1,6 @@ +import Foundation + +struct LiquidityPoolSupplyConfirmViewModel { + let amountsText: NSAttributedString + let doubleImageViewViewModel: PolkaswapDoubleSymbolViewModel +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/ViewModel/LiquidityPoolSupplyConfirmViewModelFactory.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/ViewModel/LiquidityPoolSupplyConfirmViewModelFactory.swift new file mode 100644 index 0000000000..e1831c5154 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/ViewModel/LiquidityPoolSupplyConfirmViewModelFactory.swift @@ -0,0 +1,100 @@ +import Foundation +import SSFPools +import SSFModels +import UIKit + +protocol LiquidityPoolSupplyConfirmViewModelFactory: LiquidityPoolSupplyViewModelFactory { + func buildViewModel( + baseAssetAmount: Decimal, + targetAssetAmount: Decimal, + liquidityPair: LiquidityPair, + chain: ChainModel, + locale: Locale + ) -> LiquidityPoolSupplyConfirmViewModel? +} + +final class LiquidityPoolSupplyConfirmViewModelFactoryDefault: LiquidityPoolSupplyViewModelFactoryDefault, LiquidityPoolSupplyConfirmViewModelFactory { + private enum Constants { + static let imageWidth: CGFloat = 8 + static let imageHeight: CGFloat = 14 + static let imageVerticalPosition: CGFloat = 6 + } + + func buildViewModel( + baseAssetAmount: Decimal, + targetAssetAmount: Decimal, + liquidityPair: LiquidityPair, + chain: ChainModel, + locale: Locale + ) -> LiquidityPoolSupplyConfirmViewModel? { + guard + let baseChainAsset = chain.chainAssets.first(where: { $0.asset.currencyId == liquidityPair.baseAssetId }), + let targetChainAsset = chain.chainAssets.first(where: { $0.asset.currencyId == liquidityPair.targetAssetId }) + else { + return nil + } + + let leftColor = HexColorConverter.hexStringToUIColor(hex: baseChainAsset.asset.color)?.cgColor + let rightColor = HexColorConverter.hexStringToUIColor(hex: targetChainAsset.asset.color)?.cgColor + let doubleImageViewViewModel = PolkaswapDoubleSymbolViewModel( + leftViewModel: baseChainAsset.asset.icon.map { RemoteImageViewModel(url: $0) }, + rightViewModel: targetChainAsset.asset.icon.map { RemoteImageViewModel(url: $0) }, + leftShadowColor: leftColor, + rightShadowColor: rightColor + ) + + let amountsText = buildAmountsText(baseAssetAmount: baseAssetAmount, targetAssetAmount: targetAssetAmount, baseChainAsset: baseChainAsset, targetChainAsset: targetChainAsset, locale: locale) + + return LiquidityPoolSupplyConfirmViewModel( + amountsText: amountsText, + doubleImageViewViewModel: doubleImageViewViewModel + ) + } + + private func buildAmountsText( + baseAssetAmount: Decimal, + targetAssetAmount: Decimal, + baseChainAsset: ChainAsset, + targetChainAsset: ChainAsset, + locale: Locale + ) -> NSMutableAttributedString { + let fromAmount = baseAssetAmount.toString(locale: locale) + let fromName = baseChainAsset.asset.symbolUppercased + let leftText = [fromAmount, fromName] + .compactMap { $0 } + .joined(separator: " ") + + let rightAmount = targetAssetAmount.toString(locale: locale) + let rightName = targetChainAsset.asset.symbolUppercased + let rightText = [rightAmount, rightName] + .compactMap { $0 } + .joined(separator: " ") + + let amountsTitle = insertArrow(in: [leftText, rightText]) + return amountsTitle + } + + private func insertArrow(in texts: [String]) -> NSMutableAttributedString { + let attributedString = NSMutableAttributedString() + + texts.enumerated().forEach { index, text in + attributedString.append(NSAttributedString(string: text)) + if index % 2 == 0 { + let imageAttachment = NSTextAttachment() + imageAttachment.image = R.image.iconSmallArrow() + imageAttachment.bounds = CGRect( + x: 0, + y: -Constants.imageVerticalPosition, + width: imageAttachment.image?.size.width ?? Constants.imageWidth, + height: imageAttachment.image?.size.height ?? Constants.imageHeight + ) + + let imageString = NSAttributedString(attachment: imageAttachment) + attributedString.append(imageString) + attributedString.append(NSAttributedString(string: " ")) + } + } + + return attributedString + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListViewModelFactory.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListViewModelFactory.swift index 6b830ba9a5..ef73faab05 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListViewModelFactory.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListViewModelFactory.swift @@ -88,7 +88,7 @@ final class AvailableLiquidityPoolsListViewModelFactoryDefault: AvailableLiquidi let filteredViewModels = type == .embed ? Array(poolViewModels.or([]).prefix(10)) : poolViewModels.or([]) return LiquidityPoolListViewModel( - poolViewModels: filteredViewModels, + poolViewModels: poolViewModels, titleLabelText: "Available pools", moreButtonVisible: type == .embed && (filteredViewModels.count < pairs?.count ?? 0), backgroundVisible: type == .full, diff --git a/fearlessTests/Modules/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmTests.swift b/fearlessTests/Modules/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmTests.swift new file mode 100644 index 0000000000..4f05c4ff8d --- /dev/null +++ b/fearlessTests/Modules/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmTests.swift @@ -0,0 +1,16 @@ +import XCTest + +class LiquidityPoolSupplyConfirmTests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + XCTFail("Did you forget to add tests?") + } +} From 87351c1aff9bbdcf1c5b4982570bbf37bf4c5343 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Thu, 6 Jun 2024 17:19:00 +0500 Subject: [PATCH 020/115] remote min amount for sora bridge --- fearless.xcodeproj/project.pbxproj | 4 +- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../Protocols/BaseErrorPresentable.swift | 12 +- .../EntityToModel/ChainModelMapper.swift | 5 +- .../SubstrateStorageVersion.swift | 3 + .../.xccurrentversion | 2 +- .../contents | 165 ++++++++++++++++++ .../Storage/SubstrateDataStorageFacade.swift | 2 +- .../Validators/ErrorConditionViolation.swift | 23 +++ .../CrossChain/CrossChainPresenter.swift | 4 +- .../SendDataValidatingFactory.swift | 63 +++---- fearless/Resources/chains.json | 21 ++- 12 files changed, 246 insertions(+), 60 deletions(-) create mode 100644 fearless/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v7.xcdatamodel/contents diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index 38ee57e282..66158d1a10 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -3211,6 +3211,7 @@ 07A4F5252B5004E60007C54A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = ""; }; 07A4F5262B5004E60007C54A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-PT"; path = "pt-PT.lproj/Localizable.stringsdict"; sourceTree = ""; }; 07AC51122AD8040C000970B8 /* XorlessTransfer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XorlessTransfer.swift; sourceTree = ""; }; + 07AFA26B2C119A5800CCE04D /* SubstrateDataModel_v7.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel_v7.xcdatamodel; sourceTree = ""; }; 07B018D028C7135400E05510 /* ScamInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScamInfo.swift; sourceTree = ""; }; 07B018D228C714B300E05510 /* ScamServiceOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScamServiceOperationFactory.swift; sourceTree = ""; }; 07B018DA28C9D66300E05510 /* ScamWarningExpandableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScamWarningExpandableView.swift; sourceTree = ""; }; @@ -19592,6 +19593,7 @@ FACE6C5D2BBAC3B000643CEF /* SubstrateDataModel.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + 07AFA26B2C119A5800CCE04D /* SubstrateDataModel_v7.xcdatamodel */, 07BE13CB2C071E7800EC7424 /* SubstrateDataModel_v6.xcdatamodel */, FACE6C5E2BBAC3B000643CEF /* SubstrateDataModel.xcdatamodel */, FACE6C5F2BBAC3B000643CEF /* SubstrateDataModel_v4.xcdatamodel */, @@ -19599,7 +19601,7 @@ FACE6C612BBAC3B000643CEF /* SubstrateDataModel_v5.xcdatamodel */, FACE6C622BBAC3B000643CEF /* SubstrateDataModel_v3.xcdatamodel */, ); - currentVersion = 07BE13CB2C071E7800EC7424 /* SubstrateDataModel_v6.xcdatamodel */; + currentVersion = 07AFA26B2C119A5800CCE04D /* SubstrateDataModel_v7.xcdatamodel */; path = SubstrateDataModel.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index 740d4d33f8..23c50d586a 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -133,7 +133,7 @@ "location" : "https://github.com/soramitsu/shared-features-spm.git", "state" : { "branch" : "feature/xcm-routes", - "revision" : "def77f0a4496d16421aa92e16fb14494f93bfb82" + "revision" : "f3737080f73453325fe436575b0f6c04d71e9e2a" } }, { diff --git a/fearless/Common/Protocols/BaseErrorPresentable.swift b/fearless/Common/Protocols/BaseErrorPresentable.swift index cb5758a175..4a65898beb 100644 --- a/fearless/Common/Protocols/BaseErrorPresentable.swift +++ b/fearless/Common/Protocols/BaseErrorPresentable.swift @@ -30,7 +30,6 @@ protocol BaseErrorPresentable { ) func presentSoraBridgeLowAmountError( from view: ControllerBackedProtocol, - originChainId: ChainModel.Id, locale: Locale, assetAmount: String ) @@ -225,19 +224,10 @@ extension BaseErrorPresentable where Self: SheetAlertPresentable & ErrorPresenta func presentSoraBridgeLowAmountError( from view: ControllerBackedProtocol, - originChainId: ChainModel.Id, locale: Locale, assetAmount: String ) { - let originKnownChain = Chain(chainId: originChainId) - let message: String? - switch originKnownChain { - case .kusama: - message = R.string.localizable.soraBridgeLowAmountAlert(preferredLanguages: locale.rLanguages) - default: - message = R.string.localizable.xcmLowAmauntAssetSymbolAlert(assetAmount, preferredLanguages: locale.rLanguages) - } - + let message: String? = R.string.localizable.xcmLowAmauntAssetSymbolAlert(assetAmount, preferredLanguages: locale.rLanguages) let title = R.string.localizable.commonAttention(preferredLanguages: locale.rLanguages) let closeTitle = R.string.localizable.commonCancel(preferredLanguages: locale.rLanguages) present(message: message, title: title, closeAction: closeTitle, from: view, actions: []) diff --git a/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift b/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift index d919b3b029..72dd672e87 100644 --- a/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift +++ b/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift @@ -283,7 +283,7 @@ final class ChainModelMapper { guard let id = entity.id, let symbol = entity.symbol else { return nil } - return XcmAvailableAsset(id: id, symbol: symbol) + return XcmAvailableAsset(id: id, symbol: symbol, minAmount: nil) } let availableXcmAssetDestinations = entity.xcmConfig?.availableDestinations?.allObjects as? [CDXcmAvailableDestination] ?? [] let destinations: [XcmAvailableDestination] = availableXcmAssetDestinations.compactMap { @@ -295,7 +295,7 @@ final class ChainModelMapper { guard let id = entity.id, let symbol = entity.symbol else { return nil } - return XcmAvailableAsset(id: id, symbol: symbol) + return XcmAvailableAsset(id: id, symbol: symbol, minAmount: entity.minAmount) } return XcmAvailableDestination( chainId: chainId, @@ -412,6 +412,7 @@ final class ChainModelMapper { let entity = CDXcmAvailableAsset(context: context) entity.id = $0.id entity.symbol = $0.symbol + entity.minAmount = $0.minAmount return entity } destinationEntity.assets = Set(availableAssets) as NSSet diff --git a/fearless/Common/Storage/Migration/SubstrateStorage/SubstrateStorageVersion.swift b/fearless/Common/Storage/Migration/SubstrateStorage/SubstrateStorageVersion.swift index 6759f57f27..6398f184e6 100644 --- a/fearless/Common/Storage/Migration/SubstrateStorage/SubstrateStorageVersion.swift +++ b/fearless/Common/Storage/Migration/SubstrateStorage/SubstrateStorageVersion.swift @@ -7,6 +7,7 @@ enum SubstrateStorageVersion: String, CaseIterable { case version4 = "SubstrateDataModel_v4" case version5 = "SubstrateDataModel_v5" case version6 = "SubstrateDataModel_v6" + case version7 = "SubstrateDataModel_v7" static var current: SubstrateStorageVersion { guard let currentVersion = allCases.last else { @@ -29,6 +30,8 @@ enum SubstrateStorageVersion: String, CaseIterable { case .version5: return .version6 case .version6: + return .version7 + case .version7: return nil } } diff --git a/fearless/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion b/fearless/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion index a1b7e22810..708a49b1ec 100644 --- a/fearless/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion +++ b/fearless/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - SubstrateDataModel_v6.xcdatamodel + SubstrateDataModel_v7.xcdatamodel diff --git a/fearless/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v7.xcdatamodel/contents b/fearless/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v7.xcdatamodel/contents new file mode 100644 index 0000000000..bcbad9cd6a --- /dev/null +++ b/fearless/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v7.xcdatamodel/contents @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fearless/Common/Storage/SubstrateDataStorageFacade.swift b/fearless/Common/Storage/SubstrateDataStorageFacade.swift index 464bce9fb8..fe13c417b4 100644 --- a/fearless/Common/Storage/SubstrateDataStorageFacade.swift +++ b/fearless/Common/Storage/SubstrateDataStorageFacade.swift @@ -2,7 +2,7 @@ import RobinHood import CoreData enum SubstrateStorageParams { - static let modelVersion: SubstrateStorageVersion = .version6 + static let modelVersion: SubstrateStorageVersion = .version7 static let modelDirectory: String = "SubstrateDataModel.momd" static let databaseName = "SubstrateDataModel.sqlite" diff --git a/fearless/Common/Validation/Validators/ErrorConditionViolation.swift b/fearless/Common/Validation/Validators/ErrorConditionViolation.swift index 98b5f2e900..03f4297038 100644 --- a/fearless/Common/Validation/Validators/ErrorConditionViolation.swift +++ b/fearless/Common/Validation/Validators/ErrorConditionViolation.swift @@ -22,3 +22,26 @@ final class ErrorConditionViolation: DataValidating { return .error } } + +final class ErrorThrowingViolation: DataValidating { + private let preservesCondition: () -> String? + private let onError: (_ text: String) -> Void + + init( + onError: @escaping (_ text: String) -> Void, + preservesCondition: @escaping () -> String? + ) { + self.preservesCondition = preservesCondition + self.onError = onError + } + + func validate(notifying _: DataValidatingDelegate) -> DataValidationProblem? { + guard let textError = preservesCondition() else { + return nil + } + + onError(textError) + + return .error + } +} diff --git a/fearless/Modules/CrossChain/CrossChainPresenter.swift b/fearless/Modules/CrossChain/CrossChainPresenter.swift index 32c452c3ba..a1f29b4f81 100644 --- a/fearless/Modules/CrossChain/CrossChainPresenter.swift +++ b/fearless/Modules/CrossChain/CrossChainPresenter.swift @@ -356,8 +356,8 @@ final class CrossChainPresenter { ) let soraBridgeViolated = dataValidatingFactory.soraBridgeViolated( - originCHainId: selectedOriginChainModel.chainId, - destChainId: selectedDestChainModel?.chainId, + originCHain: selectedOriginChainModel, + destChain: selectedDestChainModel, amount: inputAmountDecimal, locale: selectedLocale, asset: selectedAmountChainAsset.asset diff --git a/fearless/Modules/Send/Validators/SendDataValidatingFactory.swift b/fearless/Modules/Send/Validators/SendDataValidatingFactory.swift index d13e59c074..4db85fa013 100644 --- a/fearless/Modules/Send/Validators/SendDataValidatingFactory.swift +++ b/fearless/Modules/Send/Validators/SendDataValidatingFactory.swift @@ -1,4 +1,5 @@ import Foundation +import SSFXCM import SoraFoundation import BigInt import SSFModels @@ -26,6 +27,10 @@ enum ExistentialDepositValidationParameters { } class SendDataValidatingFactory: NSObject { + private lazy var xcmAmountInspector: XcmMinAmountInspector = { + XcmMinAmountInspectorImpl() + }() + weak var view: (Localizable & ControllerBackedProtocol)? var basePresentable: BaseErrorPresentable @@ -228,53 +233,43 @@ class SendDataValidatingFactory: NSObject { } func soraBridgeViolated( - originCHainId: ChainModel.Id, - destChainId: ChainModel.Id?, + originCHain: ChainModel, + destChain: ChainModel?, amount: Decimal, locale: Locale, asset: AssetModel ) -> DataValidating { - ErrorConditionViolation(onError: { [weak self] in - guard let self, let view = self.view, let destChainId else { + ErrorThrowingViolation(onError: { [weak self] errorText in + guard let self, let view = self.view else { return } - let assetAmount = self.minAssetAmount(originCHainId: originCHainId, destChainId: destChainId) - self.basePresentable.presentSoraBridgeLowAmountError( from: view, - originChainId: originCHainId, locale: locale, - assetAmount: assetAmount + assetAmount: errorText ) - }, preservesCondition: { - guard let destChainId = destChainId else { - return false + }, preservesCondition: { [weak self] in + guard + let self, + let destChain, + let substrateAmount = amount.toSubstrateAmount(precision: Int16(asset.precision)) + else { + return nil } - let originKnownChain = Chain(chainId: originCHainId) - let destKnownChain = Chain(chainId: destChainId) - - switch (originKnownChain, destKnownChain) { - case (.kusama, .soraMain): - return amount >= 0.05 - case (.polkadot, .soraMain), (.soraMain, .polkadot): - return amount >= 1.1 - case (.liberland, .soraMain): - guard asset.symbol.lowercased() == "lld" else { - return true - } - return amount >= 1.0 - case (.soraMain, .liberland): - guard asset.symbol.lowercased() == "lld" else { - return true + do { + try self.xcmAmountInspector.inspectMin( + amount: substrateAmount, + fromChainModel: originCHain, + destChainModel: destChain, + assetSymbol: asset.symbol + ) + return nil + } catch { + guard let xcmError = error as? XcmError, case let .minAmountError(minAmount) = xcmError else { + return nil } - return amount >= 1.0 - case (.soraMain, .acala): - return amount >= 1.0 - case (.acala, .soraMain): - return amount >= 56.0 - default: - return true + return minAmount } }) } diff --git a/fearless/Resources/chains.json b/fearless/Resources/chains.json index 6c71ca2159..53f28be957 100644 --- a/fearless/Resources/chains.json +++ b/fearless/Resources/chains.json @@ -106,7 +106,8 @@ "assets": [ { "id": "99e66d4f-00cd-4d73-bd1b-3adadcacffb2", - "symbol": "DOT" + "symbol": "DOT", + "minAmount": "11000000000" } ], @@ -228,7 +229,8 @@ "assets": [ { "id": "0ceffe96-8090-404e-815c-91118ee5dd65", - "symbol": "KSM" + "symbol": "KSM", + "minAmount": "50000000000" } ], @@ -826,7 +828,8 @@ "assets": [ { "id": "7528bb5b-2aa9-4a70-a4ed-3476aec0f87d", - "symbol": "ACA" + "symbol": "ACA", + "minAmount": "56000000000000" } ], @@ -5403,7 +5406,8 @@ "assets": [ { "id": "cd092a5a-4eb6-4318-9f11-4bf8454d67a2", - "symbol": "DOT" + "symbol": "DOT", + "minAmount": "1100000000000000000" } ] }, @@ -5412,7 +5416,8 @@ "assets": [ { "id": "fb88fa55-b8c8-4ff1-afa8-f72a86a238a4", - "symbol": "ACA" + "symbol": "ACA", + "minAmount": "1000000000000000000" } ] }, @@ -5421,7 +5426,8 @@ "assets": [ { "id": "a6b83d39-a488-4b34-8352-280705a792ea", - "symbol": "LLD" + "symbol": "LLD", + "minAmount": "1000000000000000000" }, { "id": "b774c386-5cce-454a-a845-1ec0381538ec", @@ -7525,7 +7531,8 @@ "assets": [ { "id": "a6b83d39-a488-4b34-8352-280705a792e", - "symbol": "LLD" + "symbol": "LLD", + "minAmount": "1000000000000" }, { "id": "30b43eb9-36b4-4b40-bf72-330d2e20ee86", From e40cc31814a6e02fc1d1e0ace450de3249afe9f4 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Thu, 6 Jun 2024 17:40:13 +0500 Subject: [PATCH 021/115] [#FLW-4667] We can see test networks on release version --- .../WalletBalanceSubscriptionAdapter.swift | 1 + .../Common/Storage/Migration/AssetManagementMigrator.swift | 4 +++- .../Modules/AssetManagement/AssetManagementAssembly.swift | 4 +++- fearless/Modules/AssetNetworks/AssetNetworksAssembly.swift | 1 + fearless/Modules/BackupWallet/BackupWalletAssembly.swift | 1 + fearless/Modules/CrossChain/CrossChainAssembly.swift | 1 + .../NewWallet/ChainAccount/ChainAccountViewFactory.swift | 2 +- .../NewWallet/ChainAssetList/ChainAssetListAssembly.swift | 1 + .../ChainAssetList/ChainAssetListDependencyContainer.swift | 1 + fearless/Modules/Profile/ProfileViewFactory.swift | 1 + fearless/Modules/SelectAsset/SelectAssetAssembly.swift | 1 + fearless/Modules/Send/SendAssembly.swift | 1 + fearless/Modules/Staking/Services/StakingServiceFactory.swift | 1 + .../Modules/Staking/StakingMain/StakingMainViewFactory.swift | 1 + fearless/Modules/WalletOption/WalletOptionAssembly.swift | 1 + .../Modules/WalletsManagment/WalletsManagmentAssembly.swift | 1 + 16 files changed, 20 insertions(+), 3 deletions(-) diff --git a/fearless/ApplicationLayer/Services/Balance/WalletBalanceSubscription/WalletBalanceSubscriptionAdapter.swift b/fearless/ApplicationLayer/Services/Balance/WalletBalanceSubscription/WalletBalanceSubscriptionAdapter.swift index 5f21f5e686..2754671f37 100644 --- a/fearless/ApplicationLayer/Services/Balance/WalletBalanceSubscription/WalletBalanceSubscriptionAdapter.swift +++ b/fearless/ApplicationLayer/Services/Balance/WalletBalanceSubscription/WalletBalanceSubscriptionAdapter.swift @@ -450,6 +450,7 @@ extension WalletBalanceSubscriptionAdapter: PriceLocalSubscriptionHandler { private extension WalletBalanceSubscriptionAdapter { static func createWalletBalanceAdapter() -> WalletBalanceSubscriptionAdapter { let chainRepository = ChainRepositoryFactory().createRepository( + for: NSPredicate.enabledCHain(), sortDescriptors: [NSSortDescriptor.chainsByAddressPrefix] ) let accountRepositoryFactory = AccountRepositoryFactory(storageFacade: UserDataStorageFacade.shared) diff --git a/fearless/Common/Storage/Migration/AssetManagementMigrator.swift b/fearless/Common/Storage/Migration/AssetManagementMigrator.swift index 1b5f068765..74712284bd 100644 --- a/fearless/Common/Storage/Migration/AssetManagementMigrator.swift +++ b/fearless/Common/Storage/Migration/AssetManagementMigrator.swift @@ -158,7 +158,9 @@ enum AssetManagementMigratorAssembly { let walletRepository = AccountRepositoryFactory.createRepository() - let chainRepository = ChainRepositoryFactory().createRepository() + let chainRepository = ChainRepositoryFactory().createRepository( + for: NSPredicate.enabledCHain() + ) let chainAssetFetching = ChainAssetsFetching( chainRepository: AnyDataProviderRepository(chainRepository), operationQueue: OperationManagerFacade.sharedDefaultQueue diff --git a/fearless/Modules/AssetManagement/AssetManagementAssembly.swift b/fearless/Modules/AssetManagement/AssetManagementAssembly.swift index d78a1a455f..5fe155b77b 100644 --- a/fearless/Modules/AssetManagement/AssetManagementAssembly.swift +++ b/fearless/Modules/AssetManagement/AssetManagementAssembly.swift @@ -13,7 +13,9 @@ final class AssetManagementAssembly { let localizationManager = LocalizationManager.shared let priceLocalSubscriber = PriceLocalStorageSubscriberImpl.shared - let chainRepository = ChainRepositoryFactory().createRepository() + let chainRepository = ChainRepositoryFactory().createRepository( + for: NSPredicate.enabledCHain() + ) let chainAssetFetching = ChainAssetsFetching( chainRepository: AnyDataProviderRepository(chainRepository), operationQueue: OperationManagerFacade.sharedDefaultQueue diff --git a/fearless/Modules/AssetNetworks/AssetNetworksAssembly.swift b/fearless/Modules/AssetNetworks/AssetNetworksAssembly.swift index d3430dde8b..0ae4799947 100644 --- a/fearless/Modules/AssetNetworks/AssetNetworksAssembly.swift +++ b/fearless/Modules/AssetNetworks/AssetNetworksAssembly.swift @@ -9,6 +9,7 @@ final class AssetNetworksAssembly { let priceLocalSubscriber = PriceLocalStorageSubscriberImpl.shared let chainRepository = ChainRepositoryFactory().createRepository( + for: NSPredicate.enabledCHain(), sortDescriptors: [NSSortDescriptor.chainsByAddressPrefix] ) let chainAssetFetching = ChainAssetsFetching( diff --git a/fearless/Modules/BackupWallet/BackupWalletAssembly.swift b/fearless/Modules/BackupWallet/BackupWalletAssembly.swift index 5b12d0608b..5a14e709dd 100644 --- a/fearless/Modules/BackupWallet/BackupWalletAssembly.swift +++ b/fearless/Modules/BackupWallet/BackupWalletAssembly.swift @@ -13,6 +13,7 @@ final class BackupWalletAssembly { let priceLocalSubscriber = PriceLocalStorageSubscriberImpl.shared let chainRepository = ChainRepositoryFactory().createRepository( + for: NSPredicate.enabledCHain(), sortDescriptors: [NSSortDescriptor.chainsByAddressPrefix] ) let accountRepositoryFactory = AccountRepositoryFactory(storageFacade: UserDataStorageFacade.shared) diff --git a/fearless/Modules/CrossChain/CrossChainAssembly.swift b/fearless/Modules/CrossChain/CrossChainAssembly.swift index dde9e07e33..ea434c3561 100644 --- a/fearless/Modules/CrossChain/CrossChainAssembly.swift +++ b/fearless/Modules/CrossChain/CrossChainAssembly.swift @@ -22,6 +22,7 @@ final class CrossChainAssembly { ) let chainRepository = ChainRepositoryFactory().createRepository( + for: NSPredicate.enabledCHain(), sortDescriptors: [NSSortDescriptor.chainsByAddressPrefix] ) let operationQueue = OperationQueue() diff --git a/fearless/Modules/NewWallet/ChainAccount/ChainAccountViewFactory.swift b/fearless/Modules/NewWallet/ChainAccount/ChainAccountViewFactory.swift index 7bc5f9acee..e7ec9fb734 100644 --- a/fearless/Modules/NewWallet/ChainAccount/ChainAccountViewFactory.swift +++ b/fearless/Modules/NewWallet/ChainAccount/ChainAccountViewFactory.swift @@ -23,6 +23,7 @@ enum ChainAccountViewFactory { let operationManager = OperationManagerFacade.sharedManager let eventCenter = EventCenter.shared let chainRepository = ChainRepositoryFactory().createRepository( + for: NSPredicate.enabledCHain(), sortDescriptors: [NSSortDescriptor.chainsByAddressPrefix] ) @@ -44,7 +45,6 @@ enum ChainAccountViewFactory { operationManager: operationManager ) - let accountRepositoryFactory = AccountRepositoryFactory(storageFacade: UserDataStorageFacade.shared) let walletBalanceSubscriptionAdapter = WalletBalanceSubscriptionAdapter.shared let ethereumBalanceRepositoryCacheWrapper = EthereumBalanceRepositoryCacheWrapper( diff --git a/fearless/Modules/NewWallet/ChainAssetList/ChainAssetListAssembly.swift b/fearless/Modules/NewWallet/ChainAssetList/ChainAssetListAssembly.swift index 4b0d28be90..e7f55633ad 100644 --- a/fearless/Modules/NewWallet/ChainAssetList/ChainAssetListAssembly.swift +++ b/fearless/Modules/NewWallet/ChainAssetList/ChainAssetListAssembly.swift @@ -36,6 +36,7 @@ final class ChainAssetListAssembly { repositoryWrapper: ethereumBalanceRepositoryCacheWrapper ) let chainRepository = ChainRepositoryFactory().createRepository( + for: NSPredicate.enabledCHain(), sortDescriptors: [NSSortDescriptor.chainsByAddressPrefix] ) let chainAssetFetching = ChainAssetsFetching( diff --git a/fearless/Modules/NewWallet/ChainAssetList/ChainAssetListDependencyContainer.swift b/fearless/Modules/NewWallet/ChainAssetList/ChainAssetListDependencyContainer.swift index 271bd9e559..9bda654e1d 100644 --- a/fearless/Modules/NewWallet/ChainAssetList/ChainAssetListDependencyContainer.swift +++ b/fearless/Modules/NewWallet/ChainAssetList/ChainAssetListDependencyContainer.swift @@ -15,6 +15,7 @@ final class ChainAssetListDependencyContainer { } let chainRepository = ChainRepositoryFactory().createRepository( + for: NSPredicate.enabledCHain(), sortDescriptors: [NSSortDescriptor.chainsByAddressPrefix] ) diff --git a/fearless/Modules/Profile/ProfileViewFactory.swift b/fearless/Modules/Profile/ProfileViewFactory.swift index 5ab3c309a4..21dcfd1610 100644 --- a/fearless/Modules/Profile/ProfileViewFactory.swift +++ b/fearless/Modules/Profile/ProfileViewFactory.swift @@ -25,6 +25,7 @@ final class ProfileViewFactory: ProfileViewFactoryProtocol { let accountRepository = accountRepositoryFactory.createMetaAccountRepository(for: nil, sortDescriptors: []) let chainRepository = ChainRepositoryFactory().createRepository( + for: NSPredicate.enabledCHain(), sortDescriptors: [NSSortDescriptor.chainsByAddressPrefix] ) diff --git a/fearless/Modules/SelectAsset/SelectAssetAssembly.swift b/fearless/Modules/SelectAsset/SelectAssetAssembly.swift index 99c7d9ebda..9a433b1171 100644 --- a/fearless/Modules/SelectAsset/SelectAssetAssembly.swift +++ b/fearless/Modules/SelectAsset/SelectAssetAssembly.swift @@ -20,6 +20,7 @@ final class SelectAssetAssembly { mapper: AnyCoreDataMapper(AssetModelMapper()) ) let chainRepository = ChainRepositoryFactory().createRepository( + for: NSPredicate.enabledCHain(), sortDescriptors: [NSSortDescriptor.chainsByAddressPrefix] ) diff --git a/fearless/Modules/Send/SendAssembly.swift b/fearless/Modules/Send/SendAssembly.swift index 9dbf2e0c5c..02e7f7072c 100644 --- a/fearless/Modules/Send/SendAssembly.swift +++ b/fearless/Modules/Send/SendAssembly.swift @@ -39,6 +39,7 @@ final class SendAssembly { repository: AnyDataProviderRepository(scamRepository) ) let chainRepository = ChainRepositoryFactory().createRepository( + for: NSPredicate.enabledCHain(), sortDescriptors: [NSSortDescriptor.chainsByAddressPrefix] ) let operationQueue = OperationQueue() diff --git a/fearless/Modules/Staking/Services/StakingServiceFactory.swift b/fearless/Modules/Staking/Services/StakingServiceFactory.swift index 4af14bd42c..75963b2045 100644 --- a/fearless/Modules/Staking/Services/StakingServiceFactory.swift +++ b/fearless/Modules/Staking/Services/StakingServiceFactory.swift @@ -170,6 +170,7 @@ final class StakingServiceFactory: StakingServiceFactoryProtocol { } let chainRepository = ChainRepositoryFactory().createRepository( + for: NSPredicate.enabledCHain(), sortDescriptors: [NSSortDescriptor.chainsByAddressPrefix] ) diff --git a/fearless/Modules/Staking/StakingMain/StakingMainViewFactory.swift b/fearless/Modules/Staking/StakingMain/StakingMainViewFactory.swift index 8bed0e257b..4710f4f5ec 100644 --- a/fearless/Modules/Staking/StakingMain/StakingMainViewFactory.swift +++ b/fearless/Modules/Staking/StakingMain/StakingMainViewFactory.swift @@ -182,6 +182,7 @@ final class StakingMainViewFactory: StakingMainViewFactoryProtocol { ) let chainRepository = ChainRepositoryFactory().createRepository( + for: NSPredicate.enabledCHain(), sortDescriptors: [NSSortDescriptor.chainsByAddressPrefix] ) diff --git a/fearless/Modules/WalletOption/WalletOptionAssembly.swift b/fearless/Modules/WalletOption/WalletOptionAssembly.swift index cdeb066be8..355e65865d 100644 --- a/fearless/Modules/WalletOption/WalletOptionAssembly.swift +++ b/fearless/Modules/WalletOption/WalletOptionAssembly.swift @@ -17,6 +17,7 @@ final class WalletOptionAssembly { ) let chainRepository = ChainRepositoryFactory().createRepository( + for: NSPredicate.enabledCHain(), sortDescriptors: [NSSortDescriptor.chainsByAddressPrefix] ) diff --git a/fearless/Modules/WalletsManagment/WalletsManagmentAssembly.swift b/fearless/Modules/WalletsManagment/WalletsManagmentAssembly.swift index 03f1847d59..5c1b3672e1 100644 --- a/fearless/Modules/WalletsManagment/WalletsManagmentAssembly.swift +++ b/fearless/Modules/WalletsManagment/WalletsManagmentAssembly.swift @@ -26,6 +26,7 @@ final class WalletsManagmentAssembly { let priceLocalSubscriber = PriceLocalStorageSubscriberImpl.shared let chainRepository = ChainRepositoryFactory().createRepository( + for: NSPredicate.enabledCHain(), sortDescriptors: [NSSortDescriptor.chainsByAddressPrefix] ) From 04b694c5aac3a697e43fab169316afed4cc9267f Mon Sep 17 00:00:00 2001 From: Alex Lebedko Date: Fri, 7 Jun 2024 12:09:47 +0700 Subject: [PATCH 022/115] supply ui fixes --- .../iconAddTokenPair.imageset/Contents.json | 12 ++++ .../icon_add_token_pair.pdf | Bin 0 -> 3033 bytes .../AmountInputView/AmountInputViewV2.swift | 2 +- .../View/PolkaswapDoubleSymbolView.swift | 62 +++++++++++++----- .../LiquidityPoolDetailsViewLayout.swift | 2 +- ...LiquidityPoolDetailsViewModelFactory.swift | 3 +- .../LiquidityPoolSupplyPresenter.swift | 10 +-- .../LiquidityPoolSupplyViewController.swift | 7 ++ .../LiquidityPoolSupplyViewLayout.swift | 56 ++++++++++++---- .../LiquidityPoolSupplyViewModel.swift | 1 + .../LiquidityPoolSupplyViewModelFactory.swift | 8 ++- .../LiquidityPoolSupplyConfirmPresenter.swift | 12 +++- ...idityPoolSupplyConfirmViewController.swift | 7 ++ ...LiquidityPoolSupplyConfirmViewLayout.swift | 35 +++++++++- 14 files changed, 178 insertions(+), 39 deletions(-) create mode 100644 fearless/Assets.xcassets/iconAddTokenPair.imageset/Contents.json create mode 100644 fearless/Assets.xcassets/iconAddTokenPair.imageset/icon_add_token_pair.pdf diff --git a/fearless/Assets.xcassets/iconAddTokenPair.imageset/Contents.json b/fearless/Assets.xcassets/iconAddTokenPair.imageset/Contents.json new file mode 100644 index 0000000000..af118dbf0c --- /dev/null +++ b/fearless/Assets.xcassets/iconAddTokenPair.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "icon_add_token_pair.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fearless/Assets.xcassets/iconAddTokenPair.imageset/icon_add_token_pair.pdf b/fearless/Assets.xcassets/iconAddTokenPair.imageset/icon_add_token_pair.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ebde11c543f310edab2c7a059c79754431a5c2eb GIT binary patch literal 3033 zcmdT`X;c$e6jnjR5tU6u)We_$g<41^D+s6<0=0l(zzwu88Ac6+3?>5_wA3Pq)M~{l zv=va1qa5qPq5>6)U_f!BsJPUCOY3n1#S<0WurrfnhE!}%fA;-I?)%=o-+S-QU7l-r zK%fWYDFgrrWP_MA9sogKUyv0jljEq8_?N>t8h|2L97?PZgB7VxLP6G|FljuB;07!N zRV>F>fE}ywC3#D*lI8uWP7ry!3bQA5n`mJ1QlVa!j&kT2&^8wd3)#;_I%Vs0`AXM^d&U-zE$Fj-KV$!{nYC|UscY80dkeqm z_3&+pIUU_QQ4;cHBA!^-q{xRF+ZWs{wD3u*&;9B6r&Yaax|Eu`jyFHE-$O*wKLV9%}uj7o?FBheyqAjje8Ql-1EG-nTuf@;EoH z_qK{EPs^`mRCZ5ZJNn3-bMFu2y^0JI)>>=E_w==Ev!}8>eRj_a9pXs!f-LWVSo+e$DKjA^t+26N?EGxh*)?anp z=`$sz?BqJmLi3;%?xE+0+U4>6bul?bPUBI_3Ws3pWa*3Cw&GkcOxlzhOyHS7g51vWj zM`V3*ls|NRhV)talOO8p>e<`wkAKl|9hrStP}SjdIbj@jv@vez!x5Te&T->%9y>o~ zxweW|0;nR+5K4qZCK5oVgFd79LIJr>pi?0rZ<8 zDqtl*=O<*$N}>dPkg|}cH#r2L2d$4JGPX2G3o1mxEJFe#V?P@X&(n)5;Pd&Q&=V4H zc|0D-)C0zX!4O~|R1Ppij4mS$w94=x0HevE@_wk{aD0#TqQ<^co5IT@(>IOhp@be!{Pl#Fvjzn~hzA_yd#jfAP&;?;Oi z1Wt4sYF*5s{t%d~-(F-~(3(u^ASIRC=gvnzRyAGiJsL0?BkV^ehG+ zP+}3y%3q$e0{(YuV?!F4M}J8A&L5*2fP{jGA7d>MnE3ePJ=@Tqg@(Hl40P8^{5}*r z1zK%*+Be2K?33ZvpQW}(b!odw+voMT`0sbWl=eO(uNTdH9Jn}iIE#DW zM(Ta5@oAY-8#SJ-A#U{pq}DluQqQg#yD>U^MwA zA%>wXgu}~?udYxuP6q$i*A2>8$W)m$)z$VJr-yja*9aG5r149mvO_UH5 zEe?-E_}UcWdlT+5g#<$85V#VS$x$Uhx_3?h;aoQrmWaB8Zn8Lcy^C2A3?uwYx{vx^ btO!{uO8SCIW>|?+-r)$j9Kh9892WQ&sWp^+ literal 0 HcmV?d00001 diff --git a/fearless/Common/View/AmountInputView/AmountInputViewV2.swift b/fearless/Common/View/AmountInputView/AmountInputViewV2.swift index 570a5ea58b..30f52c68c6 100644 --- a/fearless/Common/View/AmountInputView/AmountInputViewV2.swift +++ b/fearless/Common/View/AmountInputView/AmountInputViewV2.swift @@ -20,7 +20,7 @@ final class AmountInputViewV2: UIView { view.fillColor = R.color.colorSemiBlack()! view.highlightedFillColor = R.color.colorSemiBlack()! - + view.shadowColor = .clear view.strokeColor = R.color.colorWhite8()! view.highlightedStrokeColor = R.color.colorWhite8()! view.strokeWidth = 0.5 diff --git a/fearless/Common/View/PolkaswapDoubleSymbolView.swift b/fearless/Common/View/PolkaswapDoubleSymbolView.swift index bc0303540a..acfe207fa5 100644 --- a/fearless/Common/View/PolkaswapDoubleSymbolView.swift +++ b/fearless/Common/View/PolkaswapDoubleSymbolView.swift @@ -8,6 +8,11 @@ struct PolkaswapDoubleSymbolViewModel { let rightShadowColor: CGColor? } +enum PolkaswapDoubleSymbolViewMode { + case filled + case centered +} + final class PolkaswapDoubleSymbolView: UIView { private enum Constants { static let imageViewSize = CGSize(width: 41, height: 41) @@ -22,6 +27,16 @@ final class PolkaswapDoubleSymbolView: UIView { private let leftImageView = UIImageView() private let rightImageView = UIImageView() + private var imageSize: CGSize? + private var mode: PolkaswapDoubleSymbolViewMode = .centered + + init(imageSize: CGSize?, mode: PolkaswapDoubleSymbolViewMode) { + self.imageSize = imageSize + self.mode = mode + super.init(frame: .zero) + setupLayout() + } + override init(frame: CGRect) { super.init(frame: frame) setupLayout() @@ -39,14 +54,15 @@ final class PolkaswapDoubleSymbolView: UIView { } func bind(viewModel: PolkaswapDoubleSymbolViewModel) { + let size = imageSize ?? Constants.imageViewSize viewModel.rightViewModel?.loadImage( on: rightImageView, - targetSize: Constants.imageViewSize, + targetSize: size, animated: true ) viewModel.leftViewModel?.loadImage( on: leftImageView, - targetSize: Constants.imageViewSize, + targetSize: size, animated: true ) leftContainer.layer.shadowColor = viewModel.leftShadowColor @@ -54,19 +70,38 @@ final class PolkaswapDoubleSymbolView: UIView { } private func setupLayout() { - leftContainer.backgroundColor = R.color.colorBlack() - rightContainer.backgroundColor = R.color.colorBlack() + let size = imageSize ?? Constants.imageViewSize - leftContainer.addSubview(leftImageView) - leftImageView.snp.makeConstraints { make in - make.edges.equalToSuperview().inset(Constants.imageViewInset) - make.size.equalTo(Constants.imageViewSize) - } + addSubview(rightContainer) + addSubview(leftContainer) + leftContainer.addSubview(leftImageView) rightContainer.addSubview(rightImageView) - rightImageView.snp.makeConstraints { make in - make.edges.equalToSuperview().inset(Constants.imageViewInset) - make.size.equalTo(Constants.imageViewSize) + + switch mode { + case .filled: + leftImageView.snp.makeConstraints { make in + make.edges.equalToSuperview() + make.size.equalTo(size) + } + + rightImageView.snp.makeConstraints { make in + make.edges.equalToSuperview() + make.size.equalTo(size) + } + case .centered: + leftContainer.backgroundColor = R.color.colorBlack() + rightContainer.backgroundColor = R.color.colorBlack() + + leftImageView.snp.makeConstraints { make in + make.edges.equalToSuperview().inset(Constants.imageViewInset) + make.size.equalTo(size) + } + + rightImageView.snp.makeConstraints { make in + make.edges.equalToSuperview().inset(Constants.imageViewInset) + make.size.equalTo(size) + } } [leftContainer, rightContainer].forEach { view in @@ -74,9 +109,6 @@ final class PolkaswapDoubleSymbolView: UIView { view.layer.shadowOpacity = Constants.shadowOpacity } - addSubview(rightContainer) - addSubview(leftContainer) - leftContainer.snp.makeConstraints { make in make.top.leading.bottom.equalToSuperview() make.trailing.equalTo(rightContainer.snp.leading).offset(Constants.imagesInset) diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift index 2ae5f9b23e..478f10f762 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift @@ -18,7 +18,7 @@ final class LiquidityPoolDetailsViewLayout: UIView { return view }() - let doubleImageView = PolkaswapDoubleSymbolView() + let doubleImageView = PolkaswapDoubleSymbolView(imageSize: CGSize(width: 64, height: 64), mode: .filled) let pairTitleLabel: UILabel = { let label = UILabel() label.font = .h3Title diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/ViewModel/LiquidityPoolDetailsViewModelFactory.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/ViewModel/LiquidityPoolDetailsViewModelFactory.swift index 5c219ebb84..2f04abe96c 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/ViewModel/LiquidityPoolDetailsViewModelFactory.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/ViewModel/LiquidityPoolDetailsViewModelFactory.swift @@ -63,7 +63,6 @@ final class LiquidityPoolDetailsViewModelFactoryDefault: LiquidityPoolDetailsVie targetAssetPrice: targetAssetPrice ) let reservesString = reservesValue.flatMap { fiatFormatter.stringFromDecimal($0) } - let reservesLabelText: String? = reservesString.flatMap { "\($0) TVL" } let apyLabelText = apyInfo?.apy.flatMap { NumberFormatter.percentAPY.stringFromDecimal($0) } let tokenPairsIconViewModel = PolkaswapDoubleSymbolViewModel( @@ -83,7 +82,7 @@ final class LiquidityPoolDetailsViewModelFactoryDefault: LiquidityPoolDetailsVie let targetAssetViewModel = accountPoolInfo?.targetAssetPooled.flatMap { targetAssetBalanceViewModelFactory.balanceFromPrice($0, priceData: targetAssetPrice, usageCase: .detailsCrypto) } - let reservesViewModel = reservesLabelText.flatMap { TitleMultiValueViewModel(title: $0, subtitle: nil) } + let reservesViewModel = reservesString.flatMap { TitleMultiValueViewModel(title: $0, subtitle: nil) } let apyViewModel = apyLabelText.flatMap { TitleMultiValueViewModel(title: $0, subtitle: nil) } return LiquidityPoolDetailsViewModel( diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift index 1c2acd7c71..def22cf5ca 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift @@ -294,10 +294,8 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { func didTapApyInfo() { var infoText: String var infoTitle: String - infoTitle = R.string.localizable - .polkaswapMinReceived(preferredLanguages: selectedLocale.rLanguages) - infoText = R.string.localizable - .polkaswapMinimumReceivedInfo(preferredLanguages: selectedLocale.rLanguages) + infoTitle = "Strategic Bonus APY" + infoText = "Farming reward for liquidity provision" router.presentInfo( message: infoText, title: infoTitle, @@ -501,7 +499,9 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyInteractorOutput { let targetAssetPriceValue = Decimal(string: targetAssetPrice.price) { baseTargetRate = baseAssetPriceValue / targetAssetPriceValue - view?.didReceiveSwapQuoteReady() + DispatchQueue.main.async { [weak self] in + self?.view?.didReceiveSwapQuoteReady() + } } case let .failure(error): prices = [] diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift index a7998ed7ad..d6869ab579 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift @@ -104,6 +104,13 @@ final class LiquidityPoolSupplyViewController: UIViewController, ViewHolder, Hid action: #selector(handleTapPreviewButton), for: .touchUpInside ) + + let tapApyInfo = UITapGestureRecognizer( + target: self, + action: #selector(handleTapApyInfo) + ) + rootView.apyView + .addGestureRecognizer(tapApyInfo) } // MARK: - Private actions diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift index 6edbcd9a7d..b5e588b7aa 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift @@ -17,6 +17,13 @@ final class LiquidityPoolSupplyViewLayout: UIView { let navigationViewContainer = UIView() + let titleLabel: UILabel = { + let label = UILabel() + label.font = .h3Title + label.textColor = R.color.colorWhite() + return label + }() + let backButton: UIButton = { let button = UIButton() button.setImage(R.image.iconBack(), for: .normal) @@ -25,12 +32,6 @@ final class LiquidityPoolSupplyViewLayout: UIView { return button }() - let polkaswapImageView: UIImageView = { - let imageView = UIImageView() - imageView.image = R.image.polkaswap() - return imageView - }() - // MARK: content let contentView: ScrollableContainerView = { @@ -45,7 +46,7 @@ final class LiquidityPoolSupplyViewLayout: UIView { let swapToInputView = AmountInputViewV2() let switchSwapButton: UIButton = { let button = UIButton() - button.setImage(R.image.iconSwitch(), for: .normal) + button.setImage(R.image.iconAddTokenPair(), for: .normal) return button }() @@ -58,6 +59,14 @@ final class LiquidityPoolSupplyViewLayout: UIView { let apyView = UIFactory.default.createMultiView() let rewardTokenView = UIFactory.default.createMultiView() let networkFeeView = UIFactory.default.createMultiView() + let apyInfoButton: UIButton = { + let button = UIButton() + button.isUserInteractionEnabled = false + button.setImage(R.image.iconInfoFilled(), for: .normal) + return button + }() + + let tokenIconImageView = UIImageView() private lazy var multiViews = [ slippageView, @@ -123,6 +132,12 @@ final class LiquidityPoolSupplyViewLayout: UIView { slippageView.bind(viewModel: viewModel.slippageViewModel) apyView.bind(viewModel: viewModel.apyViewModel) rewardTokenView.bind(viewModel: viewModel.rewardTokenViewModel) + + viewModel.rewardTokenIconViewModel?.loadImage( + on: tokenIconImageView, + targetSize: CGSize(width: 12, height: 12), + animated: true + ) } // MARK: - Private methods @@ -140,7 +155,7 @@ final class LiquidityPoolSupplyViewLayout: UIView { } container.addSubview(backButton) - container.addSubview(polkaswapImageView) + container.addSubview(titleLabel) backButton.snp.makeConstraints { make in make.centerY.equalToSuperview() @@ -148,9 +163,8 @@ final class LiquidityPoolSupplyViewLayout: UIView { make.size.equalTo(Constants.backButtonSize) } - polkaswapImageView.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalTo(backButton.snp.trailing).offset(UIConstants.defaultOffset) + titleLabel.snp.makeConstraints { make in + make.center.equalToSuperview() } } @@ -230,6 +244,24 @@ final class LiquidityPoolSupplyViewLayout: UIView { $0.titleLabel.isUserInteractionEnabled = true } + apyView.addSubview(apyInfoButton) + + apyInfoButton.snp.makeConstraints { make in + make.leading.equalTo(apyView.titleLabel.snp.trailing).offset(4) + make.centerY.equalToSuperview() + make.size.equalTo(12) + } + + rewardTokenView.addSubview(tokenIconImageView) + + rewardTokenView.valueTop.snp.makeConstraints { make in + make.leading.equalTo(tokenIconImageView.snp.trailing).offset(4) + } + tokenIconImageView.snp.makeConstraints { make in + make.size.equalTo(12) + make.centerY.equalToSuperview() + } + return backgroundView } @@ -249,6 +281,8 @@ final class LiquidityPoolSupplyViewLayout: UIView { } private func applyLocalization() { + titleLabel.text = "Supply Liquidity" + swapFromInputView.locale = locale swapToInputView.locale = locale diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/ViewModel/LiquidityPoolSupplyViewModel.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/ViewModel/LiquidityPoolSupplyViewModel.swift index b6d62c8a8d..a4bb54ace6 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/ViewModel/LiquidityPoolSupplyViewModel.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/ViewModel/LiquidityPoolSupplyViewModel.swift @@ -4,4 +4,5 @@ struct LiquidityPoolSupplyViewModel { let slippageViewModel: TitleMultiValueViewModel? let apyViewModel: TitleMultiValueViewModel? let rewardTokenViewModel: TitleMultiValueViewModel? + let rewardTokenIconViewModel: RemoteImageViewModel? } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/ViewModel/LiquidityPoolSupplyViewModelFactory.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/ViewModel/LiquidityPoolSupplyViewModelFactory.swift index e3a3c63591..1d469e6597 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/ViewModel/LiquidityPoolSupplyViewModelFactory.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/ViewModel/LiquidityPoolSupplyViewModelFactory.swift @@ -27,13 +27,17 @@ class LiquidityPoolSupplyViewModelFactoryDefault: LiquidityPoolSupplyViewModelFa } let apyViewModel = apyString.flatMap { TitleMultiValueViewModel(title: $0, subtitle: nil) } - let rewardAssetSymbol = chain.assets.first(where: { $0.currencyId == liquidityPair.rewardAssetId })?.symbol.uppercased() + let rewardAsset = chain.assets.first(where: { $0.currencyId == liquidityPair.rewardAssetId }) + let rewardAssetSymbol = rewardAsset?.symbol.uppercased() let rewardTokenViewModel = TitleMultiValueViewModel(title: rewardAssetSymbol, subtitle: nil) + let rewardTokenIconViewModel = RemoteImageViewModel(url: rewardAsset?.icon) + return LiquidityPoolSupplyViewModel( slippageViewModel: slippageViewModel, apyViewModel: apyViewModel, - rewardTokenViewModel: rewardTokenViewModel + rewardTokenViewModel: rewardTokenViewModel, + rewardTokenIconViewModel: rewardTokenIconViewModel ) } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmPresenter.swift index 0d9d993737..55483c2d88 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmPresenter.swift @@ -211,7 +211,17 @@ extension LiquidityPoolSupplyConfirmPresenter: LiquidityPoolSupplyConfirmViewOut router.dismiss(view: view) } - func didTapApyInfo() {} + func didTapApyInfo() { + var infoText: String + var infoTitle: String + infoTitle = "Strategic Bonus APY" + infoText = "Farming reward for liquidity provision" + router.presentInfo( + message: infoText, + title: infoTitle, + from: view + ) + } func didTapConfirmButton() { guard diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewController.swift index e3cad00513..3b01e73457 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewController.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewController.swift @@ -66,6 +66,13 @@ final class LiquidityPoolSupplyConfirmViewController: UIViewController, ViewHold action: #selector(handleTapConfirmButton), for: .touchUpInside ) + + let tapApyInfo = UITapGestureRecognizer( + target: self, + action: #selector(handleTapApyInfo) + ) + rootView.apyView + .addGestureRecognizer(tapApyInfo) } // MARK: - Private actions diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewLayout.swift index 9b61b05a5c..289c44c658 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewLayout.swift @@ -36,7 +36,7 @@ final class LiquidityPoolSupplyConfirmViewLayout: UIView { return view }() - let doubleImageView = PolkaswapDoubleSymbolView() + let doubleImageView = PolkaswapDoubleSymbolView(imageSize: CGSize(width: 64, height: 64), mode: .filled) let swapStubTitle: UILabel = { let label = UILabel() label.font = .p1Paragraph @@ -85,6 +85,15 @@ final class LiquidityPoolSupplyConfirmViewLayout: UIView { return button }() + let apyInfoButton: UIButton = { + let button = UIButton() + button.isUserInteractionEnabled = false + button.setImage(R.image.iconInfoFilled(), for: .normal) + return button + }() + + let tokenIconImageView = UIImageView() + var locale: Locale = .current { didSet { applyLocalization() @@ -111,6 +120,12 @@ final class LiquidityPoolSupplyConfirmViewLayout: UIView { slippageView.bind(viewModel: viewModel.slippageViewModel) apyView.bind(viewModel: viewModel.apyViewModel) rewardTokenView.bind(viewModel: viewModel.rewardTokenViewModel) + + viewModel.rewardTokenIconViewModel?.loadImage( + on: tokenIconImageView, + targetSize: CGSize(width: 12, height: 12), + animated: true + ) } func bind(confirmViewModel: LiquidityPoolSupplyConfirmViewModel?) { @@ -197,5 +212,23 @@ final class LiquidityPoolSupplyConfirmViewLayout: UIView { make.bottom.equalToSuperview().inset(UIConstants.bigOffset) make.height.equalTo(UIConstants.actionHeight) } + + apyView.addSubview(apyInfoButton) + + apyInfoButton.snp.makeConstraints { make in + make.leading.equalTo(apyView.titleLabel.snp.trailing).offset(4) + make.centerY.equalToSuperview() + make.size.equalTo(12) + } + + rewardTokenView.addSubview(tokenIconImageView) + + rewardTokenView.valueTop.snp.makeConstraints { make in + make.leading.equalTo(tokenIconImageView.snp.trailing).offset(4) + } + tokenIconImageView.snp.makeConstraints { make in + make.size.equalTo(12) + make.centerY.equalToSuperview() + } } } From 413cef36aec7e9a374e461245e6240bd10253630 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Fri, 7 Jun 2024 12:30:49 +0500 Subject: [PATCH 023/115] [#FLW-4676] XCM. No need to offer a default network --- fearless/Modules/CrossChain/CrossChainPresenter.swift | 11 ++++------- .../Modules/CrossChain/CrossChainViewLayout.swift | 5 +++++ .../Model/CrossChainViewLoadingCollector.swift | 9 ++++++--- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/fearless/Modules/CrossChain/CrossChainPresenter.swift b/fearless/Modules/CrossChain/CrossChainPresenter.swift index 32c452c3ba..b606249405 100644 --- a/fearless/Modules/CrossChain/CrossChainPresenter.swift +++ b/fearless/Modules/CrossChain/CrossChainPresenter.swift @@ -113,7 +113,10 @@ final class CrossChainPresenter { } private func checkLoadingState() { - view?.setButtonLoadingState(isLoading: !loadingCollector.isReady) + guard let isReady = loadingCollector.isReady else { + return + } + view?.setButtonLoadingState(isLoading: !isReady) } private func provideInputViewModel() { @@ -660,12 +663,6 @@ extension CrossChainPresenter: CrossChainInteractorOutput { availableDestChainModels = filtredChainAssets .map { $0.chain } .withoutDuplicates() - - if selectedDestChainModel == nil { - selectedDestChainModel = filtredChainAssets.map { $0.chain }.first - } - provideDestSelectNetworkViewModel() - estimateFee() } func didSetup() { diff --git a/fearless/Modules/CrossChain/CrossChainViewLayout.swift b/fearless/Modules/CrossChain/CrossChainViewLayout.swift index 3f32396030..4cf15e8107 100644 --- a/fearless/Modules/CrossChain/CrossChainViewLayout.swift +++ b/fearless/Modules/CrossChain/CrossChainViewLayout.swift @@ -93,10 +93,12 @@ final class CrossChainViewLayout: UIView { // MARK: - Public methods func bind(originFeeViewModel: BalanceViewModelProtocol?) { + originNetworkFeeView.isHidden = originFeeViewModel == nil originNetworkFeeView.bindBalance(viewModel: originFeeViewModel) } func bind(destinationFeeViewModel: BalanceViewModelProtocol?) { + destinationNetworkFeeView.isHidden = destinationFeeViewModel == nil destinationNetworkFeeView.bindBalance(viewModel: destinationFeeViewModel) } @@ -134,6 +136,9 @@ final class CrossChainViewLayout: UIView { addSubview(contentView) addSubview(actionButton) + originNetworkFeeView.isHidden = true + destinationNetworkFeeView.isHidden = true + actionButton.snp.makeConstraints { make in make.height.equalTo(UIConstants.actionHeight) make.leading.trailing.equalToSuperview().inset(UIConstants.bigOffset) diff --git a/fearless/Modules/CrossChain/Model/CrossChainViewLoadingCollector.swift b/fearless/Modules/CrossChain/Model/CrossChainViewLoadingCollector.swift index ad3f386589..15703aefaf 100644 --- a/fearless/Modules/CrossChain/Model/CrossChainViewLoadingCollector.swift +++ b/fearless/Modules/CrossChain/Model/CrossChainViewLoadingCollector.swift @@ -1,8 +1,8 @@ import Foundation struct CrossChainViewLoadingCollector { - var originFeeReady: Bool = false - var destinationFeeReady: Bool = false + var originFeeReady: Bool? + var destinationFeeReady: Bool? var balanceReady: Bool = false var existentialDepositReady: Bool = false var destinationBalanceReady: Bool = false @@ -21,7 +21,10 @@ struct CrossChainViewLoadingCollector { addressExists = false } - var isReady: Bool { + var isReady: Bool? { + guard let originFeeReady, let destinationFeeReady else { + return nil + } let destinationReady = addressExists ? destinationBalanceReady : true return [ originFeeReady, From b4e9740ff98a21fff55010fa97c3792c14dabeaa Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Fri, 7 Jun 2024 13:10:21 +0500 Subject: [PATCH 024/115] [#FLW-4648] KSM. Staking. We can see two identical alerts --- .../ViewModel/StakingStateViewModelFactory+Alerts.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fearless/Modules/Staking/StakingMain/ViewModel/StakingStateViewModelFactory+Alerts.swift b/fearless/Modules/Staking/StakingMain/ViewModel/StakingStateViewModelFactory+Alerts.swift index 6043e599c8..1ac354f7e6 100644 --- a/fearless/Modules/Staking/StakingMain/ViewModel/StakingStateViewModelFactory+Alerts.swift +++ b/fearless/Modules/Staking/StakingMain/ViewModel/StakingStateViewModelFactory+Alerts.swift @@ -8,8 +8,7 @@ extension StakingStateViewModelFactory { [ findInactiveAlert(state: state), findRedeemUnbondedAlert(commonData: state.commonData, ledgerInfo: state.ledgerInfo), - findWaitingNextEraAlert(nominationStatus: state.status), - findMinNominatorBondAlert(commonData: state.commonData, ledgerInfo: state.ledgerInfo) + findWaitingNextEraAlert(nominationStatus: state.status) ].compactMap { $0 } } From f964a88afc89ecc4fc405b995e0acea57f8679b6 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Fri, 7 Jun 2024 14:03:55 +0500 Subject: [PATCH 025/115] [#FLW-4616] GLMR Staking. There are eternal shimmers --- .../Substrate/Types/StorageCodingPath.swift | 2 +- .../ParachainCollatorOperationFactory.swift | 18 ++++-------------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/fearless/Common/Substrate/Types/StorageCodingPath.swift b/fearless/Common/Substrate/Types/StorageCodingPath.swift index f1db6bf43d..6dfdf6856f 100644 --- a/fearless/Common/Substrate/Types/StorageCodingPath.swift +++ b/fearless/Common/Substrate/Types/StorageCodingPath.swift @@ -101,7 +101,7 @@ enum StorageCodingPath: Equatable, CaseIterable, StorageCodingPathProtocol { case .bottomDelegations: return (moduleName: "ParachainStaking", itemName: "BottomDelegations") case .staked: - return (moduleName: "ParachainStaking", itemName: "Staked") + return (moduleName: "ParachainStaking", itemName: "Total") case .currentBlock: return (moduleName: "System", itemName: "Number") case .bondedPools: diff --git a/fearless/Modules/Staking/Operations/ValidatorOperationFactory/ParachainCollatorOperationFactory.swift b/fearless/Modules/Staking/Operations/ValidatorOperationFactory/ParachainCollatorOperationFactory.swift index 170df1eab7..72f604e62c 100644 --- a/fearless/Modules/Staking/Operations/ValidatorOperationFactory/ParachainCollatorOperationFactory.swift +++ b/fearless/Modules/Staking/Operations/ValidatorOperationFactory/ParachainCollatorOperationFactory.swift @@ -386,19 +386,16 @@ final class ParachainCollatorOperationFactory { } func createStakedOperation( - dependingOn runtimeOperation: BaseOperation, - roundOperation: CompoundOperationWrapper<[StorageResponse]> + dependingOn runtimeOperation: BaseOperation ) -> CompoundOperationWrapper<[StorageResponse]> { guard let connection = chainRegistry.getConnection(for: chain.chainId) else { return CompoundOperationWrapper.createWithError(ChainRegistryError.connectionUnavailable) } - let params: (() throws -> [String]) = { ["\(try roundOperation.targetOperation.extractNoCancellableResultData().first?.value?.current ?? 0)"] } - let candidatePoolWrapper: CompoundOperationWrapper<[StorageResponse]> = storageRequestFactory.queryItems( engine: connection, - keyParams: params, + keys: { [try StorageKeyFactory().key(from: .staked)] }, factory: { try runtimeOperation.extractNoCancellableResultData() }, storagePath: .staked ) @@ -789,14 +786,7 @@ extension ParachainCollatorOperationFactory { } let runtimeOperation = runtimeService.fetchCoderFactoryOperation() - - let roundOperation = createRoundOperation(dependingOn: runtimeOperation) - - let stakedWrapper = createStakedOperation(dependingOn: runtimeOperation, roundOperation: roundOperation) - - stakedWrapper.allOperations.forEach { - $0.addDependency(roundOperation.targetOperation) - } + let stakedWrapper = createStakedOperation(dependingOn: runtimeOperation) let mapOperation = ClosureOperation { try stakedWrapper.targetOperation.extractNoCancellableResultData().first?.value @@ -804,7 +794,7 @@ extension ParachainCollatorOperationFactory { mapOperation.addDependency(stakedWrapper.targetOperation) - let dependencies = [runtimeOperation] + roundOperation.allOperations + stakedWrapper.allOperations + let dependencies = [runtimeOperation] + stakedWrapper.allOperations return CompoundOperationWrapper(targetOperation: mapOperation, dependencies: dependencies) } From 161d6480aa636e7629da20fb49c52482074aa0e5 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Fri, 7 Jun 2024 15:20:29 +0500 Subject: [PATCH 026/115] [#FLW-4515, #FLW-4514] There is wrong Extrinsic hash of Unstaking operation on Reef --- .../Wallet/AssetTransactionData+GiantsquidHistory.swift | 6 +++++- .../BlockExplorer/Giantsquid/GiantsquidTransfer.swift | 1 + .../History/Main/ReefSubsquidHistoryOperationFactory.swift | 1 + .../WalletTransactionDetailsViewModelFactory.swift | 6 +++++- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/fearless/Common/Extension/Wallet/AssetTransactionData+GiantsquidHistory.swift b/fearless/Common/Extension/Wallet/AssetTransactionData+GiantsquidHistory.swift index 8146555f3f..fae513f33d 100644 --- a/fearless/Common/Extension/Wallet/AssetTransactionData+GiantsquidHistory.swift +++ b/fearless/Common/Extension/Wallet/AssetTransactionData+GiantsquidHistory.swift @@ -50,6 +50,10 @@ extension AssetTransactionData { fees.append(fee) } + var context: [String: String] = [:] + if let blockHash = transfer.blockHash { + context["reefBlockHash"] = blockHash + } return AssetTransactionData( transactionId: transfer.identifier, @@ -65,7 +69,7 @@ extension AssetTransactionData { timestamp: timestamp, type: type.rawValue, reason: nil, - context: nil + context: context ) } diff --git a/fearless/Common/Network/BlockExplorer/Giantsquid/GiantsquidTransfer.swift b/fearless/Common/Network/BlockExplorer/Giantsquid/GiantsquidTransfer.swift index 3754921b4b..69bf6101d6 100644 --- a/fearless/Common/Network/BlockExplorer/Giantsquid/GiantsquidTransfer.swift +++ b/fearless/Common/Network/BlockExplorer/Giantsquid/GiantsquidTransfer.swift @@ -27,6 +27,7 @@ struct GiantsquidTransfer: Decodable { let type: String? let feeAmount: String? let signedData: GiantsquidSignedData? + let blockHash: String? var timestampInSeconds: Int64 { let locale = LocalizationManager.shared.selectedLocale diff --git a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/ReefSubsquidHistoryOperationFactory.swift b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/ReefSubsquidHistoryOperationFactory.swift index 84f724434c..4672dee7fe 100644 --- a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/ReefSubsquidHistoryOperationFactory.swift +++ b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Main/ReefSubsquidHistoryOperationFactory.swift @@ -130,6 +130,7 @@ final class ReefSubsquidHistoryOperationFactory { id } signedData + blockHash } } pageInfo { diff --git a/fearless/Modules/NewWallet/WalletTransactionDetails/ViewModel/WalletTransactionDetailsViewModelFactory.swift b/fearless/Modules/NewWallet/WalletTransactionDetails/ViewModel/WalletTransactionDetailsViewModelFactory.swift index c8593b69ab..c03a0c17f7 100644 --- a/fearless/Modules/NewWallet/WalletTransactionDetails/ViewModel/WalletTransactionDetailsViewModelFactory.swift +++ b/fearless/Modules/NewWallet/WalletTransactionDetails/ViewModel/WalletTransactionDetailsViewModelFactory.swift @@ -36,7 +36,11 @@ class WalletTransactionDetailsViewModelFactory: WalletTransactionDetailsViewMode return nil } - let hash = transaction.transactionId + var hash = transaction.transactionId + if let reefBlockHash = transaction.context?["reefBlockHash"] { + let trimmedBlockHash = reefBlockHash.hasPrefix("0x") ? reefBlockHash.dropFirst(2).prefix(5) : reefBlockHash.prefix(5) + hash = [hash, String(trimmedBlockHash)].joined(separator: "-") + } var status: String var statusIcon: UIImage? switch transaction.status { From a48289984f12c893a39bdee115e6eb09acdc1ba1 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Fri, 7 Jun 2024 17:41:48 +0500 Subject: [PATCH 027/115] [#FLW-4677] We have to pull a new balance using a pull to refresh --- .../Common/Helpers/WalletAssetsObserver.swift | 47 +++++++++++++++++++ .../AssetManagementAssembly.swift | 12 ++++- .../AssetManagementInteractor.swift | 11 ++++- .../AssetManagementPresenter.swift | 16 +++++++ .../AssetManagementViewController.swift | 17 +++++++ .../AssetManagementViewLayout.swift | 1 + .../ViewModel/AssetManagementViewModel.swift | 1 + .../AssetManagementViewModelFactory.swift | 3 +- 8 files changed, 105 insertions(+), 3 deletions(-) diff --git a/fearless/Common/Helpers/WalletAssetsObserver.swift b/fearless/Common/Helpers/WalletAssetsObserver.swift index a76e5447ca..d207f9a8a7 100644 --- a/fearless/Common/Helpers/WalletAssetsObserver.swift +++ b/fearless/Common/Helpers/WalletAssetsObserver.swift @@ -5,6 +5,10 @@ import SoraKeystore protocol WalletAssetsObserver: ApplicationServiceProtocol { func update(wallet: MetaAccountModel) + func updateVisibility( + wallet: MetaAccountModel?, + chainAssets: [ChainAsset] + ) async -> MetaAccountModel } final class WalletAssetsObserverImpl: WalletAssetsObserver { @@ -46,6 +50,20 @@ final class WalletAssetsObserverImpl: WalletAssetsObserver { setup() } + func updateVisibility( + wallet: MetaAccountModel?, + chainAssets: [ChainAsset] + ) async -> MetaAccountModel { + if let wallet { + self.wallet = wallet + } + let chains = chainAssets + .map { $0.chain } + .uniq(predicate: { $0.chainId }) + let updatedWallet = await updateVisibility(for: chains) + return updatedWallet + } + // MARK: - ApplicationServiceProtocol func setup() { @@ -112,6 +130,35 @@ final class WalletAssetsObserverImpl: WalletAssetsObserver { } } + private func updateVisibility(for chains: [ChainModel]) async -> MetaAccountModel { + let result = await withTaskGroup( + of: (ChainModel, [ChainAssetId: AccountInfo?])?.self, + returning: [ChainModel: [ChainAssetId: AccountInfo?]].self + ) { group in + chains.forEach { chain in + group.addTask { + do { + let accountInfos = try await self.accountInfoRemote.fetchAccountInfos(for: chain, wallet: self.wallet) + return (chain, accountInfos) + } catch { + return nil + } + } + } + + var taskResults = [ChainModel: [ChainAssetId: AccountInfo?]]() + for await result in group { + guard let result else { + continue + } + taskResults[result.0] = result.1 + } + return taskResults + } + updateCurrentWallet(with: result) + return wallet + } + private func fetchAccountInfos(chain: ChainModel, wallet: MetaAccountModel) async -> (ChainModel, [ChainAssetId: AccountInfo?]) { do { let accountInfos = try await accountInfoRemote.fetchAccountInfos(for: chain, wallet: wallet) diff --git a/fearless/Modules/AssetManagement/AssetManagementAssembly.swift b/fearless/Modules/AssetManagement/AssetManagementAssembly.swift index 5fe155b77b..cf1ed2c2c5 100644 --- a/fearless/Modules/AssetManagement/AssetManagementAssembly.swift +++ b/fearless/Modules/AssetManagement/AssetManagementAssembly.swift @@ -65,12 +65,22 @@ final class AssetManagementAssembly { storagePerformer: storagePerformer ) + let walletAssetsObserver = WalletAssetsObserverImpl( + wallet: wallet, + chainRegistry: chainRegistry, + accountInfoRemote: accountInfoRemote, + eventCenter: EventCenter.shared, + logger: Logger.shared, + userDefaultsStorage: SettingsManager.shared + ) + let interactor = AssetManagementInteractor( chainAssetFetching: chainAssetFetching, priceLocalSubscriber: priceLocalSubscriber, accountInfoFetchingProvider: accountInfoFetchingProvider, eventCenter: EventCenter.shared, - accountInfoRemoteService: accountInfoRemote + accountInfoRemoteService: accountInfoRemote, + walletAssetObserver: walletAssetsObserver ) let router = AssetManagementRouter() diff --git a/fearless/Modules/AssetManagement/AssetManagementInteractor.swift b/fearless/Modules/AssetManagement/AssetManagementInteractor.swift index fafdde5722..6b584da9c5 100644 --- a/fearless/Modules/AssetManagement/AssetManagementInteractor.swift +++ b/fearless/Modules/AssetManagement/AssetManagementInteractor.swift @@ -18,6 +18,7 @@ actor AssetManagementInteractor { private let accountInfoFetchingProvider: AccountInfoFetching private let eventCenter: EventCenterProtocol private let accountInfoRemoteService: AccountInfoRemoteService + private let walletAssetObserver: WalletAssetsObserver private var bufferWallet: MetaAccountModel? @@ -26,13 +27,15 @@ actor AssetManagementInteractor { priceLocalSubscriber: PriceLocalStorageSubscriber, accountInfoFetchingProvider: AccountInfoFetching, eventCenter: EventCenterProtocol, - accountInfoRemoteService: AccountInfoRemoteService + accountInfoRemoteService: AccountInfoRemoteService, + walletAssetObserver: WalletAssetsObserver ) { self.chainAssetFetching = chainAssetFetching self.priceLocalSubscriber = priceLocalSubscriber self.accountInfoFetchingProvider = accountInfoFetchingProvider self.eventCenter = eventCenter self.accountInfoRemoteService = accountInfoRemoteService + self.walletAssetObserver = walletAssetObserver self.eventCenter.add(observer: self) } @@ -123,6 +126,12 @@ extension AssetManagementInteractor: AssetManagementInteractorInput { ) return accountInfo } + + func updatedVisibility(for chainAssets: [ChainAsset]) async -> MetaAccountModel { + let updatedWallet = await walletAssetObserver.updateVisibility(wallet: bufferWallet, chainAssets: chainAssets) + bufferWallet = updatedWallet + return updatedWallet + } } // MARK: - PriceLocalSubscriptionHandler diff --git a/fearless/Modules/AssetManagement/AssetManagementPresenter.swift b/fearless/Modules/AssetManagement/AssetManagementPresenter.swift index 16b4b6e6ef..cc50302cc0 100644 --- a/fearless/Modules/AssetManagement/AssetManagementPresenter.swift +++ b/fearless/Modules/AssetManagement/AssetManagementPresenter.swift @@ -27,6 +27,9 @@ protocol AssetManagementInteractorInput: AnyObject { for chainAsset: ChainAsset, wallet: MetaAccountModel ) async throws -> AccountInfo? + func updatedVisibility( + for chainAssets: [ChainAsset] + ) async -> MetaAccountModel } final class AssetManagementPresenter { @@ -40,6 +43,7 @@ final class AssetManagementPresenter { private let viewModelFactory: AssetManagementViewModelFactory private var networkFilter: NetworkManagmentFilter? + private var viewModel: AssetManagementViewModel? private var chainAssets: [ChainAsset] = [] private var accountInfos: [ChainAssetKey: AccountInfo?] = [:] private var prices: [PriceData] = [] @@ -83,6 +87,7 @@ final class AssetManagementPresenter { search: searchText, pendingAccountInfoChainAssets: pendingAccountInfoChainAssets ) + self.viewModel = viewModel await view?.didReceive(viewModel: viewModel) } } @@ -208,6 +213,17 @@ extension AssetManagementPresenter: AssetManagementViewOutput { } getInitialData() } + + func didPullToRefresh() { + Task { + guard let chainAssets = viewModel?.dispayedChainAssets else { + return + } + let updatedWallet = await interactor.updatedVisibility(for: chainAssets) + wallet = updatedWallet + provideViewModel() + } + } } // MARK: - AssetManagementInteractorOutput diff --git a/fearless/Modules/AssetManagement/AssetManagementViewController.swift b/fearless/Modules/AssetManagement/AssetManagementViewController.swift index b54c02edef..97c4118bd1 100644 --- a/fearless/Modules/AssetManagement/AssetManagementViewController.swift +++ b/fearless/Modules/AssetManagement/AssetManagementViewController.swift @@ -10,6 +10,7 @@ protocol AssetManagementViewOutput: AnyObject { func allNetworkButtonDidTapped() func didSelectRow(at indexPath: IndexPath, viewModel: AssetManagementViewModel) func didTap(on section: Int, viewModel: AssetManagementViewModel) + func didPullToRefresh() } final class AssetManagementViewController: UIViewController, ViewHolder, HiddableBarWhenPushed, KeyboardViewAdoptable { @@ -90,8 +91,18 @@ final class AssetManagementViewController: UIViewController, ViewHolder, Hiddabl rootView.tableView.estimatedSectionFooterHeight = 0 rootView.tableView.contentInsetAdjustmentBehavior = .never + + if let refreshControl = rootView.tableView.refreshControl { + refreshControl.addTarget( + self, + action: #selector(handlePullToRefresh), + for: .valueChanged + ) + } } + // MARK: - Actions + @objc private func handleTap(sender: UIGestureRecognizer) { guard let viewModel, let section = sender.view?.tag else { @@ -100,6 +111,11 @@ final class AssetManagementViewController: UIViewController, ViewHolder, Hiddabl output.didTap(on: section, viewModel: viewModel) } + @objc + private func handlePullToRefresh() { + output.didPullToRefresh() + } + // MARK: - KeyboardViewAdoptable var target: Constraint? { rootView.keyboardAdoptableConstraint } @@ -116,6 +132,7 @@ extension AssetManagementViewController: AssetManagementViewInput { rootView.setFilter(title: viewModel.filterButtonTitle) rootView.setAddAssetButton(visible: viewModel.addAssetButtonIsHidden) rootView.tableView.reloadData() + rootView.tableView.refreshControl?.endRefreshing() reloadEmptyState(animated: false) } diff --git a/fearless/Modules/AssetManagement/AssetManagementViewLayout.swift b/fearless/Modules/AssetManagement/AssetManagementViewLayout.swift index 112fad2f95..6d23192e81 100644 --- a/fearless/Modules/AssetManagement/AssetManagementViewLayout.swift +++ b/fearless/Modules/AssetManagement/AssetManagementViewLayout.swift @@ -53,6 +53,7 @@ final class AssetManagementViewLayout: UIView { tableView.separatorStyle = .none tableView.backgroundColor = R.color.colorBlack19() tableView.tableFooterView = UIView() + tableView.refreshControl = UIRefreshControl() return tableView }() diff --git a/fearless/Modules/AssetManagement/ViewModel/AssetManagementViewModel.swift b/fearless/Modules/AssetManagement/ViewModel/AssetManagementViewModel.swift index 8140331aba..8db9515f5e 100644 --- a/fearless/Modules/AssetManagement/ViewModel/AssetManagementViewModel.swift +++ b/fearless/Modules/AssetManagement/ViewModel/AssetManagementViewModel.swift @@ -6,6 +6,7 @@ struct AssetManagementViewModel { var list: [AssetManagementTableSection] let filterButtonTitle: String let addAssetButtonIsHidden: Bool + let dispayedChainAssets: [ChainAsset] } struct AssetManagementTableSection { diff --git a/fearless/Modules/AssetManagement/ViewModel/AssetManagementViewModelFactory.swift b/fearless/Modules/AssetManagement/ViewModel/AssetManagementViewModelFactory.swift index fb06c878c1..2b95dc6a9c 100644 --- a/fearless/Modules/AssetManagement/ViewModel/AssetManagementViewModelFactory.swift +++ b/fearless/Modules/AssetManagement/ViewModel/AssetManagementViewModelFactory.swift @@ -106,7 +106,8 @@ final class AssetManagementViewModelFactoryDefault: AssetManagementViewModelFact let viewModel = AssetManagementViewModel( list: list, filterButtonTitle: filterButtonTitle, - addAssetButtonIsHidden: true + addAssetButtonIsHidden: true, + dispayedChainAssets: filtredChainAssets ) return viewModel } From 033fdbc6d73a13f0803a9d8d4f4ac8601f388b37 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Mon, 10 Jun 2024 11:26:50 +0500 Subject: [PATCH 028/115] [#FLW-4676] XCM. No need to offer a default network --- fearless/Modules/CrossChain/CrossChainPresenter.swift | 4 +++- fearless/Modules/CrossChain/CrossChainViewController.swift | 2 +- fearless/Modules/CrossChain/CrossChainViewLayout.swift | 7 ++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/fearless/Modules/CrossChain/CrossChainPresenter.swift b/fearless/Modules/CrossChain/CrossChainPresenter.swift index b606249405..fa79e5fb9a 100644 --- a/fearless/Modules/CrossChain/CrossChainPresenter.swift +++ b/fearless/Modules/CrossChain/CrossChainPresenter.swift @@ -10,7 +10,7 @@ protocol CrossChainViewInput: ControllerBackedProtocol, LoadableViewProtocol { func didReceive(assetBalanceViewModel: AssetBalanceViewModelProtocol?) func didReceive(amountInputViewModel: IAmountInputViewModel?) func didReceive(originSelectNetworkViewModel: SelectNetworkViewModel) - func didReceive(destSelectNetworkViewModel: SelectNetworkViewModel) + func didReceive(destSelectNetworkViewModel: SelectNetworkViewModel?) func didReceive(originFeeViewModel: LocalizableResource?) func didReceive(destinationFeeViewModel: LocalizableResource?) func didReceive(recipientViewModel: RecipientViewModel) @@ -159,6 +159,7 @@ final class CrossChainPresenter { private func provideDestSelectNetworkViewModel() { guard let selectedDestChainModel = selectedDestChainModel else { + view?.didReceive(destSelectNetworkViewModel: nil) return } @@ -533,6 +534,7 @@ extension CrossChainPresenter: CrossChainViewOutput { self.view = view interactor.setup(with: self) provideOriginSelectNetworkViewModel() + provideDestSelectNetworkViewModel() provideInputViewModel() } diff --git a/fearless/Modules/CrossChain/CrossChainViewController.swift b/fearless/Modules/CrossChain/CrossChainViewController.swift index 48ae7b68e0..41847f1de3 100644 --- a/fearless/Modules/CrossChain/CrossChainViewController.swift +++ b/fearless/Modules/CrossChain/CrossChainViewController.swift @@ -154,7 +154,7 @@ extension CrossChainViewController: CrossChainViewInput { rootView.bind(originalSelectNetworkViewModel: originSelectNetworkViewModel) } - func didReceive(destSelectNetworkViewModel: SelectNetworkViewModel) { + func didReceive(destSelectNetworkViewModel: SelectNetworkViewModel?) { rootView.bind(destSelectNetworkViewModel: destSelectNetworkViewModel) } diff --git a/fearless/Modules/CrossChain/CrossChainViewLayout.swift b/fearless/Modules/CrossChain/CrossChainViewLayout.swift index 4cf15e8107..09dfecd7b4 100644 --- a/fearless/Modules/CrossChain/CrossChainViewLayout.swift +++ b/fearless/Modules/CrossChain/CrossChainViewLayout.swift @@ -115,7 +115,12 @@ final class CrossChainViewLayout: UIView { .loadAmountInputIcon(on: originSelectNetworkView.iconView, animated: true) } - func bind(destSelectNetworkViewModel: SelectNetworkViewModel) { + func bind(destSelectNetworkViewModel: SelectNetworkViewModel?) { + guard let destSelectNetworkViewModel else { + destSelectNetworkView.subtitle = R.string.localizable.commonSelectNetwork(preferredLanguages: locale.rLanguages) + destSelectNetworkView.iconView.image = R.image.addressPlaceholder() + return + } destSelectNetworkView.subtitle = destSelectNetworkViewModel.chainName destSelectNetworkViewModel.iconViewModel?.cancel(on: destSelectNetworkView.iconView) destSelectNetworkView.iconView.image = nil From e868eb2e33c0760fa0f00cc6b3996b557a03bbb4 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Tue, 11 Jun 2024 13:28:30 +0500 Subject: [PATCH 029/115] local chains.json updated --- fearless/Resources/chains.json | 64 +++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/fearless/Resources/chains.json b/fearless/Resources/chains.json index 53f28be957..d07e3284c8 100644 --- a/fearless/Resources/chains.json +++ b/fearless/Resources/chains.json @@ -5029,11 +5029,6 @@ ], "nodes": [ - - { - "url": "wss://ws.framenode-7.s4.stg1.sora2.soramitsu.co.jp", - "name": "Sora Stage #7" - }, { "url": "wss://ws.framenode-8.s5.stg1.sora2.soramitsu.co.jp", "name": "Sora Stage #8" @@ -5334,7 +5329,7 @@ "id": "0ef3afdc-cdd3-47cc-bd52-817c54ae65b5", "name": "liberland merit", "symbol": "llm", - "currencyId": "0x0200000000000000000000000000010000000000000000000000000000000000", + "currencyId": "0x00073edd278e1bd6a7f9d0b27d4f3e93b73c8f0832b58a4df13c69611a99f156", "precision": 18, "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/LLM.svg", "color": "EFB900", @@ -5344,7 +5339,7 @@ "id": "1c3b4fcb-5a5f-4319-9dce-d178006eb9bf", "name": "liberland dollar", "symbol": "lld", - "currencyId": "0x00073edd278e1bd6a7f9d0b27d4f3e93b73c8f0832b58a4df13c69611a99f156", + "currencyId": "0x00513be65493a7fc3e2128d4230061a530acf40478a4affa20bbba27a310673e", "precision": 18, "priceId": "liberland-lld", "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/LLD.svg", @@ -5389,6 +5384,10 @@ { "id": "0ef3afdc-cdd3-47cc-bd52-817c54ae65b5", "symbol": "LLM" + }, + { + "id": "acc32ee0-8fdc-4743-91e1-f70cc4f3069b", + "symbol": "ASTAR" } ], "availableDestinations": [ @@ -5438,6 +5437,15 @@ "symbol": "LLM" } ] + }, + { + "chainId": "9eb76c5184c4ab8679d2d5d819fdf90b9c001403e9e17da2e14b6d8aec4029c6", + "assets": [ + { + "id": "acc32ee0-8fdc-4743-91e1-f70cc4f3069b", + "symbol": "ASTAR" + } + ] } ] }, @@ -7479,7 +7487,7 @@ "assets": [ { "id": "a6b83d39-a488-4b34-8352-280705a792ea", - "name": "liberland lld", + "name": "liberland dollar", "symbol": "lld", "precision": 12, "priceId": "liberland-lld", @@ -7488,26 +7496,26 @@ "isUtility": true, "type": "normal" }, - { - "id": "30b43eb9-36b4-4b40-bf72-330d2e20ee86", - "name": "liberland merit", - "symbol": "llm", - "precision": 12, - "currencyId": "1", - "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/LLM.svg", - "color": "EFB900", - "type": "assets" - }, - { - "id": "2e7179c9-4308-420e-a654-43c92d119717", - "name": "sora xor", - "symbol": "xor", - "precision": 12, - "currencyId": "774441749", - "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/XOR.svg", - "color": "EE2233", - "type": "assets" - } + { + "id": "30b43eb9-36b4-4b40-bf72-330d2e20ee86", + "name": "liberland merit", + "symbol": "llm", + "precision": 12, + "currencyId": "1", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/LLM.svg", + "color": "EFB900", + "type": "assets" + }, + { + "id": "2e7179c9-4308-420e-a654-43c92d119717", + "name": "sora xor", + "symbol": "xor", + "precision": 12, + "currencyId": "774441749", + "icon": "https://raw.githubusercontent.com/soramitsu/shared-features-utils/master/icons/tokens/coloured/XOR.svg", + "color": "EE2233", + "type": "assets" + } ], "xcm": { "xcmVersion": "v3", From 8ae28edf03f6a81d32c6ee6b6d0bcf9ef0d77039 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Thu, 13 Jun 2024 11:35:51 +0500 Subject: [PATCH 030/115] ChainModel equatable fix --- fearless.xcodeproj/project.pbxproj | 6 +- .../xcshareddata/swiftpm/Package.resolved | 21 ++-- ...setTransactionData+ArrowsquidHistory.swift | 10 +- ...ssetTransactionData+EtherscanHistory.swift | 4 +- ...setTransactionData+GiantsquidHistory.swift | 14 +-- .../AssetTransactionData+OklinkHistory.swift | 4 +- ...AssetTransactionData+SubqueryHistory.swift | 30 +++--- ...AssetTransactionData+SubsquidHistory.swift | 12 +-- .../Wallet/AssetTransactionData+Zeta.swift | 4 +- fearless/Common/Model/ChainAsset.swift | 2 +- .../Model/ChainRegistry/AssetModel.swift | 10 +- .../ChainRegistry/ChainSyncService.swift | 25 +++-- .../EntityToModel/AssetModelMapper.swift | 102 ------------------ .../EntityToModel/ChainModelMapper.swift | 43 +++++--- .../CrossChain/CrossChainPresenter.swift | 2 +- .../WalletTransactionHistoryViewFactory.swift | 4 +- .../ReceiveAndRequestAssetPresenter.swift | 2 +- .../SelectAsset/SelectAssetAssembly.swift | 6 -- .../SelectAsset/SelectAssetInteractor.swift | 34 ------ fearless/Modules/Send/SendPresenter.swift | 2 +- 20 files changed, 110 insertions(+), 227 deletions(-) delete mode 100644 fearless/Common/Storage/EntityToModel/AssetModelMapper.swift diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index 66158d1a10..ada5d081a4 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -2541,7 +2541,6 @@ FA93A3182836543B0021330F /* ValidatorListFilterRelaychainViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA93A3172836543B0021330F /* ValidatorListFilterRelaychainViewModelFactory.swift */; }; FA97E68B2A0281230035F5D7 /* GiantsquidRewardOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA97E68A2A0281230035F5D7 /* GiantsquidRewardOperationFactory.swift */; }; FA99422827FE925000D771E5 /* UISwitch+CustomSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA99422727FE925000D771E5 /* UISwitch+CustomSize.swift */; }; - FA99422A27FE927800D771E5 /* AssetModelMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA99422927FE927700D771E5 /* AssetModelMapper.swift */; }; FA99422D28002BB800D771E5 /* MissingAccountOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA99422C28002BB800D771E5 /* MissingAccountOptions.swift */; }; FA99423528053C5000D771E5 /* ChainAccountInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA99423428053C5000D771E5 /* ChainAccountInfo.swift */; }; FA99423728053C6800D771E5 /* IconWithTitleViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA99423628053C6800D771E5 /* IconWithTitleViewModelFactory.swift */; }; @@ -3100,6 +3099,7 @@ 0702B31929701884003519F5 /* MoneyPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoneyPresentable.swift; sourceTree = ""; }; 0702B354297948C8003519F5 /* SwapTransactionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapTransactionViewModel.swift; sourceTree = ""; }; 0702B35629794BA6003519F5 /* SwapTransactionViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapTransactionViewModelFactory.swift; sourceTree = ""; }; + 070594C62C183BBC0016264E /* shared-features-spm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "shared-features-spm"; path = "../shared-features-spm"; sourceTree = ""; }; 07089AF228B63386001566CA /* UIButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIButton.swift; sourceTree = ""; }; 07089AF428B64701001566CA /* ChainReconnectingEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainReconnectingEvent.swift; sourceTree = ""; }; 07089AF628B64928001566CA /* NetworkIssuesProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkIssuesProviderProtocol.swift; sourceTree = ""; }; @@ -5601,7 +5601,6 @@ FA93A3172836543B0021330F /* ValidatorListFilterRelaychainViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidatorListFilterRelaychainViewModelFactory.swift; sourceTree = ""; }; FA97E68A2A0281230035F5D7 /* GiantsquidRewardOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiantsquidRewardOperationFactory.swift; sourceTree = ""; }; FA99422727FE925000D771E5 /* UISwitch+CustomSize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UISwitch+CustomSize.swift"; sourceTree = ""; }; - FA99422927FE927700D771E5 /* AssetModelMapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetModelMapper.swift; sourceTree = ""; }; FA99422C28002BB800D771E5 /* MissingAccountOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissingAccountOptions.swift; sourceTree = ""; }; FA99423428053C5000D771E5 /* ChainAccountInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChainAccountInfo.swift; sourceTree = ""; }; FA99423628053C6800D771E5 /* IconWithTitleViewModelFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IconWithTitleViewModelFactory.swift; sourceTree = ""; }; @@ -8490,7 +8489,6 @@ isa = PBXGroup; children = ( FA74359E29C073790085A47E /* ChainSettingsMapper.swift */, - FA99422927FE927700D771E5 /* AssetModelMapper.swift */, 845B821826EF808D00D25C72 /* MetaAccountMapper.swift */, 84CA68DE26BEAA0F003B9453 /* ChainModelMapper.swift */, 845B822026EF8F1A00D25C72 /* ManagedMetaAccountMapper.swift */, @@ -8994,6 +8992,7 @@ 8490139F24A80984008F705E = { isa = PBXGroup; children = ( + 070594C62C183BBC0016264E /* shared-features-spm */, 849013AA24A80984008F705E /* fearless */, 849013C124A80986008F705E /* fearlessTests */, 8438E1D024BFAAD2001BDB13 /* fearlessIntegrationTests */, @@ -17805,7 +17804,6 @@ FAF5E9D627E46D77005A3448 /* AppVersionError.swift in Sources */, F462B351260C7DBE0005AB01 /* StakingRewardHistoryTableCell.swift in Sources */, FA72543C2AC2E48500EC47A6 /* ABIParameterTypes.swift in Sources */, - FA99422A27FE927800D771E5 /* AssetModelMapper.swift in Sources */, AE89720825F12143008EC414 /* ValidatorInfoViewModel.swift in Sources */, FA864445276851CF00956D8E /* ContainerViewController.swift in Sources */, FA9A8F272A72579D008FA99F /* AlchemyTokenCategory.swift in Sources */, diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index 23c50d586a..d996b8ff1e 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,4 @@ { - "originHash" : "48bf9205e83b67ed7d81b2a0bcc8ef081189084205112c031b7823572f561e20", "pins" : [ { "identity" : "appauth-ios", @@ -127,15 +126,6 @@ "version" : "0.1.7" } }, - { - "identity" : "shared-features-spm", - "kind" : "remoteSourceControl", - "location" : "https://github.com/soramitsu/shared-features-spm.git", - "state" : { - "branch" : "feature/xcm-routes", - "revision" : "f3737080f73453325fe436575b0f6c04d71e9e2a" - } - }, { "identity" : "swift-atomics", "kind" : "remoteSourceControl", @@ -226,6 +216,15 @@ "version" : "1.3.0" } }, + { + "identity" : "swiftformat", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nicklockwood/SwiftFormat", + "state" : { + "revision" : "dd989a46d0c6f15c016484bab8afe5e7a67a4022", + "version" : "0.54.0" + } + }, { "identity" : "swiftimagereadwrite", "kind" : "remoteSourceControl", @@ -299,5 +298,5 @@ } } ], - "version" : 3 + "version" : 2 } diff --git a/fearless/Common/Extension/Wallet/AssetTransactionData+ArrowsquidHistory.swift b/fearless/Common/Extension/Wallet/AssetTransactionData+ArrowsquidHistory.swift index 375fabda6c..33e01b3a7a 100644 --- a/fearless/Common/Extension/Wallet/AssetTransactionData+ArrowsquidHistory.swift +++ b/fearless/Common/Extension/Wallet/AssetTransactionData+ArrowsquidHistory.swift @@ -37,7 +37,7 @@ extension AssetTransactionData { return AssetTransactionData( transactionId: item.identifier, status: .pending, - assetId: asset.identifier, + assetId: asset.id, peerId: "", peerFirstName: nil, peerLastName: nil, @@ -79,8 +79,8 @@ extension AssetTransactionData { let feeDecimal = Decimal.fromSubstrateAmount(feeValue, precision: Int16(utilityAsset.precision)) ?? .zero let fee = AssetTransactionFee( - identifier: asset.identifier, - assetId: asset.identifier, + identifier: asset.id, + assetId: asset.id, amount: AmountDecimal(value: feeDecimal), context: nil ) @@ -91,7 +91,7 @@ extension AssetTransactionData { return AssetTransactionData( transactionId: item.extrinsicHash ?? item.identifier, status: status, - assetId: asset.identifier, + assetId: asset.id, peerId: peerId, peerFirstName: nil, peerLastName: nil, @@ -131,7 +131,7 @@ extension AssetTransactionData { return AssetTransactionData( transactionId: item.identifier, status: status, - assetId: asset.identifier, + assetId: asset.id, peerId: peerId, peerFirstName: reward.validator, peerLastName: nil, diff --git a/fearless/Common/Extension/Wallet/AssetTransactionData+EtherscanHistory.swift b/fearless/Common/Extension/Wallet/AssetTransactionData+EtherscanHistory.swift index 3bf6ffdb2e..c6ca132004 100644 --- a/fearless/Common/Extension/Wallet/AssetTransactionData+EtherscanHistory.swift +++ b/fearless/Common/Extension/Wallet/AssetTransactionData+EtherscanHistory.swift @@ -29,8 +29,8 @@ extension AssetTransactionData { let feeDecimal = Decimal.fromSubstrateAmount(feeValue, precision: Int16(utilityAsset.precision)) ?? .zero let fee = AssetTransactionFee( - identifier: asset.identifier, - assetId: asset.identifier, + identifier: asset.id, + assetId: asset.id, amount: AmountDecimal(value: feeDecimal), context: nil ) diff --git a/fearless/Common/Extension/Wallet/AssetTransactionData+GiantsquidHistory.swift b/fearless/Common/Extension/Wallet/AssetTransactionData+GiantsquidHistory.swift index 8146555f3f..fb90617aca 100644 --- a/fearless/Common/Extension/Wallet/AssetTransactionData+GiantsquidHistory.swift +++ b/fearless/Common/Extension/Wallet/AssetTransactionData+GiantsquidHistory.swift @@ -31,8 +31,8 @@ extension AssetTransactionData { if let feeAmountString = transfer.feeAmount, let feeSubstrateAmount = BigUInt(string: feeAmountString), let feeDecimalAmount = Decimal.fromSubstrateAmount(feeSubstrateAmount, precision: Int16(asset.precision)) { let fee = AssetTransactionFee( - identifier: asset.identifier, - assetId: asset.identifier, + identifier: asset.id, + assetId: asset.id, amount: AmountDecimal(value: feeDecimalAmount), context: nil ) @@ -42,8 +42,8 @@ extension AssetTransactionData { if let signedData = transfer.signedData, let fee = signedData.fee, let partialFee = fee.partialFee, let partialFeeDecimal = Decimal.fromSubstrateAmount(partialFee, precision: Int16(asset.precision)) { let fee = AssetTransactionFee( - identifier: asset.identifier, - assetId: asset.identifier, + identifier: asset.id, + assetId: asset.id, amount: AmountDecimal(value: partialFeeDecimal), context: nil ) @@ -207,8 +207,8 @@ extension AssetTransactionData { let partialFeeDecimal = Decimal.fromSubstrateAmount(partialFee, precision: Int16(asset.precision)) { let fee = AssetTransactionFee( - identifier: asset.identifier, - assetId: asset.identifier, + identifier: asset.id, + assetId: asset.id, amount: AmountDecimal(value: partialFeeDecimal), context: nil ) @@ -219,7 +219,7 @@ extension AssetTransactionData { return AssetTransactionData( transactionId: extrinsic.hash ?? extrinsic.id, status: status, - assetId: asset.identifier, + assetId: asset.id, peerId: address, peerFirstName: extrinsic.section, peerLastName: extrinsic.method, diff --git a/fearless/Common/Extension/Wallet/AssetTransactionData+OklinkHistory.swift b/fearless/Common/Extension/Wallet/AssetTransactionData+OklinkHistory.swift index c4252e2beb..713f1e5a4a 100644 --- a/fearless/Common/Extension/Wallet/AssetTransactionData+OklinkHistory.swift +++ b/fearless/Common/Extension/Wallet/AssetTransactionData+OklinkHistory.swift @@ -22,8 +22,8 @@ extension AssetTransactionData { let feeDecimal = Decimal(string: item.txFee, locale: Locale(identifier: "en_EN")) ?? .zero let fee = AssetTransactionFee( - identifier: asset.identifier, - assetId: asset.identifier, + identifier: asset.id, + assetId: asset.id, amount: AmountDecimal(value: feeDecimal), context: nil ) diff --git a/fearless/Common/Extension/Wallet/AssetTransactionData+SubqueryHistory.swift b/fearless/Common/Extension/Wallet/AssetTransactionData+SubqueryHistory.swift index e71d96447d..58794f9b7e 100644 --- a/fearless/Common/Extension/Wallet/AssetTransactionData+SubqueryHistory.swift +++ b/fearless/Common/Extension/Wallet/AssetTransactionData+SubqueryHistory.swift @@ -45,7 +45,7 @@ extension AssetTransactionData { return AssetTransactionData( transactionId: item.identifier, status: .pending, - assetId: asset.identifier, + assetId: asset.id, peerId: "", peerFirstName: nil, peerLastName: nil, @@ -86,8 +86,8 @@ extension AssetTransactionData { let feeDecimal = Decimal.fromSubstrateAmount(feeValue, precision: Int16(asset.precision)) ?? .zero let fee = AssetTransactionFee( - identifier: asset.identifier, - assetId: asset.identifier, + identifier: asset.id, + assetId: asset.id, amount: AmountDecimal(value: feeDecimal), context: nil ) @@ -100,7 +100,7 @@ extension AssetTransactionData { return AssetTransactionData( transactionId: item.identifier, status: status, - assetId: asset.identifier, + assetId: asset.id, peerId: peerId, peerFirstName: nil, peerLastName: nil, @@ -145,8 +145,8 @@ extension AssetTransactionData { let feeDecimal = Decimal.fromSubstrateAmount(feeValue, precision: Int16(asset.precision)) ?? .zero let fee = AssetTransactionFee( - identifier: asset.identifier, - assetId: asset.identifier, + identifier: asset.id, + assetId: asset.id, amount: AmountDecimal(value: feeDecimal), context: nil ) @@ -157,7 +157,7 @@ extension AssetTransactionData { return AssetTransactionData( transactionId: item.hash, status: status, - assetId: asset.identifier, + assetId: asset.id, peerId: peerId, peerFirstName: nil, peerLastName: nil, @@ -199,7 +199,7 @@ extension AssetTransactionData { return AssetTransactionData( transactionId: item.identifier, status: status, - assetId: asset.identifier, + assetId: asset.id, peerId: peerId, peerFirstName: reward.validator, peerLastName: nil, @@ -233,7 +233,7 @@ extension AssetTransactionData { return AssetTransactionData( transactionId: item.identifier, status: status, - assetId: asset.identifier, + assetId: asset.id, peerId: item.extrinsicHash, peerFirstName: nil, peerLastName: nil, @@ -267,7 +267,7 @@ extension AssetTransactionData { return AssetTransactionData( transactionId: item.identifier, status: status, - assetId: asset.identifier, + assetId: asset.id, peerId: peerId, peerFirstName: extrinsic.module, peerLastName: extrinsic.call, @@ -310,7 +310,7 @@ extension AssetTransactionData { return AssetTransactionData( transactionId: item.identifier, status: status, - assetId: asset.identifier, + assetId: asset.id, peerId: peerId, peerFirstName: item.callModule, peerLastName: item.callFunction, @@ -369,8 +369,8 @@ extension AssetTransactionData { ) ?? .zero let fee = AssetTransactionFee( - identifier: asset.identifier, - assetId: asset.identifier, + identifier: asset.id, + assetId: asset.id, amount: AmountDecimal(value: feeDecimal), context: nil ) @@ -391,7 +391,7 @@ extension AssetTransactionData { return AssetTransactionData( transactionId: item.txHash, status: item.status.walletValue, - assetId: asset.identifier, + assetId: asset.id, peerId: peerId, peerFirstName: nil, peerLastName: nil, @@ -427,7 +427,7 @@ extension AssetTransactionData { return AssetTransactionData( transactionId: item.identifier, status: item.status.walletValue, - assetId: asset.identifier, + assetId: asset.id, peerId: peerId, peerFirstName: item.callPath.moduleName, peerLastName: item.callPath.callName, diff --git a/fearless/Common/Extension/Wallet/AssetTransactionData+SubsquidHistory.swift b/fearless/Common/Extension/Wallet/AssetTransactionData+SubsquidHistory.swift index a3c9b03db5..457c09f3c8 100644 --- a/fearless/Common/Extension/Wallet/AssetTransactionData+SubsquidHistory.swift +++ b/fearless/Common/Extension/Wallet/AssetTransactionData+SubsquidHistory.swift @@ -45,7 +45,7 @@ extension AssetTransactionData { return AssetTransactionData( transactionId: item.identifier, status: .pending, - assetId: asset.identifier, + assetId: asset.id, peerId: "", peerFirstName: nil, peerLastName: nil, @@ -87,8 +87,8 @@ extension AssetTransactionData { let feeDecimal = Decimal.fromSubstrateAmount(feeValue, precision: Int16(utilityAsset.precision)) ?? .zero let fee = AssetTransactionFee( - identifier: asset.identifier, - assetId: asset.identifier, + identifier: asset.id, + assetId: asset.id, amount: AmountDecimal(value: feeDecimal), context: nil ) @@ -99,7 +99,7 @@ extension AssetTransactionData { return AssetTransactionData( transactionId: item.extrinsicHash ?? item.identifier, status: status, - assetId: asset.identifier, + assetId: asset.id, peerId: peerId, peerFirstName: nil, peerLastName: nil, @@ -139,7 +139,7 @@ extension AssetTransactionData { return AssetTransactionData( transactionId: item.identifier, status: status, - assetId: asset.identifier, + assetId: asset.id, peerId: peerId, peerFirstName: reward.validator, peerLastName: nil, @@ -171,7 +171,7 @@ extension AssetTransactionData { return AssetTransactionData( transactionId: item.identifier, status: status, - assetId: asset.identifier, + assetId: asset.id, peerId: peerId, peerFirstName: extrinsic.module, peerLastName: extrinsic.call, diff --git a/fearless/Common/Extension/Wallet/AssetTransactionData+Zeta.swift b/fearless/Common/Extension/Wallet/AssetTransactionData+Zeta.swift index 5b36362b58..6c9e938b52 100644 --- a/fearless/Common/Extension/Wallet/AssetTransactionData+Zeta.swift +++ b/fearless/Common/Extension/Wallet/AssetTransactionData+Zeta.swift @@ -27,8 +27,8 @@ extension AssetTransactionData { let feeDecimal = Decimal.fromSubstrateAmount(feeValue, precision: Int16(utilityAsset.precision)) ?? .zero let fee = AssetTransactionFee( - identifier: asset.identifier, - assetId: asset.identifier, + identifier: asset.id, + assetId: asset.id, amount: AmountDecimal(value: feeDecimal), context: nil ) diff --git a/fearless/Common/Model/ChainAsset.swift b/fearless/Common/Model/ChainAsset.swift index 8fcece8adc..9d3cb44232 100644 --- a/fearless/Common/Model/ChainAsset.swift +++ b/fearless/Common/Model/ChainAsset.swift @@ -6,7 +6,7 @@ extension ChainAsset { var assetDisplayInfo: AssetBalanceDisplayInfo { asset.displayInfo(with: chain.icon) } var identifier: String { - [chain.identifier, asset.identifier].joined(separator: " : ") + [chain.identifier, asset.id].joined(separator: " : ") } var storagePath: StorageCodingPath { diff --git a/fearless/Common/Model/ChainRegistry/AssetModel.swift b/fearless/Common/Model/ChainRegistry/AssetModel.swift index bfa1fa431b..7cdc1874be 100644 --- a/fearless/Common/Model/ChainRegistry/AssetModel.swift +++ b/fearless/Common/Model/ChainRegistry/AssetModel.swift @@ -3,10 +3,8 @@ import RobinHood import SSFModels import SoraFoundation -extension AssetModel: Identifiable { - public var identifier: String { id } - - public var displayInfo: AssetBalanceDisplayInfo { +public extension AssetModel { + var displayInfo: AssetBalanceDisplayInfo { AssetBalanceDisplayInfo( displayPrecision: 5, assetPrecision: Int16(bitPattern: precision), @@ -17,7 +15,7 @@ extension AssetModel: Identifiable { ) } - public func displayInfo(with chainIcon: URL?) -> AssetBalanceDisplayInfo { + func displayInfo(with chainIcon: URL?) -> AssetBalanceDisplayInfo { AssetBalanceDisplayInfo( displayPrecision: 5, assetPrecision: Int16(bitPattern: precision), @@ -28,7 +26,7 @@ extension AssetModel: Identifiable { ) } - public func normalizedSymbol() -> String { + func normalizedSymbol() -> String { guard symbol.hasPrefix("xc") else { return symbol } diff --git a/fearless/Common/Services/ChainRegistry/ChainSyncService.swift b/fearless/Common/Services/ChainRegistry/ChainSyncService.swift index 16cb0cfc80..b6d2e8e33b 100644 --- a/fearless/Common/Services/ChainRegistry/ChainSyncService.swift +++ b/fearless/Common/Services/ChainRegistry/ChainSyncService.swift @@ -191,12 +191,25 @@ final class ChainSyncService { switch result { case let .success(changes): - logger?.debug( - """ - Sync completed: \(changes.newOrUpdatedItems) (new or updated), - \(changes.removedItems) (removed) - """ - ) + if changes.newOrUpdatedItems.isNotEmpty { + logger?.warning( + """ + !!!! Make shure what chains.json was changed, if you see this message without chains.json changes, equatable ChainModel is broken !!!! + """ + ) + logger?.debug( + """ + Sync completed: \(changes.newOrUpdatedItems.map { $0.name }) (new or updated) + """ + ) + } + if changes.removedItems.isNotEmpty { + logger?.debug( + """ + Sync completed: \(changes.removedItems.map { $0.name }) (removed) + """ + ) + } retryAttempt = 0 diff --git a/fearless/Common/Storage/EntityToModel/AssetModelMapper.swift b/fearless/Common/Storage/EntityToModel/AssetModelMapper.swift deleted file mode 100644 index c019358eea..0000000000 --- a/fearless/Common/Storage/EntityToModel/AssetModelMapper.swift +++ /dev/null @@ -1,102 +0,0 @@ -import Foundation -import RobinHood -import CoreData -import SSFModels - -enum AssetModelMapperError: Error { - case requiredFieldsMissing -} - -final class AssetModelMapper: CoreDataMapperProtocol { - var entityIdentifierFieldName: String { "id" } - - func transform(entity: CDAsset) throws -> AssetModel { - guard - let id = entity.id, - let symbol = entity.symbol, - let name = entity.name - else { - throw AssetModelMapperError.requiredFieldsMissing - } - - let staking: RawStakingType? - if let entityStaking = entity.staking { - staking = RawStakingType(rawValue: entityStaking) - } else { - staking = nil - } - let purchaseProviders: [PurchaseProvider]? = entity.purchaseProviders?.compactMap { - PurchaseProvider(rawValue: $0) - } - - var priceProvider: PriceProvider? - if let typeRawValue = entity.priceProvider?.type, - let type = PriceProviderType(rawValue: typeRawValue), - let id = entity.priceProvider?.id { - let precision = entity.priceProvider?.precision ?? "" - priceProvider = PriceProvider(type: type, id: id, precision: Int16(precision)) - } - - return AssetModel( - id: id, - name: name, - symbol: symbol, - precision: UInt16(entity.precision), - icon: entity.icon, - price: entity.price as Decimal?, - fiatDayChange: entity.fiatDayChange as Decimal?, - currencyId: entity.currencyId, - existentialDeposit: entity.existentialDeposit, - color: entity.color, - isUtility: entity.isUtility, - isNative: entity.isNative, - staking: staking, - purchaseProviders: purchaseProviders, - type: createChainAssetModelType(from: entity.type), - ethereumType: createEthereumAssetType(from: entity.ethereumType), - priceProvider: priceProvider, - coingeckoPriceId: entity.priceId - ) - } - - func populate( - entity: CDAsset, - from model: AssetModel, - using context: NSManagedObjectContext - ) throws { - entity.id = model.id - entity.precision = Int16(model.precision) - entity.icon = model.icon - entity.priceId = model.coingeckoPriceId - entity.price = model.price as NSDecimalNumber? - entity.fiatDayChange = model.fiatDayChange as NSDecimalNumber? - entity.symbol = model.symbol - entity.existentialDeposit = model.existentialDeposit - entity.color = model.color - entity.ethereumType = model.ethereumType?.rawValue - entity.type = model.type?.rawValue - entity.ethereumType = model.ethereumType?.rawValue - - let priceProviderContext = CDPriceProvider(context: context) - priceProviderContext.type = model.priceProvider?.type.rawValue - priceProviderContext.id = model.priceProvider?.id - if let precision = model.priceProvider?.precision { - priceProviderContext.precision = "\(precision)" - } - entity.priceProvider = priceProviderContext - } - - private func createChainAssetModelType(from rawValue: String?) -> SubstrateAssetType? { - guard let rawValue = rawValue else { - return nil - } - return SubstrateAssetType(rawValue: rawValue) - } - - private func createEthereumAssetType(from rawValue: String?) -> EthereumAssetType? { - guard let rawValue = rawValue else { - return nil - } - return EthereumAssetType(rawValue: rawValue) - } -} diff --git a/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift b/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift index 72dd672e87..9a3e2cc48d 100644 --- a/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift +++ b/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift @@ -273,33 +273,47 @@ final class ChainModelMapper { } private func createXcmConfig(from entity: CDChain) -> XcmChain? { - guard let versionRaw = entity.xcmConfig?.xcmVersion else { + guard + let versionRaw = entity.xcmConfig?.xcmVersion, + let availableAssets = entity.xcmConfig?.availableAssets, + let availableDestinations = entity.xcmConfig?.availableDestinations + else { return nil } let version = XcmCallFactoryVersion(rawValue: versionRaw) - let availableXcmAssets = entity.xcmConfig?.availableAssets?.allObjects as? [CDXcmAvailableAsset] ?? [] - let assets: [XcmAvailableAsset] = availableXcmAssets.compactMap { entity in - guard let id = entity.id, let symbol = entity.symbol else { + let assets: [XcmAvailableAsset] = availableAssets.compactMap { entity in + guard + let entity = entity as? CDXcmAvailableAsset, + let id = entity.id, + let symbol = entity.symbol + else { return nil } return XcmAvailableAsset(id: id, symbol: symbol, minAmount: nil) } - let availableXcmAssetDestinations = entity.xcmConfig?.availableDestinations?.allObjects as? [CDXcmAvailableDestination] ?? [] - let destinations: [XcmAvailableDestination] = availableXcmAssetDestinations.compactMap { - guard let chainId = $0.chainId else { + let destinations: [XcmAvailableDestination] = availableDestinations.compactMap { entity in + guard + let entity = entity as? CDXcmAvailableDestination, + let chainId = entity.chainId, + let assetsEntities = entity.assets + else { return nil } - let assetsEntities = $0.assets?.allObjects as? [CDXcmAvailableAsset] ?? [] + let assets: [XcmAvailableAsset] = assetsEntities.compactMap { entity in - guard let id = entity.id, let symbol = entity.symbol else { + guard + let entity = entity as? CDXcmAvailableAsset, + let id = entity.id, + let symbol = entity.symbol + else { return nil } return XcmAvailableAsset(id: id, symbol: symbol, minAmount: entity.minAmount) } return XcmAvailableDestination( chainId: chainId, - bridgeParachainId: $0.bridgeParachainId, + bridgeParachainId: entity.bridgeParachainId, assets: assets ) } @@ -394,7 +408,9 @@ final class ChainModelMapper { let configEntity = CDChainXcmConfig(context: context) configEntity.xcmVersion = xcmConfig.xcmVersion?.rawValue - configEntity.destWeightIsPrimitive = xcmConfig.destWeightIsPrimitive ?? false + if let destWeightIsPrimitive = xcmConfig.destWeightIsPrimitive { + configEntity.destWeightIsPrimitive = destWeightIsPrimitive + } let availableAssets = xcmConfig.availableAssets.map { let entity = CDXcmAvailableAsset(context: context) @@ -404,7 +420,7 @@ final class ChainModelMapper { } configEntity.availableAssets = Set(availableAssets) as NSSet - let destinationEntities = xcmConfig.availableDestinations.compactMap { + let destinationEntities = xcmConfig.availableDestinations.map { let destinationEntity = CDXcmAvailableDestination(context: context) destinationEntity.chainId = $0.chainId @@ -481,7 +497,8 @@ extension ChainModelMapper: CoreDataMapperProtocol { parentId: entity.parentId, paraId: entity.paraId, name: entity.name!, - xcm: xcm, nodes: Set(nodes), + xcm: xcm, + nodes: Set(nodes), addressPrefix: UInt16(bitPattern: entity.addressPrefix), types: types, icon: entity.icon, diff --git a/fearless/Modules/CrossChain/CrossChainPresenter.swift b/fearless/Modules/CrossChain/CrossChainPresenter.swift index a1f29b4f81..3ce395ea70 100644 --- a/fearless/Modules/CrossChain/CrossChainPresenter.swift +++ b/fearless/Modules/CrossChain/CrossChainPresenter.swift @@ -510,7 +510,7 @@ extension CrossChainPresenter: CrossChainViewOutput { from: view, wallet: wallet, chainAssets: availableOriginChainAssets, - selectedAssetId: selectedAmountChainAsset.asset.identifier, + selectedAssetId: selectedAmountChainAsset.asset.id, output: self ) } diff --git a/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryViewFactory.swift b/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryViewFactory.swift index 510736dd1d..251e470ec6 100644 --- a/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryViewFactory.swift +++ b/fearless/Modules/NewWallet/WalletTransactionHistory/WalletTransactionHistoryViewFactory.swift @@ -23,8 +23,8 @@ enum WalletTransactionHistoryViewFactory { selectedAccount: selectedAccount, dependencyContainer: dependencyContainer, logger: Logger.shared, - defaultFilter: WalletHistoryRequest(assets: [asset.identifier]), - selectedFilter: WalletHistoryRequest(assets: [asset.identifier]), + defaultFilter: WalletHistoryRequest(assets: [asset.id]), + selectedFilter: WalletHistoryRequest(assets: [asset.id]), filters: transactionHistoryFilters(for: chain), eventCenter: EventCenter.shared, applicationHandler: ApplicationHandler() diff --git a/fearless/Modules/ReceiveAndRequestAsset/ReceiveAndRequestAssetPresenter.swift b/fearless/Modules/ReceiveAndRequestAsset/ReceiveAndRequestAssetPresenter.swift index f2ef04ba87..0a1fdb979d 100644 --- a/fearless/Modules/ReceiveAndRequestAsset/ReceiveAndRequestAssetPresenter.swift +++ b/fearless/Modules/ReceiveAndRequestAsset/ReceiveAndRequestAssetPresenter.swift @@ -197,7 +197,7 @@ extension ReceiveAndRequestAssetPresenter: ReceiveAndRequestAssetViewOutput { router.showSelectAsset( from: view, wallet: wallet, - selectedAssetId: chainAsset.asset.identifier, + selectedAssetId: chainAsset.asset.id, chainAssets: chainAsset.chain.chainAssets, output: self ) diff --git a/fearless/Modules/SelectAsset/SelectAssetAssembly.swift b/fearless/Modules/SelectAsset/SelectAssetAssembly.swift index 99c7d9ebda..3957c39c07 100644 --- a/fearless/Modules/SelectAsset/SelectAssetAssembly.swift +++ b/fearless/Modules/SelectAsset/SelectAssetAssembly.swift @@ -15,10 +15,6 @@ final class SelectAssetAssembly { isFullSize: Bool = false ) -> SelectAssetModuleCreationResult? { let localizationManager = LocalizationManager.shared - - let assetRepository = SubstrateDataStorageFacade.shared.createRepository( - mapper: AnyCoreDataMapper(AssetModelMapper()) - ) let chainRepository = ChainRepositoryFactory().createRepository( sortDescriptors: [NSSortDescriptor.chainsByAddressPrefix] ) @@ -42,9 +38,7 @@ final class SelectAssetAssembly { chainAssetFetching: chainAssetFetching, accountInfoSubscriptionAdapter: accountInfoSubscriptionAdapter, priceLocalSubscriber: priceLocalSubscriber, - assetRepository: AnyDataProviderRepository(assetRepository), chainAssets: chainAssets, - operationQueue: operationQueue, wallet: wallet ) let router = SelectAssetRouter() diff --git a/fearless/Modules/SelectAsset/SelectAssetInteractor.swift b/fearless/Modules/SelectAsset/SelectAssetInteractor.swift index 62692dd0a5..bee8740ed5 100644 --- a/fearless/Modules/SelectAsset/SelectAssetInteractor.swift +++ b/fearless/Modules/SelectAsset/SelectAssetInteractor.swift @@ -7,10 +7,8 @@ final class SelectAssetInteractor { private weak var output: SelectAssetInteractorOutput? - private let operationQueue: OperationQueue private let chainAssetFetching: ChainAssetFetchingProtocol private let accountInfoSubscriptionAdapter: AccountInfoSubscriptionAdapterProtocol - private let assetRepository: AnyDataProviderRepository private let wallet: MetaAccountModel private let priceLocalSubscriber: PriceLocalStorageSubscriber @@ -26,17 +24,13 @@ final class SelectAssetInteractor { chainAssetFetching: ChainAssetFetchingProtocol, accountInfoSubscriptionAdapter: AccountInfoSubscriptionAdapterProtocol, priceLocalSubscriber: PriceLocalStorageSubscriber, - assetRepository: AnyDataProviderRepository, chainAssets: [ChainAsset]?, - operationQueue: OperationQueue, wallet: MetaAccountModel ) { self.chainAssetFetching = chainAssetFetching self.accountInfoSubscriptionAdapter = accountInfoSubscriptionAdapter self.priceLocalSubscriber = priceLocalSubscriber - self.assetRepository = assetRepository self.chainAssets = chainAssets - self.operationQueue = operationQueue self.wallet = wallet } @@ -89,15 +83,6 @@ extension SelectAssetInteractor: AccountInfoSubscriptionAdapterHandler { extension SelectAssetInteractor: PriceLocalSubscriptionHandler { func handlePrices(result: Result<[PriceData], Error>) { - switch result { - case let .success(prices): - DispatchQueue.global().async { - self.updatePrices(with: prices) - } - case .failure: - break - } - output?.didReceivePricesData(result: result) } } @@ -118,23 +103,4 @@ private extension SelectAssetInteractor { deliveryOn: accountInfosDeliveryQueue ) } - - func updatePrices(with priceData: [PriceData]) { - let updatedAssets = priceData.compactMap { priceData -> AssetModel? in - let chainAsset = chainAssets?.first(where: { $0.asset.priceId == priceData.priceId }) - - guard let asset = chainAsset?.asset else { - return nil - } - return asset.replacingPrice(priceData) - } - - let saveOperation = assetRepository.saveOperation { - updatedAssets - } _: { - [] - } - - operationQueue.addOperation(saveOperation) - } } diff --git a/fearless/Modules/Send/SendPresenter.swift b/fearless/Modules/Send/SendPresenter.swift index ec631dea0d..c71a873d61 100644 --- a/fearless/Modules/Send/SendPresenter.swift +++ b/fearless/Modules/Send/SendPresenter.swift @@ -955,7 +955,7 @@ extension SendPresenter: SendViewOutput { router.showSelectAsset( from: view, wallet: wallet, - selectedAssetId: selectedChainAsset?.asset.identifier, + selectedAssetId: selectedChainAsset?.asset.id, chainAssets: nil, output: self ) From 58868e6056f718f7b4e6dbb652fdde7844c5681b Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Thu, 13 Jun 2024 15:35:23 +0500 Subject: [PATCH 031/115] smp has been updated --- fearless.xcodeproj/project.pbxproj | 2 -- .../xcshareddata/swiftpm/Package.resolved | 21 ++++++++++--------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index ada5d081a4..eea757cf36 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -3099,7 +3099,6 @@ 0702B31929701884003519F5 /* MoneyPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoneyPresentable.swift; sourceTree = ""; }; 0702B354297948C8003519F5 /* SwapTransactionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapTransactionViewModel.swift; sourceTree = ""; }; 0702B35629794BA6003519F5 /* SwapTransactionViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapTransactionViewModelFactory.swift; sourceTree = ""; }; - 070594C62C183BBC0016264E /* shared-features-spm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "shared-features-spm"; path = "../shared-features-spm"; sourceTree = ""; }; 07089AF228B63386001566CA /* UIButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIButton.swift; sourceTree = ""; }; 07089AF428B64701001566CA /* ChainReconnectingEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainReconnectingEvent.swift; sourceTree = ""; }; 07089AF628B64928001566CA /* NetworkIssuesProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkIssuesProviderProtocol.swift; sourceTree = ""; }; @@ -8992,7 +8991,6 @@ 8490139F24A80984008F705E = { isa = PBXGroup; children = ( - 070594C62C183BBC0016264E /* shared-features-spm */, 849013AA24A80984008F705E /* fearless */, 849013C124A80986008F705E /* fearlessTests */, 8438E1D024BFAAD2001BDB13 /* fearlessIntegrationTests */, diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index d996b8ff1e..94f0e9110e 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "48bf9205e83b67ed7d81b2a0bcc8ef081189084205112c031b7823572f561e20", "pins" : [ { "identity" : "appauth-ios", @@ -126,6 +127,15 @@ "version" : "0.1.7" } }, + { + "identity" : "shared-features-spm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/soramitsu/shared-features-spm.git", + "state" : { + "branch" : "feature/xcm-routes", + "revision" : "9cd43ddd0bd5b9c2290dbef939aac09962612323" + } + }, { "identity" : "swift-atomics", "kind" : "remoteSourceControl", @@ -216,15 +226,6 @@ "version" : "1.3.0" } }, - { - "identity" : "swiftformat", - "kind" : "remoteSourceControl", - "location" : "https://github.com/nicklockwood/SwiftFormat", - "state" : { - "revision" : "dd989a46d0c6f15c016484bab8afe5e7a67a4022", - "version" : "0.54.0" - } - }, { "identity" : "swiftimagereadwrite", "kind" : "remoteSourceControl", @@ -298,5 +299,5 @@ } } ], - "version" : 2 + "version" : 3 } From 741f53cc03cc65fb9dbced69e46ada1e27a3a3aa Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Thu, 13 Jun 2024 16:52:45 +0500 Subject: [PATCH 032/115] smp has been updated --- fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index 94f0e9110e..46661e4b6a 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -133,7 +133,7 @@ "location" : "https://github.com/soramitsu/shared-features-spm.git", "state" : { "branch" : "feature/xcm-routes", - "revision" : "9cd43ddd0bd5b9c2290dbef939aac09962612323" + "revision" : "bb5a853743aea9f74d86874fb0cd616998bf7f03" } }, { From a0174cef2a4f52aa4e01dfe13960b1abc01f6c30 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Fri, 14 Jun 2024 13:36:55 +0500 Subject: [PATCH 033/115] [#FLW-4633] New XCM route SORA <-> Astar --- fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index 46661e4b6a..fe110dbb5f 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -133,7 +133,7 @@ "location" : "https://github.com/soramitsu/shared-features-spm.git", "state" : { "branch" : "feature/xcm-routes", - "revision" : "bb5a853743aea9f74d86874fb0cd616998bf7f03" + "revision" : "4e26b1f84957f5eaebb739e7125864cc1bc5371d" } }, { From 1f4ae1542c3373f7671743d2e3eacd304e921ad4 Mon Sep 17 00:00:00 2001 From: Alex Lebedko Date: Fri, 14 Jun 2024 16:50:45 +0700 Subject: [PATCH 034/115] lp raw --- fearless.xcodeproj/project.pbxproj | 88 +++ .../iconLpBanner.imageset/Contents.json | 12 + .../iconLpBanner.imageset/lp_banner.pdf | Bin 0 -> 180797 bytes fearless/Common/View/Stacks.swift | 10 +- fearless/Common/View/UIFactory.swift | 30 + fearless/Common/View/WarningView.swift | 78 +++ .../LiquidityPoolDetailsPresenter.swift | 8 +- .../LiquidityPoolDetailsRouter.swift | 8 +- ...LiquidityPoolRemoveLiquidityAssembly.swift | 70 ++ ...quidityPoolRemoveLiquidityInteractor.swift | 203 ++++++ ...iquidityPoolRemoveLiquidityPresenter.swift | 645 ++++++++++++++++++ ...iquidityPoolRemoveLiquidityProtocols.swift | 27 + .../LiquidityPoolRemoveLiquidityRouter.swift | 43 ++ ...ityPoolRemoveLiquidityViewController.swift | 276 ++++++++ ...quidityPoolRemoveLiquidityViewLayout.swift | 289 ++++++++ ...tyPoolRemoveLiquidityConfirmAssembly.swift | 84 +++ ...yPoolRemoveLiquidityConfirmProtocols.swift | 4 + ...RemoveLiquidityConfirmViewController.swift | 109 +++ ...PoolRemoveLiquidityConfirmViewLayout.swift | 169 +++++ .../LiquidityPoolSupplyPresenter.swift | 5 - .../LiquidityPoolSupplyViewController.swift | 2 + .../LiquidityPoolSupplyViewLayout.swift | 4 +- ...AvailableLiquidityPoolsListPresenter.swift | 4 + ...leLiquidityPoolsListViewModelFactory.swift | 2 +- .../UserLiquidityPoolsListPresenter.swift | 5 + .../LiquidityPoolsListProtocols.swift | 4 +- .../LiquidityPoolsListViewController.swift | 6 +- .../LiquidityPoolsListViewLayout.swift | 18 + .../LiquidityPoolsOverviewPresenter.swift | 8 + .../LiquidityPoolsOverviewProtocols.swift | 7 +- ...LiquidityPoolsOverviewViewController.swift | 10 +- .../LiquidityPoolsOverviewViewLayout.swift | 13 +- .../MainTabBar/MainTabBarWireframe.swift | 20 +- .../PolkaswapAdjustmentPresenter.swift | 4 + .../PolkaswapAdjustmentProtocols.swift | 5 + .../PolkaswapAdjustmentRouter.swift | 13 + .../PolkaswapAdjustmentViewController.swift | 3 + .../PolkaswapAdjustmentViewLayout.swift | 12 + .../LiquidityPoolRemoveLiquidityTests.swift | 16 + ...idityPoolRemoveLiquidityConfirmTests.swift | 16 + 40 files changed, 2293 insertions(+), 37 deletions(-) create mode 100644 fearless/Assets.xcassets/iconLpBanner.imageset/Contents.json create mode 100644 fearless/Assets.xcassets/iconLpBanner.imageset/lp_banner.pdf create mode 100644 fearless/Common/View/WarningView.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityAssembly.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityInteractor.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityPresenter.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityProtocols.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityRouter.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityViewController.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityViewLayout.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmAssembly.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmProtocols.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmViewController.swift create mode 100644 fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmViewLayout.swift create mode 100644 fearlessTests/Modules/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityTests.swift create mode 100644 fearlessTests/Modules/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmTests.swift diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index b821e7df58..8cd9048998 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -235,6 +235,7 @@ 1029947E04F64B616E252949 /* LiquidityPoolSupplyConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F6B892F62579DE761073CA /* LiquidityPoolSupplyConfirmPresenter.swift */; }; 1062C095BC566A1EA8DE1C06 /* CrowdloanContributionSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C71DEF78B69F017DF460AB7 /* CrowdloanContributionSetupViewController.swift */; }; 10B4951F5E0C515EFBDBC32E /* StakingPoolCreateConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3837CE5CB2D48D8A694A7EE0 /* StakingPoolCreateConfirmPresenter.swift */; }; + 10DEF797CB3DC5BF0903EC4C /* LiquidityPoolRemoveLiquidityConfirmViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3459F610D6E5C782D8695A9 /* LiquidityPoolRemoveLiquidityConfirmViewLayout.swift */; }; 134AFD616BE52A1AE290EEF7 /* StakingBalanceFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C67ACC943DA07FC529AE69B4 /* StakingBalanceFlow.swift */; }; 135CEEC5363BE34130958578 /* ControllerAccountConfirmationInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B8473AA386E1AD6F0F0C964 /* ControllerAccountConfirmationInteractor.swift */; }; 1496E87A7652C7D230A9BB46 /* AssetNetworksRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36A3D64FF58C40E5CC6A6E89 /* AssetNetworksRouter.swift */; }; @@ -301,6 +302,7 @@ 2ED5B5FFD880BA1905051E89 /* StakingUnbondSetupFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD8B6213B00597B3F56F650D /* StakingUnbondSetupFlow.swift */; }; 2FCB062A2D873BD72B795DB3 /* AssetSelectionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A0A5EE9BE2862B085712A0 /* AssetSelectionPresenter.swift */; }; 306E249AD210DFAA8C03D435 /* AllDonePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A654294D46A966EE99764F /* AllDonePresenter.swift */; }; + 30C7FD6C58F1ED50AFB456FD /* LiquidityPoolRemoveLiquidityAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = C61BE0DFC48282DFDBB820C9 /* LiquidityPoolRemoveLiquidityAssembly.swift */; }; 3133215566E418F40844A60E /* ExportMnemonicWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ACA4A5B186EE6D40BFE9D66 /* ExportMnemonicWireframe.swift */; }; 31E260D462BF33CFCDFEBA6C /* AnalyticsRewardDetailsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE294DDEAB7902D7CE1F1BA1 /* AnalyticsRewardDetailsProtocols.swift */; }; 3229E306230161AA99B14BDD /* StakingRewardPayoutsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336395FFC4B2104A9651A2DE /* StakingRewardPayoutsViewFactory.swift */; }; @@ -347,6 +349,7 @@ 4470194B729475D683584A6C /* WalletTransactionHistoryInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375E9A7A1206E366E862D81D /* WalletTransactionHistoryInteractor.swift */; }; 44B20C179522F7E38DAA2441 /* WalletTransactionDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 916347CF697C30CD455F436E /* WalletTransactionDetailsViewController.swift */; }; 4641B3CABB5FE1DCFBEDA379 /* CreateContactPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23CF7E56EC624BDB60290387 /* CreateContactPresenter.swift */; }; + 466B1E0EEC6438F8835AAF2E /* LiquidityPoolRemoveLiquidityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835D5A5DD9258786D27BDC23 /* LiquidityPoolRemoveLiquidityViewController.swift */; }; 47995121910705DAE2967AF2 /* ChainSelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C569B746953D3BA947ACEA8D /* ChainSelectionViewController.swift */; }; 4823ED3F25DD943928D102C9 /* NftSendInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B62896CD1DAB8E534C29C96 /* NftSendInteractor.swift */; }; 4838A35CE767AE1C6A198465 /* WalletsManagmentProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5126D2E4032D179A7D210552 /* WalletsManagmentProtocols.swift */; }; @@ -368,6 +371,8 @@ 503DFF0EFCAD0A8B526FEC3A /* SelectMarketViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 320E2207FB549F7C31A80441 /* SelectMarketViewController.swift */; }; 506F0D372BCC8302E513637C /* CrowdloanContributionConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA59CE2C7AE548ACA9D66FD7 /* CrowdloanContributionConfirmWireframe.swift */; }; 50758C9BBB27AE5732FF78BA /* StakingRewardPayoutsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEBC03AB1841681427D38AF /* StakingRewardPayoutsViewController.swift */; }; + 5142E2C6609188D529BB558A /* LiquidityPoolRemoveLiquidityConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882D47A501D9D6CCE7B99691 /* LiquidityPoolRemoveLiquidityConfirmProtocols.swift */; }; + 51876200A6B1EDC54609DF46 /* LiquidityPoolRemoveLiquidityProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D49CA5CB156C1EA38BEBE00 /* LiquidityPoolRemoveLiquidityProtocols.swift */; }; 51FC48FA6FD4D2FB1781424D /* ReferralCrowdloanWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D51D60F19284936A6E9F47D /* ReferralCrowdloanWireframe.swift */; }; 525CCCA7CFD7BE570AD0FCCA /* WalletOptionProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73078ED3B2642FEAF348DB2A /* WalletOptionProtocols.swift */; }; 539340533D8383965751C6D8 /* NodeSelectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0CF2F98779D3C18D0C0A29 /* NodeSelectionTests.swift */; }; @@ -384,6 +389,7 @@ 59745D3C9602745E1417D2F6 /* ChainSelectionInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83AB0AD3A7CECD061611F60C /* ChainSelectionInteractor.swift */; }; 5980BDE494C9E473E5959C71 /* NftCollectionProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD845193EDFC3A1D0BC73719 /* NftCollectionProtocols.swift */; }; 59838EECC194BB0E6E0AEAA2 /* PolkaswapAdjustmentPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B5D683E7DE3533CA418BD21 /* PolkaswapAdjustmentPresenter.swift */; }; + 5A7D43CA17B84C71E8EEF256 /* LiquidityPoolRemoveLiquidityPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B3E9CA265E5C0F3E83429CE /* LiquidityPoolRemoveLiquidityPresenter.swift */; }; 5C796EF8ED29F564B5D1126B /* CrowdloanContributionConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F75722D2F921FD1C2D4105D /* CrowdloanContributionConfirmViewController.swift */; }; 5D0665A581B9F8DFDBD0CF7B /* WalletChainAccountDashboardWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885A9E2D3619FEFC5ED0C093 /* WalletChainAccountDashboardWireframe.swift */; }; 5DDD2206DF795CF205610455 /* AccountExportPasswordPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31226053044986BC828AA912 /* AccountExportPasswordPresenter.swift */; }; @@ -410,6 +416,7 @@ 6B4F5F4FD7820C70FB51A2F9 /* PolkaswapSwapConfirmationAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CA950D301F6605D204E99F3 /* PolkaswapSwapConfirmationAssembly.swift */; }; 6B62E63FB9E379EA19A41464 /* AccountCreateProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48C158C8D1855BCE53636934 /* AccountCreateProtocols.swift */; }; 6BAF97802DB9C640515F47C7 /* StakingMainInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 003FA37F2B240C5D7605340D /* StakingMainInteractor.swift */; }; + 6BF307ADE63FA92389340779 /* LiquidityPoolRemoveLiquidityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C52B689ECDB43EB0FEE95553 /* LiquidityPoolRemoveLiquidityTests.swift */; }; 6C0289728544A96852CCBF20 /* AddCustomNodeProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06A0B252CCD6CAE8C5EDC16 /* AddCustomNodeProtocols.swift */; }; 6C56AB4AE63AB2DC73DE98E0 /* AccountImportInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CC1FB277A878E9C9B7EAEB /* AccountImportInteractor.swift */; }; 6C846ECBBC99A97D1D15C523 /* ReferralCrowdloanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9272FED2C00908028F223E5C /* ReferralCrowdloanTests.swift */; }; @@ -1326,6 +1333,7 @@ 8A109807FBF5FE089DEDBA8E /* FiltersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C85AF940D0B379062D292D93 /* FiltersTests.swift */; }; 8A1FC8AEE234C7FEBF7B6B2E /* CrowdloanContributionConfirmTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385FE8691EA37DE9F562B34E /* CrowdloanContributionConfirmTests.swift */; }; 8A3CC3AAFF6B962CF3BE7BF3 /* CreateContactTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC952210C0714F32C3AE570 /* CreateContactTests.swift */; }; + 8A957CAF82C856E61054B02F /* LiquidityPoolRemoveLiquidityConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 916326B6F6651BBC5913B26F /* LiquidityPoolRemoveLiquidityConfirmViewController.swift */; }; 8AEF593AFE8F59F7DC0A5753 /* CustomValidatorListInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 365CAE2753E7D5F9B9DB7D1F /* CustomValidatorListInteractor.swift */; }; 8B292AD8D20AB9AB5DB905B1 /* WalletOptionAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1E3F963A56923FD036280BD /* WalletOptionAssembly.swift */; }; 8B8B79D7DB787E4B553F0825 /* AnalyticsValidatorsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08EB1FC5907B5165836318C4 /* AnalyticsValidatorsTests.swift */; }; @@ -1383,6 +1391,7 @@ A4FE32D50E4B7CB5B53E0067 /* StakingPoolInfoProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCB1F95F35D50950D0A021E /* StakingPoolInfoProtocols.swift */; }; A565F118B7ED356099662F03 /* ExportMnemonicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E32C2DC4CC106A3509BE651D /* ExportMnemonicTests.swift */; }; A5AB7027E1E73E39E4026C5C /* Pods_fearlessTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 59FDAE57EE0A97872E76E6CE /* Pods_fearlessTests.framework */; }; + A5C7F51539B8D93D9186DA2C /* LiquidityPoolRemoveLiquidityViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419D75A346CE10236161522 /* LiquidityPoolRemoveLiquidityViewLayout.swift */; }; A64E3CA61C6CEE74F2FD9825 /* PolkaswapTransaktionSettingsAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7931155840DACB340284ABBB /* PolkaswapTransaktionSettingsAssembly.swift */; }; A6855830B76B0782A696583A /* AssetNetworksInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C554924081754A981EB4243E /* AssetNetworksInteractor.swift */; }; A69D3B3F6BF76FA9C3070BBD /* AssetNetworksViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFBFF377F35B254AB3141100 /* AssetNetworksViewLayout.swift */; }; @@ -1528,6 +1537,7 @@ AF8193D9F818638254854232 /* StakingMainProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 470B64C45E547C25FCCCFC33 /* StakingMainProtocols.swift */; }; B02EAF42C91E069FE6872EE0 /* SelectValidatorsConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0E02AA5D3EBA9B94950241 /* SelectValidatorsConfirmWireframe.swift */; }; B071927DF8DD5C3CA84494BA /* RecommendedValidatorListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F61D8973ADEB461DE2AD3E13 /* RecommendedValidatorListViewController.swift */; }; + B112AC02371DE4C1DAD48BB1 /* LiquidityPoolRemoveLiquidityRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25D9454047EBBD8D8A0174A4 /* LiquidityPoolRemoveLiquidityRouter.swift */; }; B15D513381B7626AB90018F0 /* StakingPoolInfoInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA0B7C9F68A6BF3D3A6D8234 /* StakingPoolInfoInteractor.swift */; }; B1CCC5B7BF30F6ACA309B112 /* StakingRedeemViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7F5F9B54BE4234C5682BDE /* StakingRedeemViewController.swift */; }; B2E3219218E3F54EEB7D5C3C /* NftSendViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0A0CC21B0CD2A47B9B28841 /* NftSendViewController.swift */; }; @@ -1734,6 +1744,7 @@ E5B4512B99ED86EF398B648E /* WarningAlertTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E587507F8CDA7F84A1A4EA95 /* WarningAlertTests.swift */; }; E5CE21BA40C554E06B566E98 /* NftCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB19F24CAA892FE8B5FE1520 /* NftCollectionViewController.swift */; }; E5F3DF66415E54AE04D0C9A9 /* StakingMainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A05EB4FAF2FDE7DECEA93E4 /* StakingMainViewController.swift */; }; + E667BD4B6BA45B9B3464AD85 /* LiquidityPoolRemoveLiquidityConfirmAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE2059F621D024F85EFBFD0 /* LiquidityPoolRemoveLiquidityConfirmAssembly.swift */; }; E6981A506AC931D30E85169E /* WalletOptionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFD95EE4822A564C0D4D1CFE /* WalletOptionPresenter.swift */; }; E7CAD629FF0D4E97594F7A05 /* YourValidatorListInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B60728FCFBC8A9BE4C7B50B /* YourValidatorListInteractor.swift */; }; E8B8D3D290DC7057144559CE /* WalletChainAccountDashboardPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E83833EB33E51A12F96F83B /* WalletChainAccountDashboardPresenter.swift */; }; @@ -1744,6 +1755,7 @@ EB7B660B0B1BAA915C004A8D /* AnalyticsRewardDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 095DEF513136A26593FB421F /* AnalyticsRewardDetailsViewController.swift */; }; EB9D8D22AA13BF12F845856B /* ReferralCrowdloanProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 953E21C32079A8051A0EE964 /* ReferralCrowdloanProtocols.swift */; }; EC978E6C4FBF39BE9ED10C86 /* SelectValidatorsStartWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 889A825F58F5CB54118A9D35 /* SelectValidatorsStartWireframe.swift */; }; + ECA54AB8148BBA63084353FD /* LiquidityPoolRemoveLiquidityConfirmTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F75A22E215273305AF7AA2 /* LiquidityPoolRemoveLiquidityConfirmTests.swift */; }; EDC02F2FDCDB55519DB0273D /* AnalyticsRewardDetailsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761FDEBB414B1CFAD6992352 /* AnalyticsRewardDetailsTests.swift */; }; EDC72DAB0BDD63E0521E66B5 /* WarningAlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22FD9A4EA33DB4B6AFA5B0C4 /* WarningAlertViewController.swift */; }; EE6FC6EFB089A94EF105F2CC /* StakingRewardPayoutsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934678CCA0EF35B6AE4AE8A1 /* StakingRewardPayoutsTests.swift */; }; @@ -2519,6 +2531,7 @@ FA8810D52BDCD19D0084CC4B /* UserLiquidityPoolsListViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA8810D42BDCD19D0084CC4B /* UserLiquidityPoolsListViewModelFactory.swift */; }; FA887A452C107A1100CA720F /* LiquidityPoolSupplyConfirmViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA887A442C107A1100CA720F /* LiquidityPoolSupplyConfirmViewModel.swift */; }; FA887A472C107A4300CA720F /* LiquidityPoolSupplyConfirmViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA887A462C107A4300CA720F /* LiquidityPoolSupplyConfirmViewModelFactory.swift */; }; + FA887A492C1C19DB00CA720F /* WarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA887A482C1C19DB00CA720F /* WarningView.swift */; }; FA8ED43328FD960F00EBB712 /* YourValidatorListFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA8ED43228FD960F00EBB712 /* YourValidatorListFlow.swift */; }; FA8ED43628FD983A00EBB712 /* YourValidatorListRelaychainStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA8ED43528FD983A00EBB712 /* YourValidatorListRelaychainStrategy.swift */; }; FA8ED43828FD984500EBB712 /* YourValidatorListRelaychainViewModelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA8ED43728FD984500EBB712 /* YourValidatorListRelaychainViewModelState.swift */; }; @@ -3101,6 +3114,7 @@ FB377FFB8421F09C875E1968 /* NftDetailsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09373C708FE543066B943E45 /* NftDetailsInteractor.swift */; }; FB6B2B551238260D754E036D /* NftSendConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 447D03660EFFD8CF46374D85 /* NftSendConfirmViewController.swift */; }; FBE4702B7F95484235386631 /* LiquidityPoolSupplyConfirmRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5121DA4DDF11ED6A6659CEB /* LiquidityPoolSupplyConfirmRouter.swift */; }; + FC540426B1BC363842A3677B /* LiquidityPoolRemoveLiquidityInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6887529305794E17D9434D44 /* LiquidityPoolRemoveLiquidityInteractor.swift */; }; FC54046997436059EF436983 /* NftSendRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01A85735F958D05CFEFCB4DF /* NftSendRouter.swift */; }; FC9BD9BC4722D8B17B7A7ACC /* MainNftContainerProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0892AD886F2BE499C075C701 /* MainNftContainerProtocols.swift */; }; FE65F897D63CAA8F8AE4B972 /* NftCollectionAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DE7B3D0BE06472153C0A78C /* NftCollectionAssembly.swift */; }; @@ -3369,6 +3383,7 @@ 1A7B39A61DB0D2F0F1B1DBA1 /* AccountCreateInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountCreateInteractor.swift; sourceTree = ""; }; 1AF4258723E2FACBBA556D00 /* WalletSendConfirmTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletSendConfirmTests.swift; sourceTree = ""; }; 1B08B264C09E63A936384E2A /* NftDetailsRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftDetailsRouter.swift; sourceTree = ""; }; + 1B3E9CA265E5C0F3E83429CE /* LiquidityPoolRemoveLiquidityPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolRemoveLiquidityPresenter.swift; sourceTree = ""; }; 1BE8644A4F6DED808248A0FE /* YourValidatorListViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = YourValidatorListViewFactory.swift; sourceTree = ""; }; 1C52C6CD7112BF0E1E3A98CE /* PurchaseWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PurchaseWireframe.swift; sourceTree = ""; }; 1CA950D301F6605D204E99F3 /* PolkaswapSwapConfirmationAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PolkaswapSwapConfirmationAssembly.swift; sourceTree = ""; }; @@ -3389,6 +3404,7 @@ 23BC71941B91D3E372CDB11C /* CrowdloanContributionSetupViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionSetupViewLayout.swift; sourceTree = ""; }; 23CF7E56EC624BDB60290387 /* CreateContactPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateContactPresenter.swift; sourceTree = ""; }; 25B80FDDB5C3032A0BBBD826 /* NftCollectionPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftCollectionPresenter.swift; sourceTree = ""; }; + 25D9454047EBBD8D8A0174A4 /* LiquidityPoolRemoveLiquidityRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolRemoveLiquidityRouter.swift; sourceTree = ""; }; 25FF82C2FD912021A1F20876 /* PolkaswapAdjustmentViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PolkaswapAdjustmentViewLayout.swift; sourceTree = ""; }; 262F98DEF54FA9592BE22B94 /* AllDoneAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AllDoneAssembly.swift; sourceTree = ""; }; 2648EEF96694A7FEC94520E8 /* WalletHistoryFilterTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletHistoryFilterTests.swift; sourceTree = ""; }; @@ -3426,6 +3442,7 @@ 31226053044986BC828AA912 /* AccountExportPasswordPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountExportPasswordPresenter.swift; sourceTree = ""; }; 312DE7ADA5ABC3214AD3D4AD /* StakingAmountInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingAmountInteractor.swift; sourceTree = ""; }; 31302AE4D5325D2AEC030832 /* AllDoneRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AllDoneRouter.swift; sourceTree = ""; }; + 31F75A22E215273305AF7AA2 /* LiquidityPoolRemoveLiquidityConfirmTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolRemoveLiquidityConfirmTests.swift; sourceTree = ""; }; 31F848482B2AD7D6831B0CCE /* LiquidityPoolSupplyPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyPresenter.swift; sourceTree = ""; }; 320E2207FB549F7C31A80441 /* SelectMarketViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectMarketViewController.swift; sourceTree = ""; }; 3211D250E4167C916B8B9D6A /* NetworkIssuesNotificationRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NetworkIssuesNotificationRouter.swift; sourceTree = ""; }; @@ -3451,6 +3468,7 @@ 3B8473AA386E1AD6F0F0C964 /* ControllerAccountConfirmationInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ControllerAccountConfirmationInteractor.swift; sourceTree = ""; }; 3BA8DC8007FC0A322C6DF00E /* LiquidityPoolsOverviewPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsOverviewPresenter.swift; sourceTree = ""; }; 3D2C2FC3E31C03D08BDEC7A1 /* PolkaswapAdjustmentRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PolkaswapAdjustmentRouter.swift; sourceTree = ""; }; + 3D49CA5CB156C1EA38BEBE00 /* LiquidityPoolRemoveLiquidityProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolRemoveLiquidityProtocols.swift; sourceTree = ""; }; 3E992CCDC1D581F7E9D3F1CA /* AccountConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountConfirmInteractor.swift; sourceTree = ""; }; 3EDA19BC3280F1838C687EC8 /* NetworkInfoViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NetworkInfoViewFactory.swift; sourceTree = ""; }; 3F1D5849A2EBF462B32F3A9C /* ExportSeedProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExportSeedProtocols.swift; sourceTree = ""; }; @@ -3548,6 +3566,7 @@ 63B11A7ADEF107B6341C378F /* Pods-fearlessAll-fearlessIntegrationTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-fearlessAll-fearlessIntegrationTests.release.xcconfig"; path = "Target Support Files/Pods-fearlessAll-fearlessIntegrationTests/Pods-fearlessAll-fearlessIntegrationTests.release.xcconfig"; sourceTree = ""; }; 63CCCC63389A70294F816143 /* NodeSelectionProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NodeSelectionProtocols.swift; sourceTree = ""; }; 63F4BE52D0625CD8C21D2460 /* CrowdloanListViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanListViewLayout.swift; sourceTree = ""; }; + 6419D75A346CE10236161522 /* LiquidityPoolRemoveLiquidityViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolRemoveLiquidityViewLayout.swift; sourceTree = ""; }; 641B699003FF648A380F7FA6 /* LiquidityPoolSupplyConfirmViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyConfirmViewLayout.swift; sourceTree = ""; }; 6503D178156C6407EC848D41 /* LiquidityPoolSupplyRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyRouter.swift; sourceTree = ""; }; 65AD15693E21C869DE1FDD17 /* UsernameSetupWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UsernameSetupWireframe.swift; sourceTree = ""; }; @@ -3556,6 +3575,7 @@ 67B1037AEC97DBEAF9FD50C1 /* AssetNetworksPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetNetworksPresenter.swift; sourceTree = ""; }; 67B3E1906EEBE32E71E82BB6 /* NodeSelectionViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NodeSelectionViewLayout.swift; sourceTree = ""; }; 67BDE520860A67C800E7F4AB /* LiquidityPoolDetailsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolDetailsViewController.swift; sourceTree = ""; }; + 6887529305794E17D9434D44 /* LiquidityPoolRemoveLiquidityInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolRemoveLiquidityInteractor.swift; sourceTree = ""; }; 6897929D244B5C29E3FD0727 /* StakingPoolCreateRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPoolCreateRouter.swift; sourceTree = ""; }; 69B00AC48FB7D11855875EB9 /* SwapTransactionDetailPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapTransactionDetailPresenter.swift; sourceTree = ""; }; 6A28799A82C0EC2F8ABDE831 /* Pods_fearlessAll_fearless.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_fearlessAll_fearless.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -3603,6 +3623,7 @@ 82072889A33037BFC9D8F574 /* NftDetailsTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftDetailsTests.swift; sourceTree = ""; }; 82CBCBA8BF2D753248238555 /* ContactsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ContactsViewController.swift; sourceTree = ""; }; 82FD4E0BB1ABA364AFD2E891 /* WalletTransactionDetailsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletTransactionDetailsPresenter.swift; sourceTree = ""; }; + 835D5A5DD9258786D27BDC23 /* LiquidityPoolRemoveLiquidityViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolRemoveLiquidityViewController.swift; sourceTree = ""; }; 83AB0AD3A7CECD061611F60C /* ChainSelectionInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChainSelectionInteractor.swift; sourceTree = ""; }; 8400F0B7252BBCFA00E6B4CB /* WalletSelectAccountCommandTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletSelectAccountCommandTests.swift; sourceTree = ""; }; 8400F0B9252BBD2200E6B4CB /* WalletCommandFactoryProtocolMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletCommandFactoryProtocolMock.swift; sourceTree = ""; }; @@ -4471,6 +4492,7 @@ 86F7A369E31DCB9ABD556EE9 /* CrowdloanListPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanListPresenter.swift; sourceTree = ""; }; 87F9D215513308538FA3FDC4 /* ContactsTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ContactsTests.swift; sourceTree = ""; }; 8821119C96944A0E3526E93A /* StakingRedeemViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRedeemViewFactory.swift; sourceTree = ""; }; + 882D47A501D9D6CCE7B99691 /* LiquidityPoolRemoveLiquidityConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolRemoveLiquidityConfirmProtocols.swift; sourceTree = ""; }; 885A9E2D3619FEFC5ED0C093 /* WalletChainAccountDashboardWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletChainAccountDashboardWireframe.swift; sourceTree = ""; }; 889A825F58F5CB54118A9D35 /* SelectValidatorsStartWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectValidatorsStartWireframe.swift; sourceTree = ""; }; 890203CBFFCBF517C0BAA396 /* YourValidatorListTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = YourValidatorListTests.swift; sourceTree = ""; }; @@ -4489,6 +4511,7 @@ 900E67A76C0702F8123EBB47 /* AddCustomNodeInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddCustomNodeInteractor.swift; sourceTree = ""; }; 906C55FC079AF6112AF0745B /* YourValidatorListWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = YourValidatorListWireframe.swift; sourceTree = ""; }; 909ABB38BF714F553063697F /* StakingRebondConfirmationFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRebondConfirmationFlow.swift; sourceTree = ""; }; + 916326B6F6651BBC5913B26F /* LiquidityPoolRemoveLiquidityConfirmViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolRemoveLiquidityConfirmViewController.swift; sourceTree = ""; }; 916347CF697C30CD455F436E /* WalletTransactionDetailsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletTransactionDetailsViewController.swift; sourceTree = ""; }; 91D44421CCD7AD220A05CD0E /* PurchaseInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PurchaseInteractor.swift; sourceTree = ""; }; 9272FED2C00908028F223E5C /* ReferralCrowdloanTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferralCrowdloanTests.swift; sourceTree = ""; }; @@ -4674,6 +4697,7 @@ B264116DA8F64446D46E12DE /* WalletTransactionDetailsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletTransactionDetailsProtocols.swift; sourceTree = ""; }; B2655EE5CFAAD20C0FF59188 /* AssetSelectionTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetSelectionTests.swift; sourceTree = ""; }; B29514E516CEAAB159851D95 /* AccountImportProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountImportProtocols.swift; sourceTree = ""; }; + B3459F610D6E5C782D8695A9 /* LiquidityPoolRemoveLiquidityConfirmViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolRemoveLiquidityConfirmViewLayout.swift; sourceTree = ""; }; B399E7CA0A03A06EFDF1B126 /* PolkaswapTransaktionSettingsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PolkaswapTransaktionSettingsPresenter.swift; sourceTree = ""; }; B4774F00EDBB28F374797637 /* ClaimCrowdloanRewardsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ClaimCrowdloanRewardsInteractor.swift; sourceTree = ""; }; B5934BA68F375F5F8237967D /* NetworkInfoViewController.xib */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file.xib; path = NetworkInfoViewController.xib; sourceTree = ""; }; @@ -4695,6 +4719,7 @@ C316BE4F5A0342D379F783E8 /* StartSelectValidatorsTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StartSelectValidatorsTests.swift; sourceTree = ""; }; C323602A21644DCB1B2EEFF6 /* Pods_fearlessAll_fearlessIntegrationTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_fearlessAll_fearlessIntegrationTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C503100478AB56E903598A78 /* ReferralCrowdloanPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferralCrowdloanPresenter.swift; sourceTree = ""; }; + C52B689ECDB43EB0FEE95553 /* LiquidityPoolRemoveLiquidityTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolRemoveLiquidityTests.swift; sourceTree = ""; }; C53DFFFB3B5B48DB51692EFA /* LiquidityPoolDetailsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolDetailsInteractor.swift; sourceTree = ""; }; C554924081754A981EB4243E /* AssetNetworksInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetNetworksInteractor.swift; sourceTree = ""; }; C569B746953D3BA947ACEA8D /* ChainSelectionViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChainSelectionViewController.swift; sourceTree = ""; }; @@ -4707,6 +4732,7 @@ C611666B2B3C03B800F483C4 /* NftHeaderCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NftHeaderCellViewModel.swift; sourceTree = ""; }; C615668029309D3900391BF3 /* QRService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRService.swift; sourceTree = ""; }; C61566842930A07900391BF3 /* QRCreationOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRCreationOperation.swift; sourceTree = ""; }; + C61BE0DFC48282DFDBB820C9 /* LiquidityPoolRemoveLiquidityAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolRemoveLiquidityAssembly.swift; sourceTree = ""; }; C62522E202A1C5EE60D25122 /* NftSendPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftSendPresenter.swift; sourceTree = ""; }; C6264C282799A56E00FCA0DB /* WalletDetailsTableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletDetailsTableCell.swift; sourceTree = ""; }; C6264C2B2799C15C00FCA0DB /* WalletDetailsCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletDetailsCellViewModel.swift; sourceTree = ""; }; @@ -4885,6 +4911,7 @@ EC012CF1C792B34BD5FF45A2 /* NftDetailsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftDetailsProtocols.swift; sourceTree = ""; }; EC863A9CE29C63B740C6E4D9 /* LiquidityPoolsOverviewAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsOverviewAssembly.swift; sourceTree = ""; }; ECBF10B7D4707E4D7D6387CF /* FiltersViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FiltersViewFactory.swift; sourceTree = ""; }; + ECE2059F621D024F85EFBFD0 /* LiquidityPoolRemoveLiquidityConfirmAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolRemoveLiquidityConfirmAssembly.swift; sourceTree = ""; }; ED916AAFB6B5A7FA0C802615 /* Pods-fearlessTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-fearlessTests.debug.xcconfig"; path = "Target Support Files/Pods-fearlessTests/Pods-fearlessTests.debug.xcconfig"; sourceTree = ""; }; EDB9EDB05686DF11958145E1 /* ControllerAccountWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ControllerAccountWireframe.swift; sourceTree = ""; }; EDDA0B079962E00FAFBE07AD /* ChainSelectionProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChainSelectionProtocols.swift; sourceTree = ""; }; @@ -5634,6 +5661,7 @@ FA8810D42BDCD19D0084CC4B /* UserLiquidityPoolsListViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLiquidityPoolsListViewModelFactory.swift; sourceTree = ""; }; FA887A442C107A1100CA720F /* LiquidityPoolSupplyConfirmViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyConfirmViewModel.swift; sourceTree = ""; }; FA887A462C107A4300CA720F /* LiquidityPoolSupplyConfirmViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyConfirmViewModelFactory.swift; sourceTree = ""; }; + FA887A482C1C19DB00CA720F /* WarningView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WarningView.swift; sourceTree = ""; }; FA8C34B051607218638BA851 /* FiltersProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FiltersProtocols.swift; sourceTree = ""; }; FA8ED43228FD960F00EBB712 /* YourValidatorListFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YourValidatorListFlow.swift; sourceTree = ""; }; FA8ED43528FD983A00EBB712 /* YourValidatorListRelaychainStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YourValidatorListRelaychainStrategy.swift; sourceTree = ""; }; @@ -7479,6 +7507,14 @@ path = LiquidityPoolDetails; sourceTree = ""; }; + 48EAF80DCC0C537917FC5A23 /* LiquidityPoolRemoveLiquidity */ = { + isa = PBXGroup; + children = ( + C52B689ECDB43EB0FEE95553 /* LiquidityPoolRemoveLiquidityTests.swift */, + ); + path = LiquidityPoolRemoveLiquidity; + sourceTree = ""; + }; 493D529207797CDC4F179A5C /* NftSend */ = { isa = PBXGroup; children = ( @@ -7747,6 +7783,8 @@ 9439C16432098735E8F4C122 /* LiquidityPoolDetails */, 04265CAB3B55A71F49337293 /* LiquidityPoolSupply */, 749EAA035EECE4D63C56C358 /* LiquidityPoolSupplyConfirm */, + 48EAF80DCC0C537917FC5A23 /* LiquidityPoolRemoveLiquidity */, + BDB80385E6818AE7707DDFF8 /* LiquidityPoolRemoveLiquidityConfirm */, ); path = Modules; sourceTree = ""; @@ -9840,6 +9878,7 @@ FAC6CD972BA807D30013A17E /* AccessoryView.swift */, FA2222932BD2726F0031DE04 /* SkeletonLabel.swift */, FA2222952BD272A30031DE04 /* SkeletonLoadableView.swift */, + FA887A482C1C19DB00CA720F /* WarningView.swift */, ); path = View; sourceTree = ""; @@ -11531,6 +11570,14 @@ path = NodeSelection; sourceTree = ""; }; + BDB80385E6818AE7707DDFF8 /* LiquidityPoolRemoveLiquidityConfirm */ = { + isa = PBXGroup; + children = ( + 31F75A22E215273305AF7AA2 /* LiquidityPoolRemoveLiquidityConfirmTests.swift */, + ); + path = LiquidityPoolRemoveLiquidityConfirm; + sourceTree = ""; + }; BF6F50DD15230CADAC713359 /* AccountImport */ = { isa = PBXGroup; children = ( @@ -11873,6 +11920,20 @@ path = ReferralCrowdloan; sourceTree = ""; }; + D8F784121F6FDD3BCB25424B /* LiquidityPoolRemoveLiquidity */ = { + isa = PBXGroup; + children = ( + 3D49CA5CB156C1EA38BEBE00 /* LiquidityPoolRemoveLiquidityProtocols.swift */, + 25D9454047EBBD8D8A0174A4 /* LiquidityPoolRemoveLiquidityRouter.swift */, + 1B3E9CA265E5C0F3E83429CE /* LiquidityPoolRemoveLiquidityPresenter.swift */, + 6887529305794E17D9434D44 /* LiquidityPoolRemoveLiquidityInteractor.swift */, + 835D5A5DD9258786D27BDC23 /* LiquidityPoolRemoveLiquidityViewController.swift */, + 6419D75A346CE10236161522 /* LiquidityPoolRemoveLiquidityViewLayout.swift */, + C61BE0DFC48282DFDBB820C9 /* LiquidityPoolRemoveLiquidityAssembly.swift */, + ); + path = LiquidityPoolRemoveLiquidity; + sourceTree = ""; + }; DA46895F73B0FB1064146E47 /* AssetNetworks */ = { isa = PBXGroup; children = ( @@ -12043,6 +12104,17 @@ path = Purchase; sourceTree = ""; }; + F27AF708352372E8D743CD3F /* LiquidityPoolRemoveLiquidityConfirm */ = { + isa = PBXGroup; + children = ( + 882D47A501D9D6CCE7B99691 /* LiquidityPoolRemoveLiquidityConfirmProtocols.swift */, + 916326B6F6651BBC5913B26F /* LiquidityPoolRemoveLiquidityConfirmViewController.swift */, + B3459F610D6E5C782D8695A9 /* LiquidityPoolRemoveLiquidityConfirmViewLayout.swift */, + ECE2059F621D024F85EFBFD0 /* LiquidityPoolRemoveLiquidityConfirmAssembly.swift */, + ); + path = LiquidityPoolRemoveLiquidityConfirm; + sourceTree = ""; + }; F400A7C0260CE1560061D576 /* Model */ = { isa = PBXGroup; children = ( @@ -12622,6 +12694,8 @@ FA1D51D42BCFD410001353E7 /* LiquidityPools */ = { isa = PBXGroup; children = ( + D8F784121F6FDD3BCB25424B /* LiquidityPoolRemoveLiquidity */, + F27AF708352372E8D743CD3F /* LiquidityPoolRemoveLiquidityConfirm */, 3362664DE1DD71BB75722C2D /* LiquidityPoolSupplyConfirm */, EE3CBEFADE55F44DE05DCEF2 /* LiquidityPoolSupply */, 478C62A42D572C8647512722 /* LiquidityPoolDetails */, @@ -17623,6 +17697,7 @@ FAC6CD9F2BA80AB70013A17E /* WalletLanguage.swift in Sources */, 84DA3B1924C8200E00B5E27F /* ConnectionItem+Default.swift in Sources */, FAD429072A86567F001D6A16 /* BackupCreatePasswordProtocols.swift in Sources */, + FA887A492C1C19DB00CA720F /* WarningView.swift in Sources */, 84D331AF2519E8080078D044 /* TriangularedView+Style.swift in Sources */, 84C74365251E4D60009576C6 /* SigningWrapperProtocol.swift in Sources */, 844CB56A26F9C57D00396E13 /* WalletLocalStorageSubscriber.swift in Sources */, @@ -19037,6 +19112,17 @@ 24E6794278FEC01DFFE7C69C /* LiquidityPoolSupplyConfirmViewController.swift in Sources */, 74A39BC366D85CC5FCA78579 /* LiquidityPoolSupplyConfirmViewLayout.swift in Sources */, 991DA2DE1E8A45B0A8B187CD /* LiquidityPoolSupplyConfirmAssembly.swift in Sources */, + 51876200A6B1EDC54609DF46 /* LiquidityPoolRemoveLiquidityProtocols.swift in Sources */, + B112AC02371DE4C1DAD48BB1 /* LiquidityPoolRemoveLiquidityRouter.swift in Sources */, + 5A7D43CA17B84C71E8EEF256 /* LiquidityPoolRemoveLiquidityPresenter.swift in Sources */, + FC540426B1BC363842A3677B /* LiquidityPoolRemoveLiquidityInteractor.swift in Sources */, + 466B1E0EEC6438F8835AAF2E /* LiquidityPoolRemoveLiquidityViewController.swift in Sources */, + A5C7F51539B8D93D9186DA2C /* LiquidityPoolRemoveLiquidityViewLayout.swift in Sources */, + 30C7FD6C58F1ED50AFB456FD /* LiquidityPoolRemoveLiquidityAssembly.swift in Sources */, + 5142E2C6609188D529BB558A /* LiquidityPoolRemoveLiquidityConfirmProtocols.swift in Sources */, + 8A957CAF82C856E61054B02F /* LiquidityPoolRemoveLiquidityConfirmViewController.swift in Sources */, + 10DEF797CB3DC5BF0903EC4C /* LiquidityPoolRemoveLiquidityConfirmViewLayout.swift in Sources */, + E667BD4B6BA45B9B3464AD85 /* LiquidityPoolRemoveLiquidityConfirmAssembly.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -19211,6 +19297,8 @@ 6D0E50CEBCB73C23A75A7F46 /* LiquidityPoolDetailsTests.swift in Sources */, D8C33C4064C3B2F30BB478A5 /* LiquidityPoolSupplyTests.swift in Sources */, B40863AA7377BFE5F8A30259 /* LiquidityPoolSupplyConfirmTests.swift in Sources */, + 6BF307ADE63FA92389340779 /* LiquidityPoolRemoveLiquidityTests.swift in Sources */, + ECA54AB8148BBA63084353FD /* LiquidityPoolRemoveLiquidityConfirmTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/fearless/Assets.xcassets/iconLpBanner.imageset/Contents.json b/fearless/Assets.xcassets/iconLpBanner.imageset/Contents.json new file mode 100644 index 0000000000..348e8808b6 --- /dev/null +++ b/fearless/Assets.xcassets/iconLpBanner.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "lp_banner.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fearless/Assets.xcassets/iconLpBanner.imageset/lp_banner.pdf b/fearless/Assets.xcassets/iconLpBanner.imageset/lp_banner.pdf new file mode 100644 index 0000000000000000000000000000000000000000..696df1f940e514a085000f2f76c20fbfe3b09511 GIT binary patch literal 180797 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 u!4aea0@~KVx%%6VT~VyN@OO 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 SYr6;n6eA#%e0+*pO8*C0GP>me literal 0 HcmV?d00001 diff --git a/fearless/Common/View/Stacks.swift b/fearless/Common/View/Stacks.swift index 7237b9f34a..20d02c760d 100644 --- a/fearless/Common/View/Stacks.swift +++ b/fearless/Common/View/Stacks.swift @@ -199,16 +199,16 @@ private final class Spacer: UIView { func resetColors() { colors = [.systemRed, .systemBlue, .systemGreen, .systemOrange, .systemTeal, .systemPink, .systemPurple, .systemIndigo].reversed() } - func addBorder(view: UIView) { + func addBorder(setupView: UIView) { if colors.isEmpty { resetColors() } - view.border(colors.removeFirst(), width: 1) - for subview in view.subviews { - addBorder(view: subview) + setupView.border(colors.removeFirst(), width: 1) + for subview in setupView.subviews { + addBorder(setupView: subview) } } - addBorder(view: self) + addBorder(setupView: self) } @discardableResult func border(_ color: UIColor, width: CGFloat) -> UIView { diff --git a/fearless/Common/View/UIFactory.swift b/fearless/Common/View/UIFactory.swift index ab4f58e363..ed763049ec 100644 --- a/fearless/Common/View/UIFactory.swift +++ b/fearless/Common/View/UIFactory.swift @@ -104,6 +104,7 @@ protocol UIFactoryProtocol { ) -> UIToolbar func createDisabledButton() -> TriangularedButton func createRoundedButton() -> UIButton + func createWarningView(title: String, text: String) -> UIView } extension UIFactoryProtocol { @@ -731,4 +732,33 @@ final class UIFactory: UIFactoryProtocol { button.layer.cornerRadius = 12 return button } + + func createWarningView(title _: String, text _: String) -> UIView { + let iconView = UIImageView(image: R.image.iconWarning()) + iconView.snp.makeConstraints { make in + make.size.equalTo(16) + } + + let hStack = UIFactory.default.createHorizontalStackView(spacing: 8) + hStack.alignment = .top + hStack.distribution = .fillProportionally + + let vStack = UIFactory.default.createVerticalStackView(spacing: 4) + + hStack.addArrangedSubview(iconView) + hStack.addArrangedSubview(vStack) + + let titleLabel = UILabel() + titleLabel.textColor = R.color.colorOrange() + titleLabel.font = .h6Title + + let textLabel = UILabel() + textLabel.textColor = R.color.colorWhite50() + textLabel.font = .p3Paragraph + + vStack.addArrangedSubview(titleLabel) + vStack.addArrangedSubview(textLabel) + + return hStack + } } diff --git a/fearless/Common/View/WarningView.swift b/fearless/Common/View/WarningView.swift new file mode 100644 index 0000000000..c98435af47 --- /dev/null +++ b/fearless/Common/View/WarningView.swift @@ -0,0 +1,78 @@ +import UIKit + +class WarningView: UIView { + let backgroundView: TriangularedView = { + let view = TriangularedView() + view.fillColor = R.color.colorSemiBlack()! + view.highlightedFillColor = R.color.colorSemiBlack()! + view.strokeColor = R.color.colorWhite16()! + view.highlightedStrokeColor = R.color.colorWhite16()! + view.strokeWidth = 0.5 + view.shadowOpacity = 0.0 + + return view + }() + + let iconView = UIImageView(image: R.image.iconWarning()) + + let hStack: UIStackView = { + let hStack = UIFactory.default.createHorizontalStackView(spacing: 8) + hStack.alignment = .center + hStack.distribution = .fillProportionally + return hStack + }() + + let vStack = UIFactory.default.createVerticalStackView(spacing: 4) + + let titleLabel: UILabel = { + let titleLabel = UILabel() + titleLabel.textColor = R.color.colorOrange() + titleLabel.font = .h6Title + return titleLabel + }() + + let textLabel: UILabel = { + let textLabel = UILabel() + textLabel.textColor = R.color.colorWhite50() + textLabel.font = .p3Paragraph + textLabel.numberOfLines = 0 + return textLabel + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + drawSubviews() + setupConstraints() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func drawSubviews() { + addSubview(backgroundView) + backgroundView.addSubview(hStack) + + hStack.addArrangedSubview(iconView) + hStack.addArrangedSubview(vStack) + + vStack.addArrangedSubview(titleLabel) + vStack.addArrangedSubview(textLabel) + } + + private func setupConstraints() { + backgroundView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + hStack.snp.makeConstraints { make in + make.edges.equalToSuperview().inset(12) + } + + iconView.snp.makeConstraints { make in + make.size.equalTo(16) + } + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift index 27d7e1e4ff..3c14d77cde 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift @@ -164,7 +164,13 @@ extension LiquidityPoolDetailsPresenter: LiquidityPoolDetailsViewOutput { router.showSupplyFlow(liquidityPair: liquidityPair, chain: chain, wallet: wallet, from: view) } - func removeButtonClicked() {} + func removeButtonClicked() { + guard let liquidityPair else { + return + } + + router.showRemoveFlow(liquidityPair: liquidityPair, chain: chain, wallet: wallet, from: view) + } } // MARK: - LiquidityPoolDetailsInteractorOutput diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsRouter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsRouter.swift index ac1cd88191..f36f09cdc4 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsRouter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsRouter.swift @@ -11,5 +11,11 @@ final class LiquidityPoolDetailsRouter: LiquidityPoolDetailsRouterInput { view?.controller.navigationController?.pushViewController(controller, animated: true) } - func showRemoveFlow(liquidityPair _: LiquidityPair, chain _: ChainModel, wallet _: MetaAccountModel, from _: ControllerBackedProtocol?) {} + func showRemoveFlow(liquidityPair: LiquidityPair, chain: ChainModel, wallet: MetaAccountModel, from view: ControllerBackedProtocol?) { + guard let controller = LiquidityPoolRemoveLiquidityAssembly.configureModule(wallet: wallet, chain: chain, liquidityPair: liquidityPair)?.view.controller else { + return + } + + view?.controller.navigationController?.pushViewController(controller, animated: true) + } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityAssembly.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityAssembly.swift new file mode 100644 index 0000000000..049b38dd7c --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityAssembly.swift @@ -0,0 +1,70 @@ +import UIKit +import SoraFoundation +import SSFModels +import SSFPools +import SSFPolkaswap +import SoraKeystore + +final class LiquidityPoolRemoveLiquidityAssembly { + static func configureModule(wallet: MetaAccountModel, chain: ChainModel, liquidityPair: LiquidityPair) -> LiquidityPoolRemoveLiquidityModuleCreationResult? { + guard let response = wallet.fetch(for: chain.accountRequest()) else { + return nil + } + + guard let secretKeyData = try? fetchSecretKey( + for: chain, + metaId: wallet.metaId, + accountResponse: response + ) else { + return nil + } + + let localizationManager = LocalizationManager.shared + let chainRegistry = ChainRegistryFacade.sharedRegistry + let lpDataService = PolkaswapLiquidityPoolServiceAssembly.buildService(for: chain, chainRegistry: chainRegistry) + let signingWrapperData = SigningWrapperData(publicKeyData: response.publicKey, secretKeyData: secretKeyData) + + guard let lpOperationService = try? PolkaswapLiquidityPoolServiceAssembly.buildOperationService( + for: chain, + wallet: wallet.utilsModel, + chainRegistry: chainRegistry, + signingWrapperData: signingWrapperData + ) else { + return nil + } + + let accountInfoSubscriptionAdapter = AccountInfoSubscriptionAdapter( + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, + selectedMetaAccount: wallet + ) + + let interactor = LiquidityPoolRemoveLiquidityInteractor(lpOperationService: lpOperationService, lpDataService: lpDataService, liquidityPair: liquidityPair, priceLocalSubscriber: PriceLocalStorageSubscriberImpl.shared, chain: chain, accountInfoSubscriptionAdapter: accountInfoSubscriptionAdapter, wallet: wallet) + let router = LiquidityPoolRemoveLiquidityRouter() + let dataValidatingFactory = SendDataValidatingFactory(presentable: router) + let presenter = LiquidityPoolRemoveLiquidityPresenter(interactor: interactor, router: router, localizationManager: localizationManager, wallet: wallet, logger: Logger.shared, chain: chain, liquidityPair: liquidityPair, dataValidatingFactory: dataValidatingFactory, confirmViewModelFactory: nil, removeInfo: nil) + + let view = LiquidityPoolRemoveLiquidityViewController( + output: presenter, + localizationManager: localizationManager + ) + + dataValidatingFactory.view = view + + return (view, presenter) + } + + static func fetchSecretKey( + for chain: ChainModel, + metaId: String, + accountResponse: ChainAccountResponse + ) throws -> Data { + let accountId = accountResponse.isChainAccount ? accountResponse.accountId : nil + let tag: String = chain.isEthereumBased + ? KeystoreTagV2.ethereumSecretKeyTagForMetaId(metaId, accountId: accountId) + : KeystoreTagV2.substrateSecretKeyTagForMetaId(metaId, accountId: accountId) + + let keystore = Keychain() + let secretKey = try keystore.fetchKey(for: tag) + return secretKey + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityInteractor.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityInteractor.swift new file mode 100644 index 0000000000..0ea98ecb8a --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityInteractor.swift @@ -0,0 +1,203 @@ +import UIKit +import SSFPools +import SSFPolkaswap +import SSFModels +import BigInt + +protocol LiquidityPoolRemoveLiquidityInteractorOutput: AnyObject { + func didReceivePricesData(result: Result<[PriceData], Error>) + func didReceiveAccountInfo(result: Result, for chainAsset: ChainAsset) + func didReceiveDexId(_ dexId: String) + func didReceiveDexIdError(_ error: Error) + func didReceiveUserPool(pool: AccountPool?) + func didReceiveUserPoolError(error: Error) + func didReceiveFee(_ fee: BigUInt) + func didReceiveFeeError(_ error: Error) + func didReceivePoolReserves(reserves: PolkaswapPoolReservesInfo?) + func didReceivePoolReservesError(error: Error) + func didReceiveTotalIssuance(totalIssuance: BigUInt?) + func didReceiveTotalIssuanceError(error: Error) + func didReceiveTransactionHash(_ hash: String) + func didReceiveSubmitError(error: Error) +} + +final class LiquidityPoolRemoveLiquidityInteractor { + // MARK: - Private properties + + private weak var output: LiquidityPoolRemoveLiquidityInteractorOutput? + private let lpOperationService: PoolsOperationService + private let lpDataService: PolkaswapLiquidityPoolService + private let liquidityPair: LiquidityPair + private let priceLocalSubscriber: PriceLocalStorageSubscriber + private let chain: ChainModel + private let accountInfoSubscriptionAdapter: AccountInfoSubscriptionAdapterProtocol + private let wallet: MetaAccountModel + + private var pricesProvider: AnySingleValueProvider<[PriceData]>? + + init( + lpOperationService: PoolsOperationService, + lpDataService: PolkaswapLiquidityPoolService, + liquidityPair: LiquidityPair, + priceLocalSubscriber: PriceLocalStorageSubscriber, + chain: ChainModel, + accountInfoSubscriptionAdapter: AccountInfoSubscriptionAdapterProtocol, + wallet: MetaAccountModel + ) { + self.lpOperationService = lpOperationService + self.lpDataService = lpDataService + self.liquidityPair = liquidityPair + self.priceLocalSubscriber = priceLocalSubscriber + self.chain = chain + self.accountInfoSubscriptionAdapter = accountInfoSubscriptionAdapter + self.wallet = wallet + } + + private func subscribeToPrices() { + let chainAssets = chain.chainAssets + pricesProvider = priceLocalSubscriber.subscribeToPrices(for: chainAssets, listener: self) + + guard chainAssets.isNotEmpty else { + output?.didReceivePricesData(result: .success([])) + return + } + pricesProvider = priceLocalSubscriber.subscribeToPrices(for: chainAssets, listener: self) + } + + private func subscribeToAccountInfo() { + let chainAssets = chain.chainAssets + accountInfoSubscriptionAdapter.subscribe( + chainsAssets: chainAssets, + handler: self, + deliveryOn: .main + ) + } + + private func fetchDexId() { + Task { + do { + let dexId = try await lpDataService.fetchDexId(baseAssetId: liquidityPair.baseAssetId) + output?.didReceiveDexId(dexId) + } catch { + output?.didReceiveDexIdError(error) + } + } + } + + private func fetchReserves() { + Task { + do { + let assetIdPair = AssetIdPair(baseAssetIdCode: liquidityPair.baseAssetId, targetAssetIdCode: liquidityPair.targetAssetId) + let reservesStream = try await lpDataService.subscribePoolReserves(assetIdPair: assetIdPair) + + for try await reserves in reservesStream { + await MainActor.run { + output?.didReceivePoolReserves(reserves: reserves.value) + } + } + } catch { + await MainActor.run { + output?.didReceivePoolReservesError(error: error) + } + } + } + } + + private func fetchUserPool() { + guard let accountId = wallet.fetch(for: chain.accountRequest())?.accountId else { + output?.didReceiveUserPoolError(error: ChainAccountFetchingError.accountNotExists) + return + } + + Task { + do { + let assetIdPair = AssetIdPair(baseAssetIdCode: liquidityPair.baseAssetId, targetAssetIdCode: liquidityPair.targetAssetId) + let accountPool = try await lpDataService.fetchUserPool(assetIdPair: assetIdPair, accountId: accountId) + await MainActor.run { + output?.didReceiveUserPool(pool: accountPool) + } + } catch { + await MainActor.run { + output?.didReceiveUserPoolError(error: error) + } + } + } + } + + private func fetchTotalIssuance() { + guard let reservesIdString = liquidityPair.reservesId else { + return + } + + let reservesId = Data(hex: reservesIdString) + + Task { + do { + let totalIssuance = try await lpDataService.fetchTotalIssuance(reservesId: reservesId) + output?.didReceiveTotalIssuance(totalIssuance: totalIssuance) + } catch { + output?.didReceiveTotalIssuanceError(error: error) + } + } + } +} + +// MARK: - LiquidityPoolRemoveLiquidityInteractorInput + +extension LiquidityPoolRemoveLiquidityInteractor: LiquidityPoolRemoveLiquidityInteractorInput { + func setup(with output: LiquidityPoolRemoveLiquidityInteractorOutput) { + self.output = output + fetchDexId() + fetchReserves() + fetchUserPool() + fetchTotalIssuance() + subscribeToPrices() + subscribeToAccountInfo() + } + + func estimateFee(removeLiquidityInfo: RemoveLiquidityInfo) { + Task { + do { + let fee = try await lpOperationService.estimateFee(liquidityOperation: .substrateRemoveLiquidity(removeLiquidityInfo)) + output?.didReceiveFee(fee) + } catch { + output?.didReceiveFeeError(error) + } + } + } + + func submit(removeLiquidityInfo: RemoveLiquidityInfo) { + Task { + do { + let hash = try await lpOperationService.submit(liquidityOperation: .substrateRemoveLiquidity(removeLiquidityInfo)) + await MainActor.run { + output?.didReceiveTransactionHash(hash) + } + } catch { + await MainActor.run { + output?.didReceiveSubmitError(error: error) + } + } + } + } +} + +// MARK: - PriceLocalStorageSubscriber + +extension LiquidityPoolRemoveLiquidityInteractor: PriceLocalSubscriptionHandler { + func handlePrices(result: Result<[PriceData], Error>) { + output?.didReceivePricesData(result: result) + } +} + +// MARK: - AccountInfoSubscriptionAdapterHandler + +extension LiquidityPoolRemoveLiquidityInteractor: AccountInfoSubscriptionAdapterHandler { + func handleAccountInfo( + result: Result, + accountId _: AccountId, + chainAsset: ChainAsset + ) { + output?.didReceiveAccountInfo(result: result, for: chainAsset) + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityPresenter.swift new file mode 100644 index 0000000000..9848ff3a39 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityPresenter.swift @@ -0,0 +1,645 @@ +import Foundation +import SoraFoundation +import SSFPools +import BigInt +import SSFModels +import SSFPolkaswap + +protocol LiquidityPoolRemoveLiquidityConfirmViewInput: ControllerBackedProtocol { + func didReceiveNetworkFee(fee: BalanceViewModelProtocol?) + func setButtonLoadingState(isLoading: Bool) + func didUpdating() + func didReceiveConfirmViewModel(_ viewModel: LiquidityPoolSupplyConfirmViewModel?) +} + +protocol LiquidityPoolRemoveLiquidityViewInput: ControllerBackedProtocol { + func didReceiveXorBalanceViewModel(balanceViewModel: BalanceViewModelProtocol?) + func didReceiveSwapFrom(viewModel: AssetBalanceViewModelProtocol?) + func didReceiveSwapTo(viewModel: AssetBalanceViewModelProtocol?) + func didReceiveSwapFrom(amountInputViewModel: IAmountInputViewModel?) + func didReceiveSwapTo(amountInputViewModel: IAmountInputViewModel?) + func didReceiveSwapQuoteReady() + func didReceiveNetworkFee(fee: BalanceViewModelProtocol?) + func setButtonLoadingState(isLoading: Bool) + func didUpdating() +} + +protocol LiquidityPoolRemoveLiquidityInteractorInput: AnyObject { + func setup(with output: LiquidityPoolRemoveLiquidityInteractorOutput) + func estimateFee(removeLiquidityInfo: RemoveLiquidityInfo) + func submit(removeLiquidityInfo: RemoveLiquidityInfo) +} + +final class LiquidityPoolRemoveLiquidityPresenter { + // MARK: Private properties + + private weak var confirmView: LiquidityPoolRemoveLiquidityConfirmViewInput? + private weak var setupView: LiquidityPoolRemoveLiquidityViewInput? + + private let router: LiquidityPoolRemoveLiquidityRouterInput + private let interactor: LiquidityPoolRemoveLiquidityInteractorInput + private let logger: LoggerProtocol + private let chain: ChainModel + private let wallet: MetaAccountModel + private let liquidityPair: LiquidityPair + private let dataValidatingFactory: SendDataValidatingFactory + private let confirmViewModelFactory: LiquidityPoolSupplyConfirmViewModelFactory? + + private var removeInfo: RemoveLiquidityInfo? + private var reserves: PolkaswapPoolReservesInfo? + private var swapFromChainAsset: ChainAsset? + private var swapToChainAsset: ChainAsset? + private var prices: [PriceData]? + private var baseAssetInputResult: AmountInputResult? + private var baseAssetBalance: Decimal? + private var targetAssetInputResult: AmountInputResult? + private var targetAssetBalance: Decimal? + private var accountPoolInfo: AccountPool? + private var totalIssuance: BigUInt? + + private var networkFeeViewModel: BalanceViewModelProtocol? + private var networkFee: Decimal? + private var xorBalance: Decimal? + private var xorBalanceMinusFee: Decimal { + (xorBalance ?? 0) - (networkFee ?? 0) + } + + private var baseTargetRate: Decimal? + + private var dexId: String? + + private var baseAssetResultAmount: Decimal { + guard let baseAssetInputResult else { + return .zero + } + + return baseAssetInputResult.absoluteValue(from: baseAssetBalance.or(.zero)) + } + + private var targetAssetResultAmount: Decimal { + guard let targetAssetInputResult else { + return .zero + } + + return targetAssetInputResult.absoluteValue(from: targetAssetBalance.or(.zero)) + } + + // MARK: - Constructors + + init( + interactor: LiquidityPoolRemoveLiquidityInteractorInput, + router: LiquidityPoolRemoveLiquidityRouterInput, + localizationManager: LocalizationManagerProtocol, + wallet: MetaAccountModel, + logger: LoggerProtocol, + chain: ChainModel, + liquidityPair: LiquidityPair, + dataValidatingFactory: SendDataValidatingFactory, + confirmViewModelFactory: LiquidityPoolSupplyConfirmViewModelFactory?, + removeInfo: RemoveLiquidityInfo? + ) { + self.interactor = interactor + self.router = router + self.wallet = wallet + self.logger = logger + self.chain = chain + self.liquidityPair = liquidityPair + self.dataValidatingFactory = dataValidatingFactory + self.confirmViewModelFactory = confirmViewModelFactory + self.removeInfo = removeInfo + + self.localizationManager = localizationManager + } + + // MARK: - Private methods + + private func buildCallParameters() -> RemoveLiquidityInfo? { + guard + let dexId, + let baseAsset = chain.assets.first(where: { $0.currencyId == liquidityPair.baseAssetId }), + let targetAsset = chain.assets.first(where: { $0.currencyId == liquidityPair.targetAssetId }), + let totalIssuance = totalIssuance, + let reserves = reserves, + let baseAssetReserves = Decimal.fromSubstrateAmount(reserves.reserves.reserves, precision: Int16(baseAsset.precision)), + let totalIssuanceDecimal = Decimal.fromSubstrateAmount(totalIssuance, precision: Int16(baseAsset.precision)) + else { + return nil + } + + let baseAssetInfo = PooledAssetInfo(id: liquidityPair.baseAssetId, precision: Int16(baseAsset.precision)) + let targetAssetInfo = PooledAssetInfo(id: liquidityPair.targetAssetId, precision: Int16(targetAsset.precision)) + + let baseAssetAmount = baseAssetInputResult?.absoluteValue(from: baseAssetBalance ?? .zero) ?? .zero + let targetAssetAmount = targetAssetInputResult?.absoluteValue(from: targetAssetBalance ?? .zero) ?? .zero + + let info = RemoveLiquidityInfo( + dexId: dexId, + baseAsset: baseAssetInfo, + targetAsset: targetAssetInfo, + baseAssetAmount: baseAssetAmount, + targetAssetAmount: targetAssetAmount, + baseAssetReserves: baseAssetReserves, + totalIssuances: totalIssuanceDecimal, + slippage: 0.5 + ) + + return info + } + + private func refreshFee() { + guard let info = buildCallParameters() else { + return + } + + interactor.estimateFee(removeLiquidityInfo: info) + } + + private func runLoadingState() { + DispatchQueue.main.async { [weak self] in + self?.setupView?.setButtonLoadingState(isLoading: true) + self?.confirmView?.setButtonLoadingState(isLoading: true) + } + } + + private func checkLoadingState() { + DispatchQueue.main.async { [weak self] in + self?.setupView?.setButtonLoadingState(isLoading: false) + self?.confirmView?.setButtonLoadingState(isLoading: false) + } + } + + private func provideFeeViewModel() { + guard let swapFromFee = networkFee, let xorChainAsset = chain.utilityChainAssets().first else { + return + } + let balanceViewModelFactory = createBalanceViewModelFactory(for: xorChainAsset) + let feeViewModel = balanceViewModelFactory.balanceFromPrice( + swapFromFee, + priceData: prices?.first(where: { price in + price.priceId == xorChainAsset.asset.priceId + }), + isApproximately: true, + usageCase: .detailsCrypto + ).value(for: selectedLocale) + + DispatchQueue.main.async { + self.setupView?.didReceiveNetworkFee(fee: feeViewModel) + self.confirmView?.didReceiveNetworkFee(fee: feeViewModel) + } + + networkFeeViewModel = feeViewModel + + checkLoadingState() + } + + private func createBalanceViewModelFactory(for chainAsset: ChainAsset) -> BalanceViewModelFactory { + let assetInfo = chainAsset.asset.displayInfo(with: chainAsset.chain.icon) + let balanceViewModelFactory = BalanceViewModelFactory( + targetAssetInfo: assetInfo, + selectedMetaAccount: wallet + ) + return balanceViewModelFactory + } + + private func provideFromAssetVewModel(updateAmountInput: Bool = true) { + let balanceViewModelFactory = buildBalanceSwapToViewModelFactory( + wallet: wallet, + for: swapFromChainAsset + ) + + let swapFromPrice = prices?.first(where: { priceData in + swapFromChainAsset?.asset.priceId == priceData.priceId + }) + + let viewModel = balanceViewModelFactory?.createAssetBalanceViewModel( + baseAssetResultAmount, + balance: accountPoolInfo?.baseAssetPooled, + priceData: swapFromPrice + ).value(for: selectedLocale) + + let inputViewModel = balanceViewModelFactory? + .createBalanceInputViewModel(baseAssetResultAmount) + .value(for: selectedLocale) + + DispatchQueue.main.async { [weak self] in + self?.setupView?.didReceiveSwapFrom(viewModel: viewModel) + + if updateAmountInput { + self?.setupView?.didReceiveSwapFrom(amountInputViewModel: inputViewModel) + } + } + + checkLoadingState() + } + + private func provideToAssetVewModel(updateAmountInput: Bool = true) { + let balanceViewModelFactory = buildBalanceSwapToViewModelFactory( + wallet: wallet, + for: swapToChainAsset + ) + + let swapToPrice = prices?.first(where: { priceData in + swapToChainAsset?.asset.priceId == priceData.priceId + }) + + let viewModel = balanceViewModelFactory?.createAssetBalanceViewModel( + targetAssetResultAmount, + balance: accountPoolInfo?.targetAssetPooled, + priceData: swapToPrice + ).value(for: selectedLocale) + + let inputViewModel = balanceViewModelFactory? + .createBalanceInputViewModel(targetAssetResultAmount) + .value(for: selectedLocale) + + DispatchQueue.main.async { [weak self] in + self?.setupView?.didReceiveSwapTo(viewModel: viewModel) + + if updateAmountInput { + self?.setupView?.didReceiveSwapTo(amountInputViewModel: inputViewModel) + } + } + + checkLoadingState() + } + + private func buildBalanceSwapToViewModelFactory( + wallet: MetaAccountModel, + for chainAsset: ChainAsset? + ) -> BalanceViewModelFactoryProtocol? { + guard let chainAsset = chainAsset else { + return nil + } + let assetInfo = chainAsset.asset + .displayInfo(with: chainAsset.chain.icon) + let balanceViewModelFactory = BalanceViewModelFactory( + targetAssetInfo: assetInfo, + selectedMetaAccount: wallet + ) + return balanceViewModelFactory + } + + private func provideConfirmViewModel() { + guard let removeInfo else { + return + } + let viewModel = confirmViewModelFactory?.buildViewModel( + baseAssetAmount: removeInfo.baseAssetAmount, + targetAssetAmount: removeInfo.targetAssetAmount, + liquidityPair: liquidityPair, + chain: chain, + locale: selectedLocale + ) + + DispatchQueue.main.async { [weak self] in + self?.confirmView?.didReceiveConfirmViewModel(viewModel) + } + } + + private func provideXorBalanceViewModel() { + guard let xorBalance else { + DispatchQueue.main.async { [weak self] in + self?.setupView?.didReceiveXorBalanceViewModel(balanceViewModel: nil) + } + return + } + + let balanceViewModelFactory = buildBalanceSwapToViewModelFactory( + wallet: wallet, + for: chain.utilityChainAssets().first + ) + + let xorPrice = prices?.first(where: { priceData in + chain.utilityAssets().first?.priceId == priceData.priceId + }) + + let viewModel = balanceViewModelFactory?.balanceFromPrice( + xorBalance, + priceData: xorPrice, + usageCase: .detailsCrypto + ).value(for: selectedLocale) + + DispatchQueue.main.async { [weak self] in + self?.setupView?.didReceiveXorBalanceViewModel(balanceViewModel: viewModel) + } + } +} + +// MARK: - LiquidityPoolRemoveLiquidityConfirmViewOutput + +extension LiquidityPoolRemoveLiquidityPresenter: LiquidityPoolRemoveLiquidityConfirmViewOutput { + func didLoad(view: LiquidityPoolRemoveLiquidityConfirmViewInput) { + confirmView = view + interactor.setup(with: self) + provideConfirmViewModel() + } + + func didTapConfirmButton() { + guard let removeInfo else { + return + } + + interactor.submit(removeLiquidityInfo: removeInfo) + } +} + +// MARK: - LiquidityPoolRemoveLiquidityViewOutput + +extension LiquidityPoolRemoveLiquidityPresenter: LiquidityPoolRemoveLiquidityViewOutput { + func handleViewAppeared() { + swapFromChainAsset = chain.chainAssets.first(where: { $0.asset.currencyId == liquidityPair.baseAssetId }) + swapToChainAsset = chain.chainAssets.first(where: { $0.asset.currencyId == liquidityPair.targetAssetId }) + + DispatchQueue.main.async { + self.setupView?.didReceiveNetworkFee(fee: nil) + self.confirmView?.didReceiveNetworkFee(fee: nil) + } + + provideToAssetVewModel() + provideFromAssetVewModel() + refreshFee() + } + + func didLoad(view: LiquidityPoolRemoveLiquidityViewInput) { + setupView = view + interactor.setup(with: self) + } + + func didTapBackButton() { + if let setupView { + router.dismiss(view: setupView) + } else if let confirmView { + router.dismiss(view: confirmView) + } + } + + func didTapApyInfo() { + var infoText: String + var infoTitle: String + infoTitle = "Strategic Bonus APY" + infoText = "Farming reward for liquidity provision" + router.presentInfo( + message: infoText, + title: infoTitle, + from: setupView + ) + } + + func didTapPreviewButton() { + guard let info = buildCallParameters() else { + return + } + + let baseAssetFee = swapFromChainAsset?.isUtility == true ? networkFee : .zero + let targetAssetFee = swapToChainAsset?.isUtility == true ? networkFee : .zero + + let validators = [ + dataValidatingFactory.has( + fee: networkFee, + locale: selectedLocale, + onError: { [weak self] in + self?.refreshFee() + } + ), + dataValidatingFactory.canPayFeeAndAmount( + balanceType: .utility(balance: baseAssetBalance), + feeAndTip: baseAssetFee, + sendAmount: .zero, + locale: selectedLocale + ), + dataValidatingFactory.canPayFeeAndAmount( + balanceType: .utility(balance: targetAssetBalance), + feeAndTip: targetAssetFee, + sendAmount: .zero, + locale: selectedLocale + ), + dataValidatingFactory.canPayFeeAndAmount( + balanceType: .utility(balance: accountPoolInfo?.baseAssetPooled), + feeAndTip: .zero, + sendAmount: baseAssetResultAmount, + locale: selectedLocale + ), + dataValidatingFactory.canPayFeeAndAmount( + balanceType: .utility(balance: accountPoolInfo?.targetAssetPooled), + feeAndTip: .zero, + sendAmount: targetAssetResultAmount, + locale: selectedLocale + ) + ] + + DataValidationRunner(validators: validators).runValidation { [weak self] in + guard let self else { + return + } + + self.router.showConfirmation( + chain: self.chain, + wallet: self.wallet, + liquidityPair: self.liquidityPair, + info: info, + from: self.setupView + ) + } + } + + func selectFromAmountPercentage(_ percentage: Float) { + runLoadingState() + + baseAssetInputResult = .rate(Decimal(Double(percentage))) + + let baseAssetAbsolulteValue = baseAssetInputResult?.absoluteValue(from: baseAssetBalance.or(.zero)) + let targetAssetAbsoluteValue = baseAssetAbsolulteValue.or(.zero) * baseTargetRate.or(.zero) + targetAssetInputResult = .absolute(targetAssetAbsoluteValue) + + provideFromAssetVewModel() + provideToAssetVewModel() + + refreshFee() + } + + func updateFromAmount(_ newValue: Decimal) { + runLoadingState() + + baseAssetInputResult = .absolute(newValue) + + let baseAssetAbsolulteValue = baseAssetInputResult?.absoluteValue(from: baseAssetBalance.or(.zero)) + let targetAssetAbsoluteValue = baseAssetAbsolulteValue.or(.zero) * baseTargetRate.or(.zero) + targetAssetInputResult = .absolute(targetAssetAbsoluteValue) + + provideFromAssetVewModel(updateAmountInput: false) + provideToAssetVewModel() + + refreshFee() + } + + func selectToAmountPercentage(_ percentage: Float) { + runLoadingState() + + targetAssetInputResult = .rate(Decimal(Double(percentage))) + + let targetAssetAbsoluteValue = targetAssetInputResult?.absoluteValue(from: targetAssetBalance.or(.zero)) + let baseAssetAbsolulteValue = targetAssetAbsoluteValue.or(.zero) / baseTargetRate.or(1) + baseAssetInputResult = .absolute(baseAssetAbsolulteValue) + + provideFromAssetVewModel() + provideToAssetVewModel() + + refreshFee() + } + + func updateToAmount(_ newValue: Decimal) { + runLoadingState() + + targetAssetInputResult = .absolute(newValue) + + let targetAssetAbsoluteValue = targetAssetInputResult?.absoluteValue(from: targetAssetBalance.or(.zero)) + let baseAssetAbsolulteValue = targetAssetAbsoluteValue.or(.zero) / baseTargetRate.or(1) + baseAssetInputResult = .absolute(baseAssetAbsolulteValue) + + provideFromAssetVewModel() + provideToAssetVewModel(updateAmountInput: false) + + refreshFee() + } +} + +// MARK: - LiquidityPoolRemoveLiquidityInteractorOutput + +extension LiquidityPoolRemoveLiquidityPresenter: LiquidityPoolRemoveLiquidityInteractorOutput { + func didReceiveTransactionHash(_ hash: String) { + guard let utilityChainAsset = chain.utilityChainAssets().first else { + return + } + + router.complete(on: confirmView, title: hash, chainAsset: utilityChainAsset) + } + + func didReceiveSubmitError(error: Error) { + router.present(error: error, from: setupView, locale: selectedLocale) + } + + func didReceiveTotalIssuance(totalIssuance: BigUInt?) { + self.totalIssuance = totalIssuance + refreshFee() + } + + func didReceiveTotalIssuanceError(error: Error) { + logger.customError(error) + } + + func didReceiveDexId(_ dexId: String) { + self.dexId = dexId + refreshFee() + } + + func didReceiveDexIdError(_ error: Error) { + logger.customError(error) + } + + func didReceiveUserPool(pool: AccountPool?) { + accountPoolInfo = pool + + provideToAssetVewModel() + provideFromAssetVewModel() + } + + func didReceiveFee(_ fee: BigUInt) { + guard let utilityAsset = chain.utilityAssets().first else { + return + } + + networkFee = Decimal.fromSubstrateAmount(fee, precision: Int16(utilityAsset.precision)) + provideFeeViewModel() + } + + func didReceiveFeeError(_ error: Error) { + logger.customError(error) + } + + func didReceivePricesData(result: Result<[PriceData], Error>) { + switch result { + case let .success(priceData): + prices = priceData + + provideXorBalanceViewModel() + + let baseAssetPrice = prices?.first(where: { $0.priceId == swapFromChainAsset?.asset.priceId }) + let targetAssetPrice = prices?.first(where: { $0.priceId == swapToChainAsset?.asset.priceId }) + + if + let baseAssetPrice = baseAssetPrice, + let targetAssetPrice = targetAssetPrice, + let baseAssetPriceValue = Decimal(string: baseAssetPrice.price), + let targetAssetPriceValue = Decimal(string: targetAssetPrice.price) { + baseTargetRate = baseAssetPriceValue / targetAssetPriceValue + + DispatchQueue.main.async { [weak self] in + self?.setupView?.didReceiveSwapQuoteReady() + } + } + case let .failure(error): + prices = [] + logger.error("\(error)") + } + + provideFromAssetVewModel() + provideToAssetVewModel() + } + + func didReceiveAccountInfo(result: Result, for chainAsset: ChainAsset) { + switch result { + case let .success(accountInfo): + if swapFromChainAsset == chainAsset { + baseAssetBalance = accountInfo.map { + Decimal.fromSubstrateAmount( + $0.data.sendAvailable, + precision: Int16(chainAsset.asset.precision) + ) + } ?? .zero + provideFromAssetVewModel() + } + if swapToChainAsset == chainAsset { + targetAssetBalance = accountInfo.map { + Decimal.fromSubstrateAmount( + $0.data.sendAvailable, + precision: Int16(chainAsset.asset.precision) + ) + } ?? .zero + provideToAssetVewModel() + } + if chain.utilityChainAssets().first == chainAsset { + xorBalance = accountInfo.map { + Decimal.fromSubstrateAmount( + $0.data.sendAvailable, + precision: Int16(chainAsset.asset.precision) + ) + } ?? .zero + + provideXorBalanceViewModel() + } + case let .failure(error): + router.present(error: error, from: setupView, locale: selectedLocale) + } + } + + func didReceivePoolReserves(reserves: PolkaswapPoolReservesInfo?) { + self.reserves = reserves + refreshFee() + } + + func didReceiveUserPoolError(error: Error) { + logger.customError(error) + } + + func didReceivePoolReservesError(error: Error) { + logger.customError(error) + } +} + +// MARK: - Localizable + +extension LiquidityPoolRemoveLiquidityPresenter: Localizable { + func applyLocalization() {} +} + +extension LiquidityPoolRemoveLiquidityPresenter: LiquidityPoolRemoveLiquidityModuleInput {} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityProtocols.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityProtocols.swift new file mode 100644 index 0000000000..7d19f31839 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityProtocols.swift @@ -0,0 +1,27 @@ +import SSFModels +import SSFPools + +typealias LiquidityPoolRemoveLiquidityModuleCreationResult = ( + view: LiquidityPoolRemoveLiquidityViewInput, + input: LiquidityPoolRemoveLiquidityModuleInput +) + +protocol LiquidityPoolRemoveLiquidityRouterInput: AnyObject, ErrorPresentable, SheetAlertPresentable, AnyDismissable, AllDonePresentable { + func showConfirmation( + chain: ChainModel, + wallet: MetaAccountModel, + liquidityPair: LiquidityPair, + info: RemoveLiquidityInfo, + from view: ControllerBackedProtocol? + ) + + func complete( + on view: ControllerBackedProtocol?, + title: String, + chainAsset: ChainAsset + ) +} + +protocol LiquidityPoolRemoveLiquidityModuleInput: AnyObject {} + +protocol LiquidityPoolRemoveLiquidityModuleOutput: AnyObject {} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityRouter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityRouter.swift new file mode 100644 index 0000000000..8884f6a7f0 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityRouter.swift @@ -0,0 +1,43 @@ +import Foundation +import SoraUI +import SSFModels +import SSFPools + +final class LiquidityPoolRemoveLiquidityRouter: LiquidityPoolRemoveLiquidityRouterInput { + func showConfirmation( + chain: ChainModel, + wallet: MetaAccountModel, + liquidityPair: LiquidityPair, + info: RemoveLiquidityInfo, + from view: ControllerBackedProtocol? + ) { + guard let module = LiquidityPoolRemoveLiquidityConfirmAssembly.configureModule(wallet: wallet, chain: chain, liquidityPair: liquidityPair, removeInfo: info) else { + return + } + + view?.controller.navigationController?.pushViewController(module.view.controller, animated: true) + } + + func complete( + on view: ControllerBackedProtocol?, + title: String, + chainAsset: ChainAsset + ) { + let presenter = view?.controller.navigationController?.presentingViewController + + let controller = AllDoneAssembly.configureModule(chainAsset: chainAsset, hashString: title)?.view.controller + controller?.modalPresentationStyle = .custom + + let factory = ModalSheetBlurPresentationFactory( + configuration: ModalSheetPresentationConfiguration.fearlessBlur + ) + controller?.modalTransitioningFactory = factory + + view?.controller.navigationController?.dismiss(animated: true) { + if let presenter = presenter as? ControllerBackedProtocol, + let controller = controller { + presenter.controller.present(controller, animated: true) + } + } + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityViewController.swift new file mode 100644 index 0000000000..066ac90f73 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityViewController.swift @@ -0,0 +1,276 @@ +import UIKit +import SoraFoundation +import SnapKit + +protocol LiquidityPoolRemoveLiquidityViewOutput: AnyObject { + func didLoad(view: LiquidityPoolRemoveLiquidityViewInput) + func handleViewAppeared() + func didTapBackButton() + func didTapPreviewButton() + func selectFromAmountPercentage(_ percentage: Float) + func updateFromAmount(_ newValue: Decimal) + func selectToAmountPercentage(_ percentage: Float) + func updateToAmount(_ newValue: Decimal) +} + +final class LiquidityPoolRemoveLiquidityViewController: UIViewController, ViewHolder, HiddableBarWhenPushed { + typealias RootViewType = LiquidityPoolRemoveLiquidityViewLayout + var keyboardHandler: FearlessKeyboardHandler? + + private enum Constants { + static let delay: CGFloat = 0.7 + } + + // MARK: Private properties + + private let output: LiquidityPoolRemoveLiquidityViewOutput + + private var amountFromInputViewModel: IAmountInputViewModel? + private var amountToInputViewModel: IAmountInputViewModel? + + private var assetFromViewModel: AssetBalanceViewModelProtocol? + private var assetToViewModel: AssetBalanceViewModelProtocol? + + // MARK: - Constructor + + init( + output: LiquidityPoolRemoveLiquidityViewOutput, + localizationManager: LocalizationManagerProtocol? + ) { + self.output = output + super.init(nibName: nil, bundle: nil) + self.localizationManager = localizationManager + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Life cycle + + override func loadView() { + view = LiquidityPoolRemoveLiquidityViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + output.didLoad(view: self) + setupActions() + configure() + addEndEditingTapGesture(for: rootView.contentView) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if isBeingPresented || isMovingToParent { + output.handleViewAppeared() + } + } + + // MARK: - Private methods + + private func configure() { + navigationController?.setNavigationBarHidden(true, animated: true) + + rootView.swapToInputView.textField.delegate = self + rootView.swapFromInputView.textField.delegate = self + + rootView.swapFromInputView.textField.isUserInteractionEnabled = false + rootView.swapToInputView.textField.isUserInteractionEnabled = false + + let locale = localizationManager?.selectedLocale ?? Locale.current + let accessoryView = UIFactory.default.createAmountAccessoryView(for: self, locale: locale) + rootView.swapFromInputView.textField.inputAccessoryView = accessoryView + updatePreviewButton() + } + + private func updatePreviewButton() { + let isEnabled = amountFromInputViewModel?.isValid == true && amountToInputViewModel?.isValid == true && assetToViewModel != nil && assetFromViewModel != nil + rootView.previewButton.set(enabled: isEnabled) + } + + private func setupActions() { + rootView.backButton.addTarget( + self, + action: #selector(handleTapBackButton), + for: .touchUpInside + ) + + rootView.previewButton.addTarget( + self, + action: #selector(handleTapPreviewButton), + for: .touchUpInside + ) + } + + // MARK: - Private actions + + @objc private func handleTapBackButton() { + output.didTapBackButton() + } + + @objc private func handleTapPreviewButton() { + output.didTapPreviewButton() + } +} + +// MARK: - LiquidityPoolRemoveLiquidityViewInput + +extension LiquidityPoolRemoveLiquidityViewController: LiquidityPoolRemoveLiquidityViewInput { + func didReceiveXorBalanceViewModel(balanceViewModel: BalanceViewModelProtocol?) { + rootView.bindXorBalanceViewModel(balanceViewModel) + } + + func didReceiveSwapQuoteReady() { + print("Swap quotes ready") + rootView.swapFromInputView.textField.isUserInteractionEnabled = true + rootView.swapToInputView.textField.isUserInteractionEnabled = true + } + + func didReceiveSwapFrom(viewModel: AssetBalanceViewModelProtocol?) { + assetFromViewModel = viewModel + rootView.bindSwapFrom(assetViewModel: viewModel) + updatePreviewButton() + } + + func didReceiveSwapTo(viewModel: AssetBalanceViewModelProtocol?) { + assetToViewModel = viewModel + rootView.bindSwapTo(assetViewModel: viewModel) + updatePreviewButton() + } + + func didReceiveSwapFrom(amountInputViewModel: IAmountInputViewModel?) { + amountFromInputViewModel = amountInputViewModel + amountInputViewModel?.observable.remove(observer: self) + amountInputViewModel?.observable.add(observer: self) + rootView.swapFromInputView.inputFieldText = amountInputViewModel?.displayAmount + updatePreviewButton() + } + + func didReceiveSwapTo(amountInputViewModel: IAmountInputViewModel?) { + amountToInputViewModel = amountInputViewModel + amountInputViewModel?.observable.remove(observer: self) + amountInputViewModel?.observable.add(observer: self) + rootView.swapToInputView.inputFieldText = amountInputViewModel?.displayAmount + updatePreviewButton() + } + + func didReceiveNetworkFee(fee: BalanceViewModelProtocol?) { + rootView.bind(fee: fee) + updatePreviewButton() + } + + func setButtonLoadingState(isLoading: Bool) { + rootView.previewButton.set(loading: isLoading) + } + + func didUpdating() { + DispatchQueue.main.async { + self.rootView.previewButton.set(enabled: false) + } + } +} + +// MARK: - Localizable + +extension LiquidityPoolRemoveLiquidityViewController: Localizable { + func applyLocalization() { + rootView.locale = selectedLocale + } +} + +// MARK: - AmountInputAccessoryViewDelegate + +extension LiquidityPoolRemoveLiquidityViewController: AmountInputAccessoryViewDelegate { + func didSelect(on _: AmountInputAccessoryView, percentage: Float) { + if rootView.swapFromInputView.textField.isFirstResponder { + output.selectFromAmountPercentage(percentage) + } else if rootView.swapToInputView.textField.isFirstResponder { + output.selectToAmountPercentage(percentage) + } + } + + func didSelectDone(on _: AmountInputAccessoryView) { + if rootView.swapFromInputView.textField.isFirstResponder { + rootView.swapFromInputView.textField.resignFirstResponder() + } else if rootView.swapToInputView.textField.isFirstResponder { + rootView.swapToInputView.textField.resignFirstResponder() + } + } +} + +// MARK: - UITextFieldDelegate + +extension LiquidityPoolRemoveLiquidityViewController: UITextFieldDelegate { + func textField( + _ textField: UITextField, + shouldChangeCharactersIn range: NSRange, + replacementString string: String + ) -> Bool { + if textField == rootView.swapFromInputView.textField { + return amountFromInputViewModel?.didReceiveReplacement(string, for: range) ?? false + } else if textField == rootView.swapToInputView.textField { + return amountToInputViewModel?.didReceiveReplacement(string, for: range) ?? false + } + return true + } + + func textFieldDidBeginEditing(_ textField: UITextField) { + if textField == rootView.swapToInputView.textField { + rootView.swapFromInputView.textField.resignFirstResponder() + } + } + + func textFieldDidEndEditing(_: UITextField) { + rootView.swapFromInputView.set(highlighted: false, animated: false) + rootView.swapToInputView.set(highlighted: false, animated: false) + } +} + +// MARK: - AmountInputViewModelObserver + +extension LiquidityPoolRemoveLiquidityViewController: AmountInputViewModelObserver { + func amountInputDidChange() { + rootView.swapFromInputView.inputFieldText = amountFromInputViewModel?.displayAmount + rootView.swapToInputView.inputFieldText = amountToInputViewModel?.displayAmount + + NSObject.cancelPreviousPerformRequests( + withTarget: self, + selector: #selector(updateAmounts), + object: nil + ) + + perform(#selector(updateAmounts), with: nil, afterDelay: Constants.delay) + } + + @objc private func updateAmounts() { + if rootView.swapFromInputView.textField.isFirstResponder { + guard let amountFrom = amountFromInputViewModel?.decimalAmount else { + output.updateFromAmount(0) + + return + } + output.updateFromAmount(amountFrom) + } + + if rootView.swapToInputView.textField.isFirstResponder { + guard let amountTo = amountToInputViewModel?.decimalAmount else { + output.updateToAmount(0) + + return + } + output.updateToAmount(amountTo) + } + } +} + +// MARK: - KeyboardViewAdoptable + +extension LiquidityPoolRemoveLiquidityViewController: KeyboardViewAdoptable { + var target: Constraint? { rootView.keyboardAdoptableConstraint } + + func offsetFromKeyboardWithInset(_: CGFloat) -> CGFloat { UIConstants.bigOffset } + func updateWhileKeyboardFrameChanging(_: CGRect) {} +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityViewLayout.swift new file mode 100644 index 0000000000..107812b166 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityViewLayout.swift @@ -0,0 +1,289 @@ +import UIKit +import SnapKit + +final class LiquidityPoolRemoveLiquidityViewLayout: UIView { + private enum Constants { + static let navigationBarHeight: CGFloat = 56.0 + static let backButtonSize = CGSize(width: 32, height: 32) + static let imageWidth: CGFloat = 12 + static let imageHeight: CGFloat = 12 + static let imageVerticalPosition: CGFloat = 2 + static let disclaimerMinHeight: CGFloat = 42.0 + } + + var keyboardAdoptableConstraint: Constraint? + + // MARK: navigation + + let navigationViewContainer = UIView() + + let titleLabel: UILabel = { + let label = UILabel() + label.font = .h3Title + label.textColor = R.color.colorWhite() + return label + }() + + let backButton: UIButton = { + let button = UIButton() + button.setImage(R.image.iconBack(), for: .normal) + button.layer.masksToBounds = true + button.backgroundColor = R.color.colorWhite8() + return button + }() + + // MARK: content + + let contentView: ScrollableContainerView = { + let view = ScrollableContainerView() + view.stackView.isLayoutMarginsRelativeArrangement = true + view.stackView.layoutMargins = UIEdgeInsets(top: UIConstants.bigOffset, left: 0.0, bottom: 0.0, right: 0.0) + view.stackView.spacing = 8 + return view + }() + + let swapFromInputView = AmountInputViewV2() + let swapToInputView = AmountInputViewV2() + let switchSwapButton: UIButton = { + let button = UIButton() + button.setImage(R.image.iconAddTokenPair(), for: .normal) + return button + }() + + let networkFeeView = UIFactory.default.createMultiView() + let balanceView = UIFactory.default.createMultiView() + + private lazy var multiViews = [ + balanceView, + networkFeeView + ] + + let previewButton: TriangularedButton = { + let button = TriangularedButton() + button.applyEnabledStyle() + return button + }() + + let warningView = WarningView() + + var locale: Locale = .current { + didSet { + applyLocalization() + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = R.color.colorBlack19() + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + backButton.rounded() + } + + // MARK: - Public methods + + func bind(fee: BalanceViewModelProtocol?) { + networkFeeView.bindBalance(viewModel: fee) + networkFeeView.isHidden = false + } + + func bindSwapFrom(assetViewModel: AssetBalanceViewModelProtocol?) { + guard let assetViewModel else { + return + } + + swapFromInputView.bind(viewModel: assetViewModel) + } + + func bindSwapTo(assetViewModel: AssetBalanceViewModelProtocol?) { + guard let assetViewModel else { + return + } + + swapToInputView.bind(viewModel: assetViewModel) + } + + func bindXorBalanceViewModel(_ viewModel: BalanceViewModelProtocol?) { + balanceView.bindBalance(viewModel: viewModel) + } + + // MARK: - Private methods + + private func setupLayout() { + addSubview(navigationViewContainer) + setupNavigationLayout(for: navigationViewContainer) + setupContentsLayout() + } + + private func setupNavigationLayout(for container: UIView) { + container.snp.makeConstraints { make in + make.top.leading.trailing.equalToSuperview() + make.height.equalTo(Constants.navigationBarHeight) + } + + container.addSubview(backButton) + container.addSubview(titleLabel) + + backButton.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.leading.equalTo(UIConstants.bigOffset) + make.size.equalTo(Constants.backButtonSize) + } + + titleLabel.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } + + private func setupContentsLayout() { + addSubview(contentView) + addSubview(previewButton) + addSubview(warningView) + + contentView.snp.makeConstraints { make in + make.top.equalTo(navigationViewContainer.snp.bottom) + make.leading.trailing.equalToSuperview() + make.bottom.equalTo(previewButton.snp.top).offset(-16) + } + + previewButton.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(16) + make.height.equalTo(UIConstants.actionHeight) + keyboardAdoptableConstraint = make.bottom.equalToSuperview().inset(16).constraint + } + + warningView.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(16) + make.bottom.equalTo(previewButton.snp.top).offset(-16) + } + + let switchInputsView = createSwitchInputsView() + contentView.stackView.addArrangedSubview(switchInputsView) + switchInputsView.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview() + } + + let feesView = createFeesView() + contentView.stackView.addArrangedSubview(feesView) + feesView.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(16) + } + } + + private func createSwitchInputsView() -> UIView { + let container = UIView() + container.addSubview(swapFromInputView) + container.addSubview(swapToInputView) + container.addSubview(switchSwapButton) + + swapFromInputView.snp.makeConstraints { make in + make.top.equalToSuperview() + make.leading.trailing.equalToSuperview().inset(UIConstants.bigOffset) + } + + swapToInputView.snp.makeConstraints { make in + make.top.equalTo(swapFromInputView.snp.bottom).offset(UIConstants.defaultOffset) + make.leading.trailing.equalToSuperview().inset(UIConstants.bigOffset) + make.bottom.equalToSuperview() + } + + switchSwapButton.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.centerY.equalTo(swapFromInputView.snp.bottom).offset(UIConstants.defaultOffset / 2) + } + + return container + } + + private func createFeesView() -> UIView { + func makeCommonConstraints(for view: UIView) { + view.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview() + } + } + + let backgroundView = UIFactory.default.createInfoBackground() + let container = UIFactory.default.createVerticalStackView() + + backgroundView.addSubview(container) + container.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(16) + make.bottom.equalToSuperview().inset(8) + make.top.equalToSuperview() + } + + multiViews.forEach { + container.addArrangedSubview($0) + makeCommonConstraints(for: $0) + $0.titleLabel.isUserInteractionEnabled = true + } + + return backgroundView + } + + private func createMultiView() -> TitleMultiValueView { + let view = TitleMultiValueView() + view.titleLabel.font = .h6Title + view.titleLabel.textColor = R.color.colorWhite50() + view.valueTop.font = .p1Paragraph + view.valueTop.textColor = R.color.colorWhite() + view.valueBottom.font = .p2Paragraph + view.valueBottom.textColor = R.color.colorStrokeGray() + view.borderView.isHidden = true + view.equalsLabelsWidth = true + view.valueTop.lineBreakMode = .byTruncatingTail + view.valueBottom.lineBreakMode = .byTruncatingMiddle + return view + } + + private func applyLocalization() { + warningView.titleLabel.text = "NOTE" + warningView.textLabel.text = "Removing pool tokens converts your position back into underlying tokens at the current rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive." + titleLabel.text = "Remove Liquidity" + + swapFromInputView.locale = locale + swapToInputView.locale = locale + + let texts = [ + R.string.localizable + .polkaswapNetworkFee(preferredLanguages: locale.rLanguages) + ] + + [ + networkFeeView.titleLabel + ].enumerated().forEach { index, label in + setInfoImage(for: label, text: texts[index]) + } + + balanceView.titleLabel.text = R.string.localizable.assetdetailsBalanceTransferable(preferredLanguages: locale.rLanguages) + previewButton.imageWithTitleView?.title = R.string.localizable + .commonPreview(preferredLanguages: locale.rLanguages) + } + + private func setInfoImage(for label: UILabel, text: String) { + let attributedString = NSMutableAttributedString(string: text) + + let imageAttachment = NSTextAttachment() + imageAttachment.image = R.image.iconInfoFilled() + imageAttachment.bounds = CGRect( + x: 0, + y: -Constants.imageVerticalPosition, + width: Constants.imageWidth, + height: Constants.imageHeight + ) + + let imageString = NSAttributedString(attachment: imageAttachment) + attributedString.append(NSAttributedString(string: " ")) + attributedString.append(imageString) + + label.attributedText = attributedString + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmAssembly.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmAssembly.swift new file mode 100644 index 0000000000..f8f884bd1b --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmAssembly.swift @@ -0,0 +1,84 @@ +import UIKit +import SoraFoundation +import SSFPolkaswap +import SSFPools +import SoraKeystore +import SSFModels + +final class LiquidityPoolRemoveLiquidityConfirmAssembly { + static func configureModule( + wallet: MetaAccountModel, + chain: ChainModel, + liquidityPair: LiquidityPair, + removeInfo: RemoveLiquidityInfo + ) -> LiquidityPoolRemoveLiquidityConfirmModuleCreationResult? { + guard let response = wallet.fetch(for: chain.accountRequest()) else { + return nil + } + + guard let secretKeyData = try? fetchSecretKey( + for: chain, + metaId: wallet.metaId, + accountResponse: response + ) else { + return nil + } + + let localizationManager = LocalizationManager.shared + let chainRegistry = ChainRegistryFacade.sharedRegistry + let lpDataService = PolkaswapLiquidityPoolServiceAssembly.buildService(for: chain, chainRegistry: chainRegistry) + let signingWrapperData = SigningWrapperData(publicKeyData: response.publicKey, secretKeyData: secretKeyData) + + guard let lpOperationService = try? PolkaswapLiquidityPoolServiceAssembly.buildOperationService( + for: chain, + wallet: wallet.utilsModel, + chainRegistry: chainRegistry, + signingWrapperData: signingWrapperData + ) else { + return nil + } + + let accountInfoSubscriptionAdapter = AccountInfoSubscriptionAdapter( + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, + selectedMetaAccount: wallet + ) + + let interactor = LiquidityPoolRemoveLiquidityInteractor(lpOperationService: lpOperationService, lpDataService: lpDataService, liquidityPair: liquidityPair, priceLocalSubscriber: PriceLocalStorageSubscriberImpl.shared, chain: chain, accountInfoSubscriptionAdapter: accountInfoSubscriptionAdapter, wallet: wallet) + let router = LiquidityPoolRemoveLiquidityRouter() + let dataValidatingFactory = SendDataValidatingFactory(presentable: router) + let presenter = LiquidityPoolRemoveLiquidityPresenter( + interactor: interactor, + router: router, + localizationManager: localizationManager, + wallet: wallet, + logger: Logger.shared, + chain: chain, + liquidityPair: liquidityPair, + dataValidatingFactory: dataValidatingFactory, + confirmViewModelFactory: LiquidityPoolSupplyConfirmViewModelFactoryDefault(), + removeInfo: removeInfo + ) + + let view = LiquidityPoolRemoveLiquidityConfirmViewController( + output: presenter, + localizationManager: localizationManager + ) + + return (view, presenter) + } + + static func fetchSecretKey( + for chain: ChainModel, + metaId: String, + accountResponse: ChainAccountResponse + ) throws -> Data { + let accountId = accountResponse.isChainAccount ? accountResponse.accountId : nil + let tag: String = chain.isEthereumBased + ? KeystoreTagV2.ethereumSecretKeyTagForMetaId(metaId, accountId: accountId) + : KeystoreTagV2.substrateSecretKeyTagForMetaId(metaId, accountId: accountId) + + let keystore = Keychain() + let secretKey = try keystore.fetchKey(for: tag) + return secretKey + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmProtocols.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmProtocols.swift new file mode 100644 index 0000000000..8287222411 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmProtocols.swift @@ -0,0 +1,4 @@ +typealias LiquidityPoolRemoveLiquidityConfirmModuleCreationResult = ( + view: LiquidityPoolRemoveLiquidityConfirmViewInput, + input: LiquidityPoolRemoveLiquidityModuleInput +) diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmViewController.swift new file mode 100644 index 0000000000..0c55f61718 --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmViewController.swift @@ -0,0 +1,109 @@ +import UIKit +import SoraFoundation + +protocol LiquidityPoolRemoveLiquidityConfirmViewOutput: AnyObject { + func didLoad(view: LiquidityPoolRemoveLiquidityConfirmViewInput) + func handleViewAppeared() + func didTapBackButton() + func didTapConfirmButton() +} + +final class LiquidityPoolRemoveLiquidityConfirmViewController: UIViewController, ViewHolder, HiddableBarWhenPushed { + typealias RootViewType = LiquidityPoolRemoveLiquidityConfirmViewLayout + + // MARK: Private properties + + private let output: LiquidityPoolRemoveLiquidityConfirmViewOutput + + // MARK: - Constructor + + init( + output: LiquidityPoolRemoveLiquidityConfirmViewOutput, + localizationManager: LocalizationManagerProtocol? + ) { + self.output = output + super.init(nibName: nil, bundle: nil) + self.localizationManager = localizationManager + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Life cycle + + override func loadView() { + view = LiquidityPoolRemoveLiquidityConfirmViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + output.didLoad(view: self) + setupActions() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if isBeingPresented || isMovingToParent { + output.handleViewAppeared() + } + } + + // MARK: - Private methods + + private func setupActions() { + rootView.backButton.addTarget( + self, + action: #selector(handleTapBackButton), + for: .touchUpInside + ) + + rootView.confirmButton.addTarget( + self, + action: #selector(handleTapConfirmButton), + for: .touchUpInside + ) + } + + // MARK: - Private actions + + @objc private func handleTapBackButton() { + output.didTapBackButton() + } + + @objc private func handleTapConfirmButton() { + output.didTapConfirmButton() + } +} + +// MARK: - LiquidityPoolSupplyConfirmViewInput + +extension LiquidityPoolRemoveLiquidityConfirmViewController: LiquidityPoolRemoveLiquidityConfirmViewInput { + func didReceiveNetworkFee(fee: BalanceViewModelProtocol?) { + rootView.bind(feeViewModel: fee) + } + + func setButtonLoadingState(isLoading: Bool) { + rootView.confirmButton.set(loading: isLoading) + } + + func didUpdating() { + DispatchQueue.main.async { + self.rootView.confirmButton.set(enabled: false) + } + } + + func didReceiveConfirmViewModel(_ viewModel: LiquidityPoolSupplyConfirmViewModel?) { + rootView.bind(confirmViewModel: viewModel) + } +} + +// MARK: - Localizable + +extension LiquidityPoolRemoveLiquidityConfirmViewController: Localizable { + func applyLocalization() { + rootView.locale = selectedLocale + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmViewLayout.swift new file mode 100644 index 0000000000..93c8bd95df --- /dev/null +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmViewLayout.swift @@ -0,0 +1,169 @@ +import UIKit + +final class LiquidityPoolRemoveLiquidityConfirmViewLayout: UIView { + private enum Constants { + static let navigationBarHeight: CGFloat = 56.0 + static let backButtonSize = CGSize(width: 32, height: 32) + } + + let navigationViewContainer = UIView() + + let backButton: UIButton = { + let button = UIButton() + button.setImage(R.image.iconBack(), for: .normal) + button.layer.masksToBounds = true + button.backgroundColor = R.color.colorWhite8() + return button + }() + + let titleLabel: UILabel = { + let label = UILabel() + label.font = .h3Title + label.textColor = R.color.colorWhite() + return label + }() + + let contentView: ScrollableContainerView = { + let view = ScrollableContainerView() + view.stackView.isLayoutMarginsRelativeArrangement = true + view.stackView.layoutMargins = UIEdgeInsets( + top: 24.0, + left: 0.0, + bottom: UIConstants.actionHeight + UIConstants.bigOffset * 2, + right: 0.0 + ) + view.stackView.spacing = UIConstants.bigOffset + return view + }() + + let doubleImageView = PolkaswapDoubleSymbolView(imageSize: CGSize(width: 64, height: 64), mode: .filled) + let amountsLabel: UILabel = { + let label = UILabel() + label.font = .h3Title + label.textColor = R.color.colorWhite() + label.numberOfLines = 2 + return label + }() + + let infoBackground = UIFactory.default.createInfoBackground() + let infoViewsStackView: UIStackView = { + let stackView = UIFactory.default.createVerticalStackView() + stackView.alignment = .center + stackView.spacing = 12 + return stackView + }() + + let networkFeeView = UIFactory.default.createConfirmationMultiView() + + private lazy var multiViews = [ + networkFeeView + ] + + let confirmButton: TriangularedButton = { + let button = TriangularedButton() + button.applyEnabledStyle() + return button + }() + + var locale: Locale = .current { + didSet { + applyLocalization() + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = R.color.colorBlack19() + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + backButton.rounded() + } + + func bind(confirmViewModel: LiquidityPoolSupplyConfirmViewModel?) { + amountsLabel.attributedText = confirmViewModel?.amountsText + + if let doubleImageViewModel = confirmViewModel?.doubleImageViewViewModel { + doubleImageView.bind(viewModel: doubleImageViewModel) + } + } + + func bind(feeViewModel: BalanceViewModelProtocol?) { + networkFeeView.bindBalance(viewModel: feeViewModel) + } + + // MARK: - Private methods + + private func applyLocalization() { + titleLabel.text = "Remove Liquidity" + networkFeeView.titleLabel.text = R.string.localizable + .commonNetworkFee(preferredLanguages: locale.rLanguages) + confirmButton.imageWithTitleView?.title = R.string.localizable + .commonConfirm(preferredLanguages: locale.rLanguages) + } + + private func setupLayout() { + func makeCommonConstraints(for view: UIView) { + view.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview() + } + } + + addSubview(navigationViewContainer) + navigationViewContainer.snp.makeConstraints { make in + make.top.leading.trailing.equalToSuperview() + make.height.equalTo(Constants.navigationBarHeight) + } + + navigationViewContainer.addSubview(backButton) + backButton.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.leading.equalTo(UIConstants.bigOffset) + make.size.equalTo(Constants.backButtonSize) + } + + navigationViewContainer.addSubview(titleLabel) + titleLabel.snp.makeConstraints { make in + make.center.equalToSuperview() + } + + addSubview(contentView) + addSubview(confirmButton) + + contentView.snp.makeConstraints { make in + make.top.equalTo(navigationViewContainer.snp.bottom) + make.leading.trailing.equalToSuperview().inset(UIConstants.bigOffset) + make.bottom.equalTo(safeAreaLayoutGuide) + } + + contentView.stackView.addArrangedSubview(infoBackground) + + infoViewsStackView.addArrangedSubview(doubleImageView) + infoViewsStackView.addArrangedSubview(amountsLabel) + + infoBackground.addSubview(infoViewsStackView) + makeCommonConstraints(for: infoBackground) + infoViewsStackView.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(UIConstants.accessoryItemsSpacing) + make.top.bottom.equalToSuperview().inset(UIConstants.defaultOffset) + } + + multiViews.forEach { view in + infoViewsStackView.addArrangedSubview(view) + makeCommonConstraints(for: view) + } + + confirmButton.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(UIConstants.bigOffset) + make.bottom.equalToSuperview().inset(UIConstants.bigOffset) + make.height.equalTo(UIConstants.actionHeight) + } + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift index def22cf5ca..392d58c60f 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift @@ -47,7 +47,6 @@ final class LiquidityPoolSupplyPresenter { private var swapFromChainAsset: ChainAsset? private var swapToChainAsset: ChainAsset? private var prices: [PriceData]? - private var swapVariant: SwapVariant = .desiredInput private var apyInfo: PoolApyInfo? private var slippadgeTolerance: Float = Constants.slippadgeTolerance @@ -353,7 +352,6 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { func selectFromAmountPercentage(_ percentage: Float) { runLoadingState() - swapVariant = .desiredInput baseAssetInputResult = .rate(Decimal(Double(percentage))) let baseAssetAbsolulteValue = baseAssetInputResult?.absoluteValue(from: baseAssetBalance.or(.zero)) @@ -369,7 +367,6 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { func updateFromAmount(_ newValue: Decimal) { runLoadingState() - swapVariant = .desiredInput baseAssetInputResult = .absolute(newValue) let baseAssetAbsolulteValue = baseAssetInputResult?.absoluteValue(from: baseAssetBalance.or(.zero)) @@ -385,7 +382,6 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { func selectToAmountPercentage(_ percentage: Float) { runLoadingState() - swapVariant = .desiredOutput targetAssetInputResult = .rate(Decimal(Double(percentage))) let targetAssetAbsoluteValue = targetAssetInputResult?.absoluteValue(from: targetAssetBalance.or(.zero)) @@ -401,7 +397,6 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { func updateToAmount(_ newValue: Decimal) { runLoadingState() - swapVariant = .desiredOutput targetAssetInputResult = .absolute(newValue) let targetAssetAbsoluteValue = targetAssetInputResult?.absoluteValue(from: targetAssetBalance.or(.zero)) diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift index d6869ab579..b19c37661c 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift @@ -132,6 +132,8 @@ final class LiquidityPoolSupplyViewController: UIViewController, ViewHolder, Hid extension LiquidityPoolSupplyViewController: LiquidityPoolSupplyViewInput { func didReceiveSwapQuoteReady() { + print("Swap quotes ready") + rootView.swapFromInputView.textField.isUserInteractionEnabled = true rootView.swapToInputView.textField.isUserInteractionEnabled = true } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift index b5e588b7aa..40796af79d 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift @@ -42,8 +42,8 @@ final class LiquidityPoolSupplyViewLayout: UIView { return view }() - let swapFromInputView = AmountInputViewV2() - let swapToInputView = AmountInputViewV2() + let swapFromInputView = AmountInputViewV2(type: .bonded) + let swapToInputView = AmountInputViewV2(type: .bonded) let switchSwapButton: UIButton = { let button = UIButton() button.setImage(R.image.iconAddTokenPair(), for: .normal) diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListPresenter.swift index ffc853ca19..8d00e54384 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListPresenter.swift @@ -89,6 +89,10 @@ extension AvailableLiquidityPoolsListPresenter: LiquidityPoolsListViewOutput { func didTapMoreButton() { moduleOutput?.didTapMoreAvailablePools() } + + func didTapBackButton() { + router.dismiss(view: view) + } } extension AvailableLiquidityPoolsListPresenter: AvailableLiquidityPoolsListInteractorOutput { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListViewModelFactory.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListViewModelFactory.swift index ef73faab05..6b830ba9a5 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListViewModelFactory.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListViewModelFactory.swift @@ -88,7 +88,7 @@ final class AvailableLiquidityPoolsListViewModelFactoryDefault: AvailableLiquidi let filteredViewModels = type == .embed ? Array(poolViewModels.or([]).prefix(10)) : poolViewModels.or([]) return LiquidityPoolListViewModel( - poolViewModels: poolViewModels, + poolViewModels: filteredViewModels, titleLabelText: "Available pools", moreButtonVisible: type == .embed && (filteredViewModels.count < pairs?.count ?? 0), backgroundVisible: type == .full, diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift index 7d6316fb8b..8f019ca573 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift @@ -89,6 +89,10 @@ extension UserLiquidityPoolsListPresenter: LiquidityPoolsListViewOutput { func didTapMoreButton() { moduleOutput?.didTapMoreUserPools() } + + func didTapBackButton() { + router.dismiss(view: view) + } } extension UserLiquidityPoolsListPresenter: UserLiquidityPoolsListInteractorOutput { @@ -98,6 +102,7 @@ extension UserLiquidityPoolsListPresenter: UserLiquidityPoolsListInteractorOutpu func didReceiveLiquidityPairs(pools: [LiquidityPair]?) { self.pools = pools + moduleOutput?.shouldShowUserPools(pools?.isNotEmpty == true) provideViewModel() } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListProtocols.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListProtocols.swift index 319313e683..62aeb22069 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListProtocols.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListProtocols.swift @@ -12,6 +12,7 @@ protocol LiquidityPoolsListViewOutput: AnyObject { func didLoad(view: LiquidityPoolsListViewInput) func didTapOn(viewModel: LiquidityPoolListCellModel) func didTapMoreButton() + func didTapBackButton() } protocol LiquidityPoolsListInteractorInput: AnyObject { @@ -19,7 +20,7 @@ protocol LiquidityPoolsListInteractorInput: AnyObject { func fetchPools() } -protocol LiquidityPoolsListRouterInput: AnyObject { +protocol LiquidityPoolsListRouterInput: AnyObject, AnyDismissable { func showPoolDetails( assetIdPair: AssetIdPair, chain: ChainModel, @@ -34,4 +35,5 @@ protocol LiquidityPoolsListModuleInput: AnyObject {} protocol LiquidityPoolsListModuleOutput: AnyObject { func didTapMoreUserPools() func didTapMoreAvailablePools() + func shouldShowUserPools(_ shouldShow: Bool) } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift index d4275f6e44..bfd1a1435a 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift @@ -1,7 +1,7 @@ import UIKit import SoraFoundation -final class LiquidityPoolsListViewController: UIViewController, ViewHolder { +final class LiquidityPoolsListViewController: UIViewController, ViewHolder, HiddableBarWhenPushed { typealias RootViewType = LiquidityPoolsListViewLayout // MARK: Private properties @@ -42,6 +42,10 @@ final class LiquidityPoolsListViewController: UIViewController, ViewHolder { rootView.moreButton.addAction { [weak self] in self?.output.didTapMoreButton() } + + rootView.backButton.addAction { [weak self] in + self?.output.didTapBackButton() + } } // MARK: - Private methods diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift index 8aac65dd2b..8e412ce8ad 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift @@ -25,6 +25,15 @@ final class LiquidityPoolsListViewLayout: UIView { return button }() + let backButton: UIButton = { + let button = UIButton() + button.setImage(R.image.iconClose(), for: .normal) + button.layer.masksToBounds = true + button.backgroundColor = R.color.colorWhite8() + button.isHidden = true + return button + }() + let tableView: SelfSizingTableView = { let view = SelfSizingTableView(frame: .zero) view.backgroundColor = .clear @@ -60,6 +69,7 @@ final class LiquidityPoolsListViewLayout: UIView { func bind(viewModel: LiquidityPoolListViewModel) { titleLabel.text = viewModel.titleLabelText moreButton.isHidden = !viewModel.moreButtonVisible + backButton.isHidden = !viewModel.backgroundVisible backgroundImageView.isHidden = !viewModel.backgroundVisible tableView.refreshControl = viewModel.refreshAvailable ? UIRefreshControl() : nil @@ -69,6 +79,7 @@ final class LiquidityPoolsListViewLayout: UIView { override func layoutSubviews() { super.layoutSubviews() moreButton.rounded() + backButton.rounded() } private func drawSubviews() { @@ -78,6 +89,7 @@ final class LiquidityPoolsListViewLayout: UIView { topBar.addSubview(titleLabel) topBar.addSubview(moreButton) + topBar.addSubview(backButton) } private func setupConstraints() { @@ -107,6 +119,12 @@ final class LiquidityPoolsListViewLayout: UIView { make.trailing.equalToSuperview().inset(12) make.centerY.equalToSuperview() } + + backButton.snp.makeConstraints { make in + make.size.equalTo(32) + make.trailing.equalToSuperview().inset(16) + make.centerY.equalToSuperview() + } } private func applyLocalization() { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewPresenter.swift index 2fcc301bb9..7b643645d2 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewPresenter.swift @@ -38,6 +38,10 @@ extension LiquidityPoolsOverviewPresenter: LiquidityPoolsOverviewViewOutput { self.view = view interactor.setup(with: self) } + + func backButtonClicked() { + router.dismiss(view: view) + } } // MARK: - LiquidityPoolsOverviewInteractorOutput @@ -60,4 +64,8 @@ extension LiquidityPoolsOverviewPresenter: LiquidityPoolsListModuleOutput { func didTapMoreAvailablePools() { router.showAllAvailablePools(chain: chain, wallet: wallet, from: view, moduleOutput: self) } + + func shouldShowUserPools(_ shouldShow: Bool) { + view?.changeUserPoolsVisibility(visible: shouldShow) + } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewProtocols.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewProtocols.swift index 1e2eae403c..d90b93c364 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewProtocols.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewProtocols.swift @@ -2,10 +2,13 @@ import SSFModels typealias LiquidityPoolsOverviewModuleCreationResult = (view: LiquidityPoolsOverviewViewInput, input: LiquidityPoolsOverviewModuleInput) -protocol LiquidityPoolsOverviewViewInput: ControllerBackedProtocol {} +protocol LiquidityPoolsOverviewViewInput: ControllerBackedProtocol { + func changeUserPoolsVisibility(visible: Bool) +} protocol LiquidityPoolsOverviewViewOutput: AnyObject { func didLoad(view: LiquidityPoolsOverviewViewInput) + func backButtonClicked() } protocol LiquidityPoolsOverviewInteractorInput: AnyObject { @@ -14,7 +17,7 @@ protocol LiquidityPoolsOverviewInteractorInput: AnyObject { protocol LiquidityPoolsOverviewInteractorOutput: AnyObject {} -protocol LiquidityPoolsOverviewRouterInput: AnyObject { +protocol LiquidityPoolsOverviewRouterInput: AnyObject, AnyDismissable { func showAllAvailablePools( chain: ChainModel, wallet: MetaAccountModel, diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewController.swift index 2b827e7101..9219cbfe26 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewController.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewController.swift @@ -41,6 +41,10 @@ final class LiquidityPoolsOverviewViewController: UIViewController, ViewHolder, super.viewDidLoad() output.didLoad(view: self) + rootView.navigationBar.backButton.addAction { [weak self] in + self?.output.backButtonClicked() + } + setupEmbededUserPoolsView() setupEmbededAvailablePoolsView() } @@ -72,7 +76,11 @@ final class LiquidityPoolsOverviewViewController: UIViewController, ViewHolder, // MARK: - LiquidityPoolsOverviewViewInput -extension LiquidityPoolsOverviewViewController: LiquidityPoolsOverviewViewInput {} +extension LiquidityPoolsOverviewViewController: LiquidityPoolsOverviewViewInput { + func changeUserPoolsVisibility(visible: Bool) { + rootView.userPoolsContainerView.isHidden = !visible + } +} // MARK: - Localizable diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewLayout.swift index 54bb766ac4..77bfe9068c 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewLayout.swift @@ -21,12 +21,13 @@ final class LiquidityPoolsOverviewViewLayout: UIView { }() let scrollView = UIScrollView() - let scrollViewBackgroundView = UIView() + let scrollViewBackgroundView = UIFactory.default.createVerticalStackView(spacing: 16) let userPoolsContainerView = TriangularedBlurView() let availablePoolsContainerView = TriangularedBlurView() override init(frame: CGRect) { super.init(frame: frame) + userPoolsContainerView.isHidden = true drawSubviews() setupConstraints() } @@ -55,10 +56,10 @@ final class LiquidityPoolsOverviewViewLayout: UIView { addSubview(navigationBar) addSubview(scrollView) scrollView.addSubview(scrollViewBackgroundView) - scrollViewBackgroundView.addSubview(userPoolsContainerView) - scrollViewBackgroundView.addSubview(availablePoolsContainerView) + scrollViewBackgroundView.addArrangedSubview(userPoolsContainerView) + scrollViewBackgroundView.addArrangedSubview(availablePoolsContainerView) - navigationBar.setLeftViews([polkaswapImageView]) + navigationBar.setCenterViews([polkaswapImageView]) } private func setupConstraints() { @@ -81,13 +82,11 @@ final class LiquidityPoolsOverviewViewLayout: UIView { } userPoolsContainerView.snp.makeConstraints { make in - make.leading.trailing.top.equalToSuperview().inset(16) + make.leading.trailing.equalToSuperview().inset(16) } availablePoolsContainerView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(16) - make.top.equalTo(userPoolsContainerView.snp.bottom).offset(16) - make.bottom.lessThanOrEqualToSuperview().offset(16) } } } diff --git a/fearless/Modules/MainTabBar/MainTabBarWireframe.swift b/fearless/Modules/MainTabBar/MainTabBarWireframe.swift index 827750678a..a26cc8b43b 100644 --- a/fearless/Modules/MainTabBar/MainTabBarWireframe.swift +++ b/fearless/Modules/MainTabBar/MainTabBarWireframe.swift @@ -4,22 +4,22 @@ import WalletConnectSign final class MainTabBarWireframe: MainTabBarWireframeProtocol { func presentPolkaswap(on view: ControllerBackedProtocol?, wallet: MetaAccountModel) { - guard - let tabBarController = view?.controller, - let viewController = LiquidityPoolsOverviewAssembly.configureModule(wallet: wallet)?.view.controller - else { - return - } - let navigationController = FearlessNavigationController(rootViewController: viewController) - // guard // let tabBarController = view?.controller, -// let viewController = PolkaswapAdjustmentAssembly.configureModule(chainAsset: nil, wallet: wallet)?.view.controller +// let viewController = LiquidityPoolsOverviewAssembly.configureModule(wallet: wallet)?.view.controller // else { // return // } -// // let navigationController = FearlessNavigationController(rootViewController: viewController) + + guard + let tabBarController = view?.controller, + let viewController = PolkaswapAdjustmentAssembly.configureModule(chainAsset: nil, wallet: wallet)?.view.controller + else { + return + } + + let navigationController = FearlessNavigationController(rootViewController: viewController) let presentingController = tabBarController.topModalViewController presentingController.present(navigationController, animated: true, completion: nil) } diff --git a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentPresenter.swift b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentPresenter.swift index 14036ce01d..72d09784d9 100644 --- a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentPresenter.swift +++ b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentPresenter.swift @@ -714,6 +714,10 @@ extension PolkaswapAdjustmentPresenter: PolkaswapAdjustmentViewOutput { } detailsViewModel = provideDetailsViewModel(with: amounts) } + + func didTapLiquidityPools() { + router.presentLiquidityPools(on: view, wallet: wallet) + } } // MARK: - PolkaswapAdjustmentInteractorOutput diff --git a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentProtocols.swift b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentProtocols.swift index af936446f4..e8122518eb 100644 --- a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentProtocols.swift +++ b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentProtocols.swift @@ -35,6 +35,7 @@ protocol PolkaswapAdjustmentViewOutput: AnyObject { func selectToAmountPercentage(_ percentage: Float) func updateToAmount(_ newValue: Decimal) func viewDidAppear() + func didTapLiquidityPools() } protocol PolkaswapAdjustmentInteractorInput: AnyObject { @@ -91,6 +92,10 @@ protocol PolkaswapAdjustmentRouterInput: PresentDismissable, ErrorPresentable, S moduleOutput: PolkaswapDisclaimerModuleOutput?, from view: ControllerBackedProtocol? ) + func presentLiquidityPools( + on view: ControllerBackedProtocol?, + wallet: MetaAccountModel + ) } protocol PolkaswapAdjustmentModuleInput: AnyObject {} diff --git a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentRouter.swift b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentRouter.swift index 09227c9e3b..9597d557b6 100644 --- a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentRouter.swift +++ b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentRouter.swift @@ -71,4 +71,17 @@ final class PolkaswapAdjustmentRouter: PolkaswapAdjustmentRouterInput { view?.controller.present(module.view.controller, animated: true) } + + func presentLiquidityPools(on view: ControllerBackedProtocol?, wallet: MetaAccountModel) { + guard + let tabBarController = view?.controller, + let viewController = LiquidityPoolsOverviewAssembly.configureModule(wallet: wallet)?.view.controller + else { + return + } + let navigationController = FearlessNavigationController(rootViewController: viewController) + + let presentingController = tabBarController.topModalViewController + presentingController.present(navigationController, animated: true, completion: nil) + } } diff --git a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewController.swift b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewController.swift index 6a09440d5a..ed566e237a 100644 --- a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewController.swift +++ b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewController.swift @@ -106,6 +106,9 @@ final class PolkaswapAdjustmentViewController: UIViewController, ViewHolder, Hid action: #selector(handleTapSwitchInputsButton), for: .touchUpInside ) + rootView.bannerButton.addAction { [weak self] in + self?.output.didTapLiquidityPools() + } let tapMinReceiveInfo = UITapGestureRecognizer( target: self, diff --git a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewLayout.swift b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewLayout.swift index b6c28b4a22..3315d38c60 100644 --- a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewLayout.swift +++ b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewLayout.swift @@ -60,6 +60,11 @@ final class PolkaswapAdjustmentViewLayout: UIView { let fromPerToPriceView = UIFactory.default.createMultiView() let toPerFromPriceView = UIFactory.default.createMultiView() let networkFeeView = UIFactory.default.createMultiView() + let bannerButton: UIButton = { + let button = UIButton() + button.setImage(R.image.iconLpBanner(), for: .normal) + return button + }() private lazy var multiViews = [ minMaxReceivedView, @@ -180,6 +185,7 @@ final class PolkaswapAdjustmentViewLayout: UIView { private func setupContentsLayout() { addSubview(contentView) + addSubview(bannerButton) addSubview(previewButton) contentView.snp.makeConstraints { make in @@ -194,6 +200,12 @@ final class PolkaswapAdjustmentViewLayout: UIView { keyboardAdoptableConstraint = make.bottom.equalToSuperview().inset(UIConstants.bigOffset).constraint } + bannerButton.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(16) + make.height.equalTo(139) + make.bottom.equalTo(previewButton.snp.top).inset(-16) + } + let switchInputsView = createSwitchInputsView() contentView.stackView.addArrangedSubview(switchInputsView) switchInputsView.snp.makeConstraints { make in diff --git a/fearlessTests/Modules/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityTests.swift b/fearlessTests/Modules/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityTests.swift new file mode 100644 index 0000000000..d6e5acbc9a --- /dev/null +++ b/fearlessTests/Modules/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityTests.swift @@ -0,0 +1,16 @@ +import XCTest + +class LiquidityPoolRemoveLiquidityTests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + XCTFail("Did you forget to add tests?") + } +} diff --git a/fearlessTests/Modules/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmTests.swift b/fearlessTests/Modules/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmTests.swift new file mode 100644 index 0000000000..2c7ffce235 --- /dev/null +++ b/fearlessTests/Modules/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmTests.swift @@ -0,0 +1,16 @@ +import XCTest + +class LiquidityPoolRemoveLiquidityConfirmTests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + XCTFail("Did you forget to add tests?") + } +} From 805d1fdd7ee07ed6c75d68dabd8d27afee406692 Mon Sep 17 00:00:00 2001 From: Alex Lebedko Date: Mon, 17 Jun 2024 09:35:58 +0700 Subject: [PATCH 035/115] libs update --- fearless.xcodeproj/project.pbxproj | 10 ---------- .../xcshareddata/swiftpm/Package.resolved | 18 +++++++++--------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index 8cd9048998..26260e9d9c 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -5656,7 +5656,6 @@ FA8800672B31A335000AE5EB /* StakingAccountSubscriptionV13.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingAccountSubscriptionV13.swift; sourceTree = ""; }; FA8800692B31A33E000AE5EB /* StakingAccountSubscriptionV14.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingAccountSubscriptionV14.swift; sourceTree = ""; }; FA88006B2B31A46D000AE5EB /* StakingAccountSubscriptionAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingAccountSubscriptionAssembly.swift; sourceTree = ""; }; - FA8810D12BDCAF350084CC4B /* shared-features-spm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "shared-features-spm"; path = "../shared-features-spm"; sourceTree = ""; }; FA8810D22BDCCF7E0084CC4B /* UserLiquidityPoolsListPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLiquidityPoolsListPresenter.swift; sourceTree = ""; }; FA8810D42BDCD19D0084CC4B /* UserLiquidityPoolsListViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLiquidityPoolsListViewModelFactory.swift; sourceTree = ""; }; FA887A442C107A1100CA720F /* LiquidityPoolSupplyConfirmViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyConfirmViewModel.swift; sourceTree = ""; }; @@ -9170,7 +9169,6 @@ 8490139F24A80984008F705E = { isa = PBXGroup; children = ( - FAF25D3F2BD7C45E00EA4E3B /* Packages */, 849013AA24A80984008F705E /* fearless */, 849013C124A80986008F705E /* fearlessTests */, 8438E1D024BFAAD2001BDB13 /* fearlessIntegrationTests */, @@ -15525,14 +15523,6 @@ path = Adapter; sourceTree = ""; }; - FAF25D3F2BD7C45E00EA4E3B /* Packages */ = { - isa = PBXGroup; - children = ( - FA8810D12BDCAF350084CC4B /* shared-features-spm */, - ); - name = Packages; - sourceTree = ""; - }; FAF5E9C827E46D3E005A3448 /* Codable */ = { isa = PBXGroup; children = ( diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6d49df0a6e..8f914aac19 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -135,6 +135,15 @@ "version" : "0.1.7" } }, + { + "identity" : "shared-features-spm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/soramitsu/shared-features-spm.git", + "state" : { + "branch" : "storage-requests-update", + "revision" : "dfe89df77ebcecaed12ef92f3114ab05439b0bb7" + } + }, { "identity" : "swift-atomics", "kind" : "remoteSourceControl", @@ -225,15 +234,6 @@ "version" : "1.3.0" } }, - { - "identity" : "swiftformat", - "kind" : "remoteSourceControl", - "location" : "https://github.com/nicklockwood/SwiftFormat", - "state" : { - "revision" : "4bf475154c1c98dcdf751037f930f8e5c72597a4", - "version" : "0.53.10" - } - }, { "identity" : "swiftimagereadwrite", "kind" : "remoteSourceControl", From b339c0db7974f4bca846377f5126e546adcbbd99 Mon Sep 17 00:00:00 2001 From: Alex Lebedko Date: Mon, 17 Jun 2024 10:29:03 +0700 Subject: [PATCH 036/115] cache reset --- .../LiquidityPoolRemoveLiquidityPresenter.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityPresenter.swift index 9848ff3a39..c4a4a9ca11 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityPresenter.swift @@ -65,7 +65,6 @@ final class LiquidityPoolRemoveLiquidityPresenter { } private var baseTargetRate: Decimal? - private var dexId: String? private var baseAssetResultAmount: Decimal { From a63ef71f1b93be2d1b349b621c1ff7aa81ad26c5 Mon Sep 17 00:00:00 2001 From: Alex Lebedko Date: Mon, 17 Jun 2024 12:29:19 +0700 Subject: [PATCH 037/115] packages update --- fearless.xcodeproj/project.pbxproj | 10 ++++++++++ .../xcshareddata/swiftpm/Package.resolved | 18 +++++++++--------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index 26260e9d9c..058559b8a1 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -5661,6 +5661,7 @@ FA887A442C107A1100CA720F /* LiquidityPoolSupplyConfirmViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyConfirmViewModel.swift; sourceTree = ""; }; FA887A462C107A4300CA720F /* LiquidityPoolSupplyConfirmViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyConfirmViewModelFactory.swift; sourceTree = ""; }; FA887A482C1C19DB00CA720F /* WarningView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WarningView.swift; sourceTree = ""; }; + FA887A4B2C1FFFD400CA720F /* shared-features-spm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "shared-features-spm"; path = "../shared-features-spm"; sourceTree = ""; }; FA8C34B051607218638BA851 /* FiltersProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FiltersProtocols.swift; sourceTree = ""; }; FA8ED43228FD960F00EBB712 /* YourValidatorListFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YourValidatorListFlow.swift; sourceTree = ""; }; FA8ED43528FD983A00EBB712 /* YourValidatorListRelaychainStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YourValidatorListRelaychainStrategy.swift; sourceTree = ""; }; @@ -9169,6 +9170,7 @@ 8490139F24A80984008F705E = { isa = PBXGroup; children = ( + FA887A4A2C1FFFD400CA720F /* Packages */, 849013AA24A80984008F705E /* fearless */, 849013C124A80986008F705E /* fearlessTests */, 8438E1D024BFAAD2001BDB13 /* fearlessIntegrationTests */, @@ -14110,6 +14112,14 @@ path = ViewModel; sourceTree = ""; }; + FA887A4A2C1FFFD400CA720F /* Packages */ = { + isa = PBXGroup; + children = ( + FA887A4B2C1FFFD400CA720F /* shared-features-spm */, + ); + name = Packages; + sourceTree = ""; + }; FA8ED43128FD8D6200EBB712 /* Flows */ = { isa = PBXGroup; children = ( diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8f914aac19..d216b7bafc 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -135,15 +135,6 @@ "version" : "0.1.7" } }, - { - "identity" : "shared-features-spm", - "kind" : "remoteSourceControl", - "location" : "https://github.com/soramitsu/shared-features-spm.git", - "state" : { - "branch" : "storage-requests-update", - "revision" : "dfe89df77ebcecaed12ef92f3114ab05439b0bb7" - } - }, { "identity" : "swift-atomics", "kind" : "remoteSourceControl", @@ -234,6 +225,15 @@ "version" : "1.3.0" } }, + { + "identity" : "swiftformat", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nicklockwood/SwiftFormat", + "state" : { + "revision" : "dd989a46d0c6f15c016484bab8afe5e7a67a4022", + "version" : "0.54.0" + } + }, { "identity" : "swiftimagereadwrite", "kind" : "remoteSourceControl", From f0ab191e93f459ffb678fdf68545112dca7acbc5 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Mon, 17 Jun 2024 12:49:32 +0500 Subject: [PATCH 038/115] =?UTF-8?q?[#FLW-4683]=20We=20can=E2=80=99t=20read?= =?UTF-8?q?=20ETH=20QR=20with=20amount=20in=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fearless.xcodeproj/project.pbxproj | 68 +------------- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../QRService/QRExtractionService.swift | 78 --------------- .../QRService/QRExtractionServiceError.swift | 8 ++ .../AddressChainDefiner.swift | 14 +++ .../Common/QRCoding/BokoloCashQRInfo.swift | 68 -------------- fearless/Common/QRCoding/CexQRInfo.swift | 32 ------- fearless/Common/QRCoding/QRCoderFactory.swift | 68 -------------- .../Common/QRCoding/QRCreationOperation.swift | 70 -------------- fearless/Common/QRCoding/QRService.swift | 60 ------------ fearless/Common/QRCoding/SoraQRInfo.swift | 94 ------------------- .../EntityToModel/ChainModelMapper.swift | 4 +- .../CrossChain/CrossChainPresenter.swift | 1 + .../GetPreinstalledWalletAssembly.swift | 5 +- .../GetPreinstalledWalletInteractor.swift | 28 +++--- .../NFT/NftSend/NftSendPresenter.swift | 1 + .../ReceiveAndRequestAssetAssembly.swift | 6 +- .../ReceiveAndRequestAssetPresenter.swift | 78 +++++++-------- .../ScanQR/Matchers/QRInfoMatcher.swift | 17 ---- .../ScanQR/Matchers/QRMatcherProtocol.swift | 46 --------- .../ScanQR/Matchers/QRUriMatcher.swift | 17 ---- fearless/Modules/ScanQR/ScanQRAssembly.swift | 14 ++- .../Modules/ScanQR/ScanQRInteractor.swift | 31 +++--- fearless/Modules/ScanQR/ScanQRPresenter.swift | 74 ++------------- fearless/Modules/ScanQR/ScanQRProtocols.swift | 4 +- fearless/Modules/Send/SendFlow.swift | 7 +- fearless/Modules/Send/SendPresenter.swift | 44 ++++++++- fearless/Modules/Send/SendRouter.swift | 3 +- ...WalletConnectActiveSessionsPresenter.swift | 1 + .../WalletMainContainerPresenter.swift | 5 +- 30 files changed, 177 insertions(+), 771 deletions(-) delete mode 100644 fearless/ApplicationLayer/Services/QRService/QRExtractionService.swift create mode 100644 fearless/ApplicationLayer/Services/QRService/QRExtractionServiceError.swift delete mode 100644 fearless/Common/QRCoding/BokoloCashQRInfo.swift delete mode 100644 fearless/Common/QRCoding/CexQRInfo.swift delete mode 100644 fearless/Common/QRCoding/QRCoderFactory.swift delete mode 100644 fearless/Common/QRCoding/QRCreationOperation.swift delete mode 100644 fearless/Common/QRCoding/QRService.swift delete mode 100644 fearless/Common/QRCoding/SoraQRInfo.swift delete mode 100644 fearless/Modules/ScanQR/Matchers/QRInfoMatcher.swift delete mode 100644 fearless/Modules/ScanQR/Matchers/QRMatcherProtocol.swift delete mode 100644 fearless/Modules/ScanQR/Matchers/QRUriMatcher.swift diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index 38ee57e282..96d1c459cc 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -52,10 +52,6 @@ 070B2C5F289CE49700F78F82 /* SelectableListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 070B2C5D289CE49700F78F82 /* SelectableListViewController.swift */; }; 070B2C61289CFE0000F78F82 /* SelectedNetworkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 070B2C60289CFE0000F78F82 /* SelectedNetworkButton.swift */; }; 070CDD6D2ACAACB900F3F20A /* EthereumRemoteBalanceFetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 070CDD6C2ACAACB900F3F20A /* EthereumRemoteBalanceFetching.swift */; }; - 070CDD702ACACEDA00F3F20A /* QRMatcherProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 070CDD6F2ACACEDA00F3F20A /* QRMatcherProtocol.swift */; }; - 070CDD752ACAE04100F3F20A /* SoraQRInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 070CDD742ACAE04100F3F20A /* SoraQRInfo.swift */; }; - 070CDD772ACAE05D00F3F20A /* CexQRInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 070CDD762ACAE05D00F3F20A /* CexQRInfo.swift */; }; - 070CDD792ACBE19100F3F20A /* BokoloCashQRInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 070CDD782ACBE19000F3F20A /* BokoloCashQRInfo.swift */; }; 070CDD842ACBE59700F3F20A /* ReceiveAssetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 070CDD7B2ACBE59700F3F20A /* ReceiveAssetViewModel.swift */; }; 070CDD852ACBE59700F3F20A /* ReceiveAndRequestAssetRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 070CDD7C2ACBE59700F3F20A /* ReceiveAndRequestAssetRouter.swift */; }; 070CDD862ACBE59700F3F20A /* ReceiveAndRequestAssetViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 070CDD7D2ACBE59700F3F20A /* ReceiveAndRequestAssetViewLayout.swift */; }; @@ -1562,8 +1558,6 @@ C603E81128583C2A00007B72 /* StakingMainRelaychainStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C603E81028583C2A00007B72 /* StakingMainRelaychainStrategy.swift */; }; C61166692B3BFA9000F483C4 /* NftHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C61166682B3BFA9000F483C4 /* NftHeaderCell.swift */; }; C611666C2B3C03B800F483C4 /* NftHeaderCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C611666B2B3C03B800F483C4 /* NftHeaderCellViewModel.swift */; }; - C615668129309D3900391BF3 /* QRService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C615668029309D3900391BF3 /* QRService.swift */; }; - C61566852930A07900391BF3 /* QRCreationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C61566842930A07900391BF3 /* QRCreationOperation.swift */; }; C6264C292799A56E00FCA0DB /* WalletDetailsTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6264C282799A56E00FCA0DB /* WalletDetailsTableCell.swift */; }; C6264C2C2799C15C00FCA0DB /* WalletDetailsCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6264C2B2799C15C00FCA0DB /* WalletDetailsCellViewModel.swift */; }; C6264C2E2799C7EE00FCA0DB /* WalletDetailsViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6264C2D2799C7EE00FCA0DB /* WalletDetailsViewLayout.swift */; }; @@ -1899,7 +1893,7 @@ FA00489B282CCFDC0032FF49 /* SelectValidatorsStartRelaychainViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA00489A282CCFDC0032FF49 /* SelectValidatorsStartRelaychainViewModelFactory.swift */; }; FA0066E92935D07D0068FC61 /* RecommendedValidatorListPoolStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA0066E82935D07D0068FC61 /* RecommendedValidatorListPoolStrategy.swift */; }; FA072C14277AE2FE00731718 /* QRCaptureService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA072C13277AE2FE00731718 /* QRCaptureService.swift */; }; - FA072C16277B023D00731718 /* QRExtractionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA072C15277B023D00731718 /* QRExtractionService.swift */; }; + FA072C16277B023D00731718 /* QRExtractionServiceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA072C15277B023D00731718 /* QRExtractionServiceError.swift */; }; FA072C21277B19A900731718 /* ImageGalleryProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA072C20277B19A900731718 /* ImageGalleryProtocols.swift */; }; FA072C25277C0FA900731718 /* ApplicationSettingsPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA072C24277C0FA900731718 /* ApplicationSettingsPresentable.swift */; }; FA08B3652A31A09E00D9D126 /* BigUInt+StringInitializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA08B3642A31A09E00D9D126 /* BigUInt+StringInitializable.swift */; }; @@ -2385,7 +2379,6 @@ FA7254432AC2E48500EC47A6 /* WalletConnectService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA72541D2AC2E48400EC47A6 /* WalletConnectService.swift */; }; FA7254442AC2E48500EC47A6 /* WalletConnectSocketFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA72541E2AC2E48400EC47A6 /* WalletConnectSocketFactory.swift */; }; FA7254452AC2E48500EC47A6 /* WalletConnectPayloadFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA72541F2AC2E48500EC47A6 /* WalletConnectPayloadFactory.swift */; }; - FA7254472AC2E4AB00EC47A6 /* QRUriMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA7254462AC2E4AB00EC47A6 /* QRUriMatcher.swift */; }; FA7254672AC2F12D00EC47A6 /* WalletConnect in Frameworks */ = {isa = PBXBuildFile; productRef = FA7254662AC2F12D00EC47A6 /* WalletConnect */; }; FA7254692AC2F12D00EC47A6 /* WalletConnectAuth in Frameworks */ = {isa = PBXBuildFile; productRef = FA7254682AC2F12D00EC47A6 /* WalletConnectAuth */; }; FA72546B2AC2F12D00EC47A6 /* WalletConnectNetworking in Frameworks */ = {isa = PBXBuildFile; productRef = FA72546A2AC2F12D00EC47A6 /* WalletConnectNetworking */; }; @@ -2719,8 +2712,6 @@ FAADC1BA2926597400DA9903 /* ScanQRPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAADC1B32926597400DA9903 /* ScanQRPresenter.swift */; }; FAADC1BB2926597400DA9903 /* ScanQRProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAADC1B42926597400DA9903 /* ScanQRProtocols.swift */; }; FAADC1BC2926597400DA9903 /* ScanQRRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAADC1B52926597400DA9903 /* ScanQRRouter.swift */; }; - FAADC1C129265A7900DA9903 /* QRInfoMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAADC1BF29265A7900DA9903 /* QRInfoMatcher.swift */; }; - FAADC1C629265C9600DA9903 /* QRCoderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAADC1C529265C9600DA9903 /* QRCoderFactory.swift */; }; FAAF61742877DBC50094B4BC /* EthereumIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAAF61732877DBC50094B4BC /* EthereumIcon.swift */; }; FAAF946C2A0CFF90009A4BA5 /* Array+Duplicates.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAAF946B2A0CFF90009A4BA5 /* Array+Duplicates.swift */; }; FAB0EDD927AA692D003D93C2 /* NodeSelectionTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB0EDD827AA692D003D93C2 /* NodeSelectionTableCell.swift */; }; @@ -3117,10 +3108,6 @@ 070B2C5D289CE49700F78F82 /* SelectableListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectableListViewController.swift; sourceTree = ""; }; 070B2C60289CFE0000F78F82 /* SelectedNetworkButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedNetworkButton.swift; sourceTree = ""; }; 070CDD6C2ACAACB900F3F20A /* EthereumRemoteBalanceFetching.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EthereumRemoteBalanceFetching.swift; sourceTree = ""; }; - 070CDD6F2ACACEDA00F3F20A /* QRMatcherProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRMatcherProtocol.swift; sourceTree = ""; }; - 070CDD742ACAE04100F3F20A /* SoraQRInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoraQRInfo.swift; sourceTree = ""; }; - 070CDD762ACAE05D00F3F20A /* CexQRInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CexQRInfo.swift; sourceTree = ""; }; - 070CDD782ACBE19000F3F20A /* BokoloCashQRInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BokoloCashQRInfo.swift; sourceTree = ""; }; 070CDD7B2ACBE59700F3F20A /* ReceiveAssetViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveAssetViewModel.swift; sourceTree = ""; }; 070CDD7C2ACBE59700F3F20A /* ReceiveAndRequestAssetRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveAndRequestAssetRouter.swift; sourceTree = ""; }; 070CDD7D2ACBE59700F3F20A /* ReceiveAndRequestAssetViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveAndRequestAssetViewLayout.swift; sourceTree = ""; }; @@ -4640,8 +4627,6 @@ C603E81028583C2A00007B72 /* StakingMainRelaychainStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingMainRelaychainStrategy.swift; sourceTree = ""; }; C61166682B3BFA9000F483C4 /* NftHeaderCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NftHeaderCell.swift; sourceTree = ""; }; C611666B2B3C03B800F483C4 /* NftHeaderCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NftHeaderCellViewModel.swift; sourceTree = ""; }; - C615668029309D3900391BF3 /* QRService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRService.swift; sourceTree = ""; }; - C61566842930A07900391BF3 /* QRCreationOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRCreationOperation.swift; sourceTree = ""; }; C62522E202A1C5EE60D25122 /* NftSendPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftSendPresenter.swift; sourceTree = ""; }; C6264C282799A56E00FCA0DB /* WalletDetailsTableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletDetailsTableCell.swift; sourceTree = ""; }; C6264C2B2799C15C00FCA0DB /* WalletDetailsCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletDetailsCellViewModel.swift; sourceTree = ""; }; @@ -4989,7 +4974,7 @@ FA00489A282CCFDC0032FF49 /* SelectValidatorsStartRelaychainViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectValidatorsStartRelaychainViewModelFactory.swift; sourceTree = ""; }; FA0066E82935D07D0068FC61 /* RecommendedValidatorListPoolStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecommendedValidatorListPoolStrategy.swift; sourceTree = ""; }; FA072C13277AE2FE00731718 /* QRCaptureService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCaptureService.swift; sourceTree = ""; }; - FA072C15277B023D00731718 /* QRExtractionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRExtractionService.swift; sourceTree = ""; }; + FA072C15277B023D00731718 /* QRExtractionServiceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRExtractionServiceError.swift; sourceTree = ""; }; FA072C20277B19A900731718 /* ImageGalleryProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGalleryProtocols.swift; sourceTree = ""; }; FA072C24277C0FA900731718 /* ApplicationSettingsPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationSettingsPresentable.swift; sourceTree = ""; }; FA08B3642A31A09E00D9D126 /* BigUInt+StringInitializable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BigUInt+StringInitializable.swift"; sourceTree = ""; }; @@ -5450,7 +5435,6 @@ FA72541D2AC2E48400EC47A6 /* WalletConnectService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectService.swift; sourceTree = ""; }; FA72541E2AC2E48400EC47A6 /* WalletConnectSocketFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectSocketFactory.swift; sourceTree = ""; }; FA72541F2AC2E48500EC47A6 /* WalletConnectPayloadFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectPayloadFactory.swift; sourceTree = ""; }; - FA7254462AC2E4AB00EC47A6 /* QRUriMatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRUriMatcher.swift; sourceTree = ""; }; FA7336BD2A0E3B7F0096A291 /* NetworkClientFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkClientFactory.swift; sourceTree = ""; }; FA7336BE2A0E3B7F0096A291 /* RequestConfiguratorFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestConfiguratorFactory.swift; sourceTree = ""; }; FA7336BF2A0E3B7F0096A291 /* RequestSignerFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestSignerFactory.swift; sourceTree = ""; }; @@ -5781,8 +5765,6 @@ FAADC1B32926597400DA9903 /* ScanQRPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScanQRPresenter.swift; sourceTree = ""; }; FAADC1B42926597400DA9903 /* ScanQRProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScanQRProtocols.swift; sourceTree = ""; }; FAADC1B52926597400DA9903 /* ScanQRRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScanQRRouter.swift; sourceTree = ""; }; - FAADC1BF29265A7900DA9903 /* QRInfoMatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRInfoMatcher.swift; sourceTree = ""; }; - FAADC1C529265C9600DA9903 /* QRCoderFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRCoderFactory.swift; sourceTree = ""; }; FAAF61732877DBC50094B4BC /* EthereumIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthereumIcon.swift; sourceTree = ""; }; FAAF946B2A0CFF90009A4BA5 /* Array+Duplicates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Duplicates.swift"; sourceTree = ""; }; FAB0EDD827AA692D003D93C2 /* NodeSelectionTableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeSelectionTableCell.swift; sourceTree = ""; }; @@ -6403,26 +6385,6 @@ path = SelectNetwork; sourceTree = ""; }; - 070CDD6E2ACACEBC00F3F20A /* Matchers */ = { - isa = PBXGroup; - children = ( - 070CDD6F2ACACEDA00F3F20A /* QRMatcherProtocol.swift */, - FAADC1BF29265A7900DA9903 /* QRInfoMatcher.swift */, - FA7254462AC2E4AB00EC47A6 /* QRUriMatcher.swift */, - ); - path = Matchers; - sourceTree = ""; - }; - 070CDD732ACAE02400F3F20A /* QRInfos */ = { - isa = PBXGroup; - children = ( - 070CDD782ACBE19000F3F20A /* BokoloCashQRInfo.swift */, - 070CDD742ACAE04100F3F20A /* SoraQRInfo.swift */, - 070CDD762ACAE05D00F3F20A /* CexQRInfo.swift */, - ); - name = QRInfos; - sourceTree = ""; - }; 070CDD7A2ACBE59700F3F20A /* ReceiveAndRequestAsset */ = { isa = PBXGroup; children = ( @@ -9060,7 +9022,6 @@ FAC6CDA22BA80D590013A17E /* Errors */, FAC6CD9B2BA8096D0013A17E /* Localization */, FA6C178F2993601A00A55254 /* AddressChainDefiner */, - FAADC1BD29265A7900DA9903 /* QRCoding */, 07FBC9E328BE269F00ED65B4 /* ChainIssuesCenter */, FA4B929C2844D0C80003BCEF /* SyntaxSugar */, FAF5E9C827E46D3E005A3448 /* Codable */, @@ -12331,7 +12292,7 @@ isa = PBXGroup; children = ( FA072C13277AE2FE00731718 /* QRCaptureService.swift */, - FA072C15277B023D00731718 /* QRExtractionService.swift */, + FA072C15277B023D00731718 /* QRExtractionServiceError.swift */, ); path = QRService; sourceTree = ""; @@ -14475,7 +14436,6 @@ FAADC1AE2926597400DA9903 /* ScanQR */ = { isa = PBXGroup; children = ( - 070CDD6E2ACACEBC00F3F20A /* Matchers */, FAADC1AF2926597400DA9903 /* ScanQRAssembly.swift */, FAADC1B02926597400DA9903 /* ScanQRViewLayout.swift */, FAADC1B12926597400DA9903 /* ScanQRInteractor.swift */, @@ -14487,17 +14447,6 @@ path = ScanQR; sourceTree = ""; }; - FAADC1BD29265A7900DA9903 /* QRCoding */ = { - isa = PBXGroup; - children = ( - 070CDD732ACAE02400F3F20A /* QRInfos */, - C61566842930A07900391BF3 /* QRCreationOperation.swift */, - C615668029309D3900391BF3 /* QRService.swift */, - FAADC1C529265C9600DA9903 /* QRCoderFactory.swift */, - ); - path = QRCoding; - sourceTree = ""; - }; FAB0EDD727AA6922003D93C2 /* Views */ = { isa = PBXGroup; children = ( @@ -15972,7 +15921,6 @@ 84DD5F4C263DE82C00425ACF /* DataValidationRunner.swift in Sources */, 84D8F16B24D8263300AF43E9 /* ModalPickerFactory.swift in Sources */, 847C965E255363B9002D288F /* ExportRestoreJsonWireframe.swift in Sources */, - FA7254472AC2E4AB00EC47A6 /* QRUriMatcher.swift in Sources */, FAE9EBA3288ABBD4009390B6 /* AnalyticRewardsParachainViewModelFactory.swift in Sources */, 8434C9E425401EF3009E4191 /* TransactionHistoryItem.swift in Sources */, AE8B8835267349C200AB0AA9 /* CustomVlidatorRelaychainListFilter.swift in Sources */, @@ -16369,7 +16317,6 @@ 84FD3DB92540F70400A234E3 /* TransactionHistoryItem+Extrinsic.swift in Sources */, 84F6B6432619A8480038F10D /* SubscanConcreteExtrinsicsData.swift in Sources */, FAD429212A865680001D6A16 /* WalletNamePresenter.swift in Sources */, - C61566852930A07900391BF3 /* QRCreationOperation.swift in Sources */, 844EFB65265FD61D0090ACB1 /* CrowdloanContributeConfirmViewModel.swift in Sources */, FAADC1B72926597400DA9903 /* ScanQRViewLayout.swift in Sources */, AEE5FAFF26415E0C002B8FDC /* StakingRebondSetupPresenter.swift in Sources */, @@ -16609,7 +16556,6 @@ 84A2C90C24E192F50020D3B7 /* ShakeAnimator.swift in Sources */, FA6262352AC2E35A005D3D95 /* WalletConnectConfirmationInputData.swift in Sources */, FA7336DF2A0E3B880096A291 /* NetworkRequestType.swift in Sources */, - 070CDD752ACAE04100F3F20A /* SoraQRInfo.swift in Sources */, FACD42992A5BE811009975AA /* SubstrateStorageMigrator+Sync.swift in Sources */, FAA0132E28DA12B6000A5230 /* StakingPoolNetworkInfoViewModel.swift in Sources */, FAADC1AA29261F7000DA9903 /* PoolRolesConfirmInteractor.swift in Sources */, @@ -16716,7 +16662,6 @@ F4113B3F260C77FF00DF4DBA /* StakingRewardPayoutsViewLayout.swift in Sources */, FA17B4BA27E873E9006E0735 /* WarningAlertPresentAnimator.swift in Sources */, FA584C782AB2BCD500F6F020 /* UniversalMediaView.swift in Sources */, - 070CDD792ACBE19100F3F20A /* BokoloCashQRInfo.swift in Sources */, C66E68CE2BFC119300A14759 /* OnboardingConfigVersionResolver.swift in Sources */, AEA0C8A8267B6B3200F9666F /* SelectedValidatorListPresenter.swift in Sources */, FAFFAE9529AC84B10074AF1F /* GiantsquidTransfer.swift in Sources */, @@ -17180,7 +17125,6 @@ 84D8F15524D80CA100AF43E9 /* ModalPickerViewController.swift in Sources */, FA2569A1274CE66100875A53 /* SubscanMemoData.swift in Sources */, 84F43BAA25DEB75000AEDA56 /* StakingMainViewModel.swift in Sources */, - FAADC1C129265A7900DA9903 /* QRInfoMatcher.swift in Sources */, 84DD5F35263D86EF00425ACF /* AccountFetching.swift in Sources */, 849014A624AA801B008F705E /* TextSharingSource.swift in Sources */, 8490152524ABCBF3008F705E /* AmountFormatterFactory.swift in Sources */, @@ -17345,7 +17289,6 @@ FAD4292E2A865680001D6A16 /* BackupWalletImportedProtocols.swift in Sources */, 84BAB6102642C286007782D0 /* SelectedRebondVariant.swift in Sources */, 84F5107C263C0C11005D15AE /* AnyProviderCleaning.swift in Sources */, - C615668129309D3900391BF3 /* QRService.swift in Sources */, FAA0134628DA12CD000A5230 /* StakingUnbondSetupPoolViewModelState.swift in Sources */, 84BEE22325646AC000D05EB3 /* SelectedUsernameChanged.swift in Sources */, 8467FD4124ED3C72005D486C /* AlignableContentControl.swift in Sources */, @@ -18003,7 +17946,6 @@ C7D77690E10875CF1856EBA1 /* StakingRewardPayoutsProtocols.swift in Sources */, FA936BCE286C41DF0059B97A /* StakingRebondConfirmationRelaychainStrategy.swift in Sources */, FA4B928F284493C60003BCEF /* DelegateCall.swift in Sources */, - 070CDD702ACACEDA00F3F20A /* QRMatcherProtocol.swift in Sources */, 2918DCAEB91F8276446873C8 /* StakingRewardPayoutsWireframe.swift in Sources */, AB5E2A2B4CC823E6F6515ADD /* StakingRewardPayoutsPresenter.swift in Sources */, 84100F3C26A60E4C00A5054E /* YourValidatorListWarningSectionView.swift in Sources */, @@ -18288,8 +18230,7 @@ 8BF525D6B5DFB7CF6C03B015 /* AnalyticsValidatorsViewController.swift in Sources */, FA6262662AC2E35A005D3D95 /* WalletConnectProposalAssembly.swift in Sources */, 237AD34CD1C2778834D7B330 /* AnalyticsValidatorsViewFactory.swift in Sources */, - 070CDD772ACAE05D00F3F20A /* CexQRInfo.swift in Sources */, - FA072C16277B023D00731718 /* QRExtractionService.swift in Sources */, + FA072C16277B023D00731718 /* QRExtractionServiceError.swift in Sources */, FA37AE2A2859BA1D001DCA96 /* StakingBondMoreConfirmationParachainStrategy.swift in Sources */, 31E260D462BF33CFCDFEBA6C /* AnalyticsRewardDetailsProtocols.swift in Sources */, FA6262482AC2E35A005D3D95 /* WalletConnectActiveSessionsViewController.swift in Sources */, @@ -18472,7 +18413,6 @@ FA62625A2AC2E35A005D3D95 /* MultiSelectNetworksInteractor.swift in Sources */, FA34EEDC2B98723C0042E73E /* OnboardingPresenter.swift in Sources */, FA74359929C0735B0085A47E /* ChainSettingsRepositoryFactory.swift in Sources */, - FAADC1C629265C9600DA9903 /* QRCoderFactory.swift in Sources */, 75F3A0D30C8BC050489F4644 /* WalletOptionRouter.swift in Sources */, E6981A506AC931D30E85169E /* WalletOptionPresenter.swift in Sources */, 3245549CB47E65B28A2C01CD /* WalletOptionInteractor.swift in Sources */, diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index 740d4d33f8..af27930102 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -133,7 +133,7 @@ "location" : "https://github.com/soramitsu/shared-features-spm.git", "state" : { "branch" : "feature/xcm-routes", - "revision" : "def77f0a4496d16421aa92e16fb14494f93bfb82" + "revision" : "22290daa02ba856e8aa03a49cc9eb458c3e9df34" } }, { diff --git a/fearless/ApplicationLayer/Services/QRService/QRExtractionService.swift b/fearless/ApplicationLayer/Services/QRService/QRExtractionService.swift deleted file mode 100644 index 16c513600a..0000000000 --- a/fearless/ApplicationLayer/Services/QRService/QRExtractionService.swift +++ /dev/null @@ -1,78 +0,0 @@ -import Foundation -import UIKit - -protocol QRExtractionServiceProtocol { - func extract( - from image: UIImage, - dispatchCompletionIn queue: DispatchQueue?, - completionBlock: @escaping (Result) -> Void - ) -} - -enum QRExtractionServiceError: Error { - case invalidImage - case detectorUnavailable - case noFeatures - case plainAddress(address: String) -} - -final class QRExtractionService { - private let processingQueue: DispatchQueue - - init(processingQueue: DispatchQueue) { - self.processingQueue = processingQueue - } - - private func proccess(image: UIImage) -> Result { - var optionalImage: CIImage? - - if let ciImage = CIImage(image: image) { - optionalImage = ciImage - } else if let cgImage = image.cgImage { - optionalImage = CIImage(cgImage: cgImage) - } - - guard let ciImage = optionalImage else { - return .failure(QRExtractionServiceError.invalidImage) - } - - let options = [CIDetectorAccuracy: CIDetectorAccuracyHigh] - guard let detector = CIDetector( - ofType: CIDetectorTypeQRCode, - context: nil, - options: options - ) else { - return .failure(QRExtractionServiceError.detectorUnavailable) - } - - let features = detector.features(in: ciImage) - - let receivedString = features.compactMap { ($0 as? CIQRCodeFeature)?.messageString }.first - - guard let receivedString = receivedString else { - return .failure(QRExtractionServiceError.noFeatures) - } - - return .success(receivedString) - } -} - -extension QRExtractionService: QRExtractionServiceProtocol { - func extract( - from image: UIImage, - dispatchCompletionIn queue: DispatchQueue?, - completionBlock: @escaping (Result) -> Void - ) { - processingQueue.async { - let result = self.proccess(image: image) - - if let queue = queue { - queue.async { - completionBlock(result) - } - } else { - completionBlock(result) - } - } - } -} diff --git a/fearless/ApplicationLayer/Services/QRService/QRExtractionServiceError.swift b/fearless/ApplicationLayer/Services/QRService/QRExtractionServiceError.swift new file mode 100644 index 0000000000..838b5ae218 --- /dev/null +++ b/fearless/ApplicationLayer/Services/QRService/QRExtractionServiceError.swift @@ -0,0 +1,8 @@ +import Foundation + +enum QRExtractionServiceError: Error { + case invalidImage + case detectorUnavailable + case noFeatures + case plainAddress(address: String) +} diff --git a/fearless/Common/AddressChainDefiner/AddressChainDefiner.swift b/fearless/Common/AddressChainDefiner/AddressChainDefiner.swift index 28d21cdb5a..7f7413bcfd 100644 --- a/fearless/Common/AddressChainDefiner/AddressChainDefiner.swift +++ b/fearless/Common/AddressChainDefiner/AddressChainDefiner.swift @@ -56,6 +56,9 @@ final class AddressChainDefiner { let chains = try? fetchOperation.extractNoCancellableResultData() let posssibleChains = chains?.filter { [weak self, address] chain in guard let strongSelf = self else { return false } + guard strongSelf.chainIsEnabled(chain: chain) else { + return false + } return strongSelf.validate(address: address, for: chain).isValidOrSame } continuation.resume(returning: posssibleChains) @@ -72,4 +75,15 @@ final class AddressChainDefiner { } return .valid(address) } + + private func chainIsEnabled(chain: ChainModel) -> Bool { + let chainAssets = chain.chainAssets + let enabledAssetIds: [String] = wallet.assetsVisibility + .filter { !$0.hidden } + .map { $0.assetId } + let enabled = chainAssets.filter { + enabledAssetIds.contains($0.identifier) + } + return enabled.isNotEmpty + } } diff --git a/fearless/Common/QRCoding/BokoloCashQRInfo.swift b/fearless/Common/QRCoding/BokoloCashQRInfo.swift deleted file mode 100644 index 61e9686953..0000000000 --- a/fearless/Common/QRCoding/BokoloCashQRInfo.swift +++ /dev/null @@ -1,68 +0,0 @@ -import Foundation -import SSFQRService -#if F_RELEASE - import MPQRCoreSDK -#endif - -struct BokoloCashQRInfo: QRInfo { - let address: String - let assetId: String? - let transactionAmount: String? -} - -final class BokoloCashDecoder: QRDecoderProtocol { - #if F_RELEASE - func decode(data: Data) throws -> QRInfoType { - guard - let incomingURL = URL(dataRepresentation: data, relativeTo: nil, isAbsolute: true), - let payload = fetchPayloadData(url: incomingURL) - else { - throw QRDecoderError.brokenFormat - } - - let payloadData = try MPQRParser.parse(string: payload) - - var maiData: MAIData? - for value in EMVQRConstants.availableIDTags { - if let item = try? payloadData.getMAIData(forTagString: value) { - maiData = item - break - } - } - - guard let accountId = maiData?.AID else { - throw QRDecoderError.accountIdMismatch - } - - let qrInfo = BokoloCashQRInfo( - address: accountId, - assetId: payloadData.transactionCurrencyCode, - transactionAmount: payloadData.transactionAmount - ) - return .bokoloCash(qrInfo) - } - #else - func decode(data _: Data) throws -> QRInfoType { - throw ConvenienceError(error: "Not available on simulator") - } - #endif - - private func fetchPayloadData(url: URL?) -> String? { - guard - let url = url, - let longComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), - let queryItems = longComponents.queryItems, - let qr = queryItems.first(where: { component in - component.name == "qr" - }) - else { - return nil - } - - return qr.value?.replacingOccurrences(of: "+", with: " ") - } -} - -enum EMVQRConstants { - static let availableIDTags = Array(26 ... 51).map { String($0) } -} diff --git a/fearless/Common/QRCoding/CexQRInfo.swift b/fearless/Common/QRCoding/CexQRInfo.swift deleted file mode 100644 index e4f73478a0..0000000000 --- a/fearless/Common/QRCoding/CexQRInfo.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation -import SSFQRService - -struct CexQRInfo: QRInfo, Equatable { - let address: String -} - -final class CexQREncoder { - func encode(address: String) throws -> Data { - guard let data = address.data(using: .utf8) else { - throw QREncoderError.brokenData - } - return data - } -} - -final class CexQRDecoder: QRDecoderProtocol { - func decode(data: Data) throws -> QRInfoType { - guard let address = String(data: data, encoding: .utf8) else { - throw QRDecoderError.brokenFormat - } - - let substrateAccountId = try? address.toAccountIdWithTryExtractPrefix() - let ethereumAccountId = try? address.toAccountIdWithTryExtractPrefix() - - guard substrateAccountId != nil || ethereumAccountId != nil else { - throw QRDecoderError.brokenFormat - } - - return .cex(CexQRInfo(address: address)) - } -} diff --git a/fearless/Common/QRCoding/QRCoderFactory.swift b/fearless/Common/QRCoding/QRCoderFactory.swift deleted file mode 100644 index e35ea53b03..0000000000 --- a/fearless/Common/QRCoding/QRCoderFactory.swift +++ /dev/null @@ -1,68 +0,0 @@ -import Foundation - -protocol QRCoderFactoryProtocol { - func createEncoder() -> QREncoderProtocol - func createDecoder() -> QRDecoderProtocol -} - -protocol QREncoderProtocol { - func encode(with type: QRType) throws -> Data -} - -enum QRInfoType { - case bokoloCash(BokoloCashQRInfo) - case sora(SoraQRInfo) - case cex(CexQRInfo) -} - -protocol QRDecoderProtocol { - func decode(data: Data) throws -> QRInfoType -} - -enum QRType { - case address(String) - case addressInfo(SoraQRInfo) -} - -final class QRCoderFactory: QRCoderFactoryProtocol { - func createEncoder() -> QREncoderProtocol { - QREncoder() - } - - func createDecoder() -> QRDecoderProtocol { - QRDecoder() - } -} - -final class QREncoder: QREncoderProtocol { - func encode(with type: QRType) throws -> Data { - switch type { - case let .address(address): - return try CexQREncoder().encode(address: address) - case let .addressInfo(addressInfo): - return try SoraQREncoder().encode(addressInfo: addressInfo) - } - } -} - -final class QRDecoder: QRDecoderProtocol { - private lazy var qrDecoders: [QRDecoderProtocol] = [ - BokoloCashDecoder(), - SoraQRDecoder(), - CexQRDecoder() - ] - - func decode(data: Data) throws -> QRInfoType { - let types = qrDecoders.compactMap { - try? $0.decode(data: data) - } - guard - types.count == 1, - let infoType = types.first - else { - throw ConvenienceError(error: "Qr must have one coincidence") - } - - return infoType - } -} diff --git a/fearless/Common/QRCoding/QRCreationOperation.swift b/fearless/Common/QRCoding/QRCreationOperation.swift deleted file mode 100644 index a6a1187e86..0000000000 --- a/fearless/Common/QRCoding/QRCreationOperation.swift +++ /dev/null @@ -1,70 +0,0 @@ -import UIKit -import CoreImage -import RobinHood - -enum QRCreationOperationError: Error { - case generatorUnavailable - case generatedImageInvalid - case bitmapImageCreationFailed -} - -public final class QRCreationOperation: BaseOperation { - let payload: Data - let qrSize: CGSize - - init(payload: Data, qrSize: CGSize) { - self.payload = payload - self.qrSize = qrSize - - super.init() - } - - override public func main() { - super.main() - - guard let filter = CIFilter(name: "CIQRCodeGenerator") else { - if !isCancelled { - result = .failure(QRCreationOperationError.generatorUnavailable) - } - - return - } - - filter.setValue(payload, forKey: "inputMessage") - filter.setValue("Q", forKey: "inputCorrectionLevel") - - guard let qrImage = filter.outputImage else { - if !isCancelled { - result = .failure(QRCreationOperationError.generatedImageInvalid) - } - - return - } - - let transformedImage: CIImage - - if qrImage.extent.size.width * qrImage.extent.height > 0.0 { - let transform = CGAffineTransform( - scaleX: qrSize.width / qrImage.extent.width, - y: qrSize.height / qrImage.extent.height - ) - transformedImage = qrImage.transformed(by: transform) - } else { - transformedImage = qrImage - } - - let context = CIContext() - - guard let cgImage = context.createCGImage(transformedImage, from: transformedImage.extent) else { - if !isCancelled { - result = .failure(QRCreationOperationError.bitmapImageCreationFailed) - } - - return - } - - if !isCancelled { - result = .success(UIImage(cgImage: cgImage)) - } - } -} diff --git a/fearless/Common/QRCoding/QRService.swift b/fearless/Common/QRCoding/QRService.swift deleted file mode 100644 index 8d316cd120..0000000000 --- a/fearless/Common/QRCoding/QRService.swift +++ /dev/null @@ -1,60 +0,0 @@ -import RobinHood -import UIKit - -protocol QROperationFactoryProtocol: AnyObject { - func createCreationOperation(for payload: Data, qrSize: CGSize) -> QRCreationOperation -} - -final class QROperationFactory: QROperationFactoryProtocol { - func createCreationOperation(for payload: Data, qrSize: CGSize) -> QRCreationOperation { - QRCreationOperation(payload: payload, qrSize: qrSize) - } -} - -protocol QRServiceProtocol: AnyObject { - @discardableResult - func generate( - with qrType: QRType, - qrSize: CGSize, - runIn queue: DispatchQueue, - completionBlock: @escaping (Result?) -> Void - ) throws -> Operation -} - -final class QRService { - private let operationFactory: QROperationFactoryProtocol - private let operationQueue: OperationQueue - private let encoder: QREncoderProtocol - - init( - operationFactory: QROperationFactoryProtocol, - encoder: QREncoderProtocol = QREncoder(), - operationQueue: OperationQueue = OperationQueue() - ) { - self.operationFactory = operationFactory - self.encoder = encoder - self.operationQueue = operationQueue - } -} - -extension QRService: QRServiceProtocol { - @discardableResult - func generate( - with qrType: QRType, - qrSize: CGSize, - runIn queue: DispatchQueue, - completionBlock: @escaping (Result?) -> Void - ) throws -> Operation { - let payload = try encoder.encode(with: qrType) - let operation = operationFactory.createCreationOperation(for: payload, qrSize: qrSize) - - operation.completionBlock = { - queue.async { - completionBlock(operation.result) - } - } - - operationQueue.addOperation(operation) - return operation - } -} diff --git a/fearless/Common/QRCoding/SoraQRInfo.swift b/fearless/Common/QRCoding/SoraQRInfo.swift deleted file mode 100644 index 39151e1850..0000000000 --- a/fearless/Common/QRCoding/SoraQRInfo.swift +++ /dev/null @@ -1,94 +0,0 @@ -import Foundation -import SSFQRService - -struct SoraQRInfo: QRInfo, Equatable { - let prefix: String - let address: String - let rawPublicKey: Data - let username: String - let assetId: String - let amount: String? - - init( - prefix: String = SubstrateQRConstants.prefix, - address: String, - rawPublicKey: Data, - username: String, - assetId: String, - amount: String? - ) { - self.prefix = prefix - self.address = address - self.rawPublicKey = rawPublicKey - self.username = username - self.assetId = assetId - self.amount = amount - } -} - -final class SoraQREncoder { - private let separator: String - - init(separator: String = SubstrateQRConstants.fieldsSeparator) { - self.separator = separator - } - - func encode(addressInfo: SoraQRInfo) throws -> Data { - let fields: [String] = [ - addressInfo.prefix, - addressInfo.address, - addressInfo.rawPublicKey.toHex(includePrefix: true), - addressInfo.username, - addressInfo.assetId, - addressInfo.amount - ].compactMap { $0 } - - guard let data = fields.joined(separator: separator).data(using: .utf8) else { - throw QREncoderError.brokenData - } - - return data - } -} - -final class SoraQRDecoder: QRDecoderProtocol { - public func decode(data: Data) throws -> QRInfoType { - guard let decodedString = String(data: data, encoding: .utf8) else { - throw QRDecoderError.brokenFormat - } - - let fields = decodedString.components(separatedBy: SubstrateQRConstants.fieldsSeparator) - - guard fields.count >= 3 else { - throw QRDecoderError.unexpectedNumberOfFields - } - - let prefix = fields[0] - let address = fields[1] - let publicKey = try Data(hexStringSSF: fields[2]) - let username = fields[3] - let assetId = fields[4] - let amount = fields[safe: 5] - - if address.hasPrefix("0x") { - let qrInfo = SoraQRInfo( - address: address, - rawPublicKey: publicKey, - username: username, - assetId: assetId, - amount: amount - ) - return .sora(qrInfo) - } - - let qrInfo = SoraQRInfo( - prefix: prefix, - address: address, - rawPublicKey: publicKey, - username: username, - assetId: assetId, - amount: amount - ) - return .sora(qrInfo) - } -} diff --git a/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift b/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift index d919b3b029..b455b0ff6b 100644 --- a/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift +++ b/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift @@ -283,7 +283,7 @@ final class ChainModelMapper { guard let id = entity.id, let symbol = entity.symbol else { return nil } - return XcmAvailableAsset(id: id, symbol: symbol) + return XcmAvailableAsset(id: id, symbol: symbol, minAmount: nil) } let availableXcmAssetDestinations = entity.xcmConfig?.availableDestinations?.allObjects as? [CDXcmAvailableDestination] ?? [] let destinations: [XcmAvailableDestination] = availableXcmAssetDestinations.compactMap { @@ -295,7 +295,7 @@ final class ChainModelMapper { guard let id = entity.id, let symbol = entity.symbol else { return nil } - return XcmAvailableAsset(id: id, symbol: symbol) + return XcmAvailableAsset(id: id, symbol: symbol, minAmount: nil) } return XcmAvailableDestination( chainId: chainId, diff --git a/fearless/Modules/CrossChain/CrossChainPresenter.swift b/fearless/Modules/CrossChain/CrossChainPresenter.swift index fa79e5fb9a..b77e332d56 100644 --- a/fearless/Modules/CrossChain/CrossChainPresenter.swift +++ b/fearless/Modules/CrossChain/CrossChainPresenter.swift @@ -5,6 +5,7 @@ import BigInt import SSFExtrinsicKit import SSFUtils import SSFModels +import SSFQRService protocol CrossChainViewInput: ControllerBackedProtocol, LoadableViewProtocol { func didReceive(assetBalanceViewModel: AssetBalanceViewModelProtocol?) diff --git a/fearless/Modules/GetPreinstalledWallet/GetPreinstalledWalletAssembly.swift b/fearless/Modules/GetPreinstalledWallet/GetPreinstalledWalletAssembly.swift index 95732f500a..7e82b9fd3e 100644 --- a/fearless/Modules/GetPreinstalledWallet/GetPreinstalledWalletAssembly.swift +++ b/fearless/Modules/GetPreinstalledWallet/GetPreinstalledWalletAssembly.swift @@ -1,6 +1,7 @@ import UIKit import SoraFoundation import SoraKeystore +import SSFQRService final class GetPreinstalledWalletAssembly { static func configureModuleForExistingUser() -> GetPreinstalledWalletModuleCreationResult? { @@ -20,7 +21,6 @@ final class GetPreinstalledWalletAssembly { delegate: nil, delegateQueue: nil ) - let qrExtractionService = QRExtractionService(processingQueue: .global()) guard let keystoreImportService: KeystoreImportServiceProtocol = URLHandlingService.shared.findService() @@ -36,8 +36,9 @@ final class GetPreinstalledWalletAssembly { let accountRepositoryFactory = AccountRepositoryFactory(storageFacade: UserDataStorageFacade.shared) let accountRepository = accountRepositoryFactory.createMetaAccountRepository(for: nil, sortDescriptors: []) + let qrService = QRServiceDefault(matchers: [QRPreinstalledWalletMatcher()]) let interactor = GetPreinstalledWalletInteractor( - qrExtractionService: qrExtractionService, + qrService: qrService, qrScanService: qrScanService, accountOperationFactory: accountOperationFactory, accountRepository: accountRepository, diff --git a/fearless/Modules/GetPreinstalledWallet/GetPreinstalledWalletInteractor.swift b/fearless/Modules/GetPreinstalledWallet/GetPreinstalledWalletInteractor.swift index d7be574bba..8d834bb137 100644 --- a/fearless/Modules/GetPreinstalledWallet/GetPreinstalledWalletInteractor.swift +++ b/fearless/Modules/GetPreinstalledWallet/GetPreinstalledWalletInteractor.swift @@ -1,17 +1,18 @@ import UIKit +import SSFQRService import RobinHood final class GetPreinstalledWalletInteractor: BaseAccountImportInteractor { // MARK: - Private properties private weak var output: GetPreinstalledWalletInteractorOutput? - private let qrExtractionService: QRExtractionServiceProtocol + private let qrService: QRService private let qrScanService: QRCaptureServiceProtocol private let settings: SelectedWalletSettings private let eventCenter: EventCenterProtocol init( - qrExtractionService: QRExtractionServiceProtocol, + qrService: QRService, qrScanService: QRCaptureServiceProtocol, accountOperationFactory: MetaAccountOperationFactoryProtocol, accountRepository: AnyDataProviderRepository, @@ -21,7 +22,7 @@ final class GetPreinstalledWalletInteractor: BaseAccountImportInteractor { settings: SelectedWalletSettings, eventCenter: EventCenterProtocol ) { - self.qrExtractionService = qrExtractionService + self.qrService = qrService self.qrScanService = qrScanService self.settings = settings self.eventCenter = eventCenter @@ -87,21 +88,14 @@ extension GetPreinstalledWalletInteractor: GetPreinstalledWalletInteractorInput } func extractQr(from image: UIImage) { - qrExtractionService.extract( - from: image, - dispatchCompletionIn: .main - ) { [weak self] result in - switch result { - case let .success(code): - self?.output?.handleMatched(code: code) - case let .failure(error): - if case let QRExtractionServiceError.plainAddress(address) = error { - self?.output?.handleAddress(address) - return - } - - self?.output?.handleQRService(error: error) + do { + let matcher = try qrService.extractQrCode(from: image) + guard let preinstalledWallet = matcher.preinstalledWallet else { + throw ConvenienceError(error: "Matches has't preinstalled wallet") } + output?.handleAddress(preinstalledWallet) + } catch { + output?.handleQRService(error: error) } } diff --git a/fearless/Modules/NFT/NftSend/NftSendPresenter.swift b/fearless/Modules/NFT/NftSend/NftSendPresenter.swift index 24ce7608a7..baaf69bac7 100644 --- a/fearless/Modules/NFT/NftSend/NftSendPresenter.swift +++ b/fearless/Modules/NFT/NftSend/NftSendPresenter.swift @@ -2,6 +2,7 @@ import Foundation import SoraFoundation import BigInt import SSFModels +import SSFQRService final class NftSendPresenter { // MARK: Private properties diff --git a/fearless/Modules/ReceiveAndRequestAsset/ReceiveAndRequestAssetAssembly.swift b/fearless/Modules/ReceiveAndRequestAsset/ReceiveAndRequestAssetAssembly.swift index bf96e4cf90..a62ea09640 100644 --- a/fearless/Modules/ReceiveAndRequestAsset/ReceiveAndRequestAssetAssembly.swift +++ b/fearless/Modules/ReceiveAndRequestAsset/ReceiveAndRequestAssetAssembly.swift @@ -2,6 +2,7 @@ import UIKit import SoraUI import SoraFoundation import SSFModels +import SSFQRService final class ReceiveAndRequestAssetAssembly { static func configureModule( @@ -25,10 +26,7 @@ final class ReceiveAndRequestAssetAssembly { ) let router = ReceiveAndRequestAssetRouter() - let qrService = QRService( - operationFactory: QROperationFactory(), - encoder: QREncoder() - ) + let qrService = QRServiceDefault() let sharingFactory = AccountShareFactory() let presenter = ReceiveAndRequestAssetPresenter( diff --git a/fearless/Modules/ReceiveAndRequestAsset/ReceiveAndRequestAssetPresenter.swift b/fearless/Modules/ReceiveAndRequestAsset/ReceiveAndRequestAssetPresenter.swift index f2ef04ba87..016b1eae97 100644 --- a/fearless/Modules/ReceiveAndRequestAsset/ReceiveAndRequestAssetPresenter.swift +++ b/fearless/Modules/ReceiveAndRequestAsset/ReceiveAndRequestAssetPresenter.swift @@ -28,10 +28,10 @@ final class ReceiveAndRequestAssetPresenter { private let wallet: MetaAccountModel private var chainAsset: ChainAsset - private let qrService: QRServiceProtocol + private let qrService: QRService private let sharingFactory: AccountShareFactoryProtocol - private var qrOperation: Operation? + private var qrOperation: Task? private var inputResult: AmountInputResult? private var accountInfos: [ChainAssetKey: AccountInfo?] = [:] private var pricesData: [PriceData]? = [] @@ -45,7 +45,7 @@ final class ReceiveAndRequestAssetPresenter { init( wallet: MetaAccountModel, chainAsset: ChainAsset, - qrService: QRServiceProtocol, + qrService: QRService, sharingFactory: AccountShareFactoryProtocol, interactor: ReceiveAndRequestAssetInteractorInput, router: ReceiveAndRequestAssetRouterInput, @@ -134,44 +134,44 @@ final class ReceiveAndRequestAssetPresenter { private func generateQR() { cancelQRGeneration() - guard let account = wallet.fetch(for: chainAsset.chain.accountRequest()), let address = account.toAddress() else { - processOperation(result: .failure(ChainAccountFetchingError.accountNotExists)) - return - } - var qrType: QRType = .address(address) - if chainAsset.chain.isSora { - let balance = getBalance() - var inputAmount = inputResult?.absoluteValue(from: balance).stringWithPointSeparator - if - chainAsset.asset.currencyId == BokoloConstants.bokoloCashAssetCurrencyId, - var input = inputResult?.absoluteValue(from: balance) { - var drounded = Decimal() - NSDecimalRound(&drounded, &input, 2, .plain) - inputAmount = input.stringWithPointSeparator - } - let addressInfo = SoraQRInfo( - prefix: SubstrateQRConstants.prefix, - address: address, - rawPublicKey: account.publicKey, - username: wallet.name, - assetId: chainAsset.asset.currencyId ?? "", - amount: inputAmount - ) - qrType = .addressInfo(addressInfo) - } - do { - qrOperation = try qrService.generate( - with: qrType, - qrSize: Constants.qrSize, - runIn: .main - ) { [weak self] operationResult in - if let result = operationResult { - self?.qrOperation = nil - self?.processOperation(result: result) + qrOperation = Task { + do { + guard let account = wallet.fetch(for: chainAsset.chain.accountRequest()), let address = account.toAddress() else { + throw ChainAccountFetchingError.accountNotExists + } + var qrType: QRType = .address(address) + if chainAsset.chain.isSora { + let balance = getBalance() + var inputAmount = inputResult?.absoluteValue(from: balance).stringWithPointSeparator + if + chainAsset.asset.currencyId == BokoloConstants.bokoloCashAssetCurrencyId, + var input = inputResult?.absoluteValue(from: balance) { + var drounded = Decimal() + NSDecimalRound(&drounded, &input, 2, .plain) + inputAmount = input.stringWithPointSeparator + } + let addressInfo = SoraQRInfo( + prefix: SubstrateQRConstants.prefix, + address: address, + rawPublicKey: account.publicKey, + username: wallet.name, + assetId: chainAsset.asset.currencyId ?? "", + amount: inputAmount + ) + qrType = .addressInfo(addressInfo) + } + + let image = try await qrService.generate(with: qrType, qrSize: Constants.qrSize) + await MainActor.run { + processOperation(result: .success(image)) + } + return image + } catch { + await MainActor.run { + processOperation(result: .failure(error)) } + return nil } - } catch { - processOperation(result: .failure(error)) } } diff --git a/fearless/Modules/ScanQR/Matchers/QRInfoMatcher.swift b/fearless/Modules/ScanQR/Matchers/QRInfoMatcher.swift deleted file mode 100644 index 7395b933e5..0000000000 --- a/fearless/Modules/ScanQR/Matchers/QRInfoMatcher.swift +++ /dev/null @@ -1,17 +0,0 @@ -import SSFUtils - -final class QRInfoMatcher: QRMatcherProtocol { - private let decoder: QRDecoderProtocol - - init(decoder: QRDecoderProtocol) { - self.decoder = decoder - } - - func match(code: String) -> QRMatcherType? { - guard let data = code.data(using: .utf8), let info = try? decoder.decode(data: data) else { - return nil - } - - return .qrInfo(info) - } -} diff --git a/fearless/Modules/ScanQR/Matchers/QRMatcherProtocol.swift b/fearless/Modules/ScanQR/Matchers/QRMatcherProtocol.swift deleted file mode 100644 index f213c73171..0000000000 --- a/fearless/Modules/ScanQR/Matchers/QRMatcherProtocol.swift +++ /dev/null @@ -1,46 +0,0 @@ - -import Foundation -import SSFUtils - -enum QRMatcherType { - case qrInfo(QRInfoType) - case uri(String) - - var address: String? { - switch self { - case let .qrInfo(qRInfoType): - switch qRInfoType { - case let .sora(soraQRInfo): - return soraQRInfo.address - case let .cex(cexQRInfo): - return cexQRInfo.address - case let .bokoloCash(qrInfo): - return qrInfo.address - } - case .uri: - return nil - } - } - - var qrInfo: QRInfoType? { - switch self { - case let .qrInfo(qRInfoType): - return qRInfoType - case .uri: - return nil - } - } - - var uri: String? { - switch self { - case .qrInfo: - return nil - case let .uri(uri): - return uri - } - } -} - -protocol QRMatcherProtocol { - func match(code: String) -> QRMatcherType? -} diff --git a/fearless/Modules/ScanQR/Matchers/QRUriMatcher.swift b/fearless/Modules/ScanQR/Matchers/QRUriMatcher.swift deleted file mode 100644 index a8e1b6a7d0..0000000000 --- a/fearless/Modules/ScanQR/Matchers/QRUriMatcher.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Foundation - -final class QRUriMatcherImpl: QRMatcherProtocol { - private let scheme: String - - init(scheme: String) { - self.scheme = scheme - } - - func match(code: String) -> QRMatcherType? { - guard let url = URL(string: code), url.scheme == scheme else { - return nil - } - - return .uri(code) - } -} diff --git a/fearless/Modules/ScanQR/ScanQRAssembly.swift b/fearless/Modules/ScanQR/ScanQRAssembly.swift index 14b4f18de7..1318178784 100644 --- a/fearless/Modules/ScanQR/ScanQRAssembly.swift +++ b/fearless/Modules/ScanQR/ScanQRAssembly.swift @@ -1,10 +1,11 @@ import UIKit import SoraFoundation +import SSFQRService final class ScanQRAssembly { static func configureModule( moduleOutput: ScanQRModuleOutput, - matchers: [QRMatcherProtocol] = ScanQRAssembly.defaultMatchers + matchers: [QRMatcher] = ScanQRAssembly.defaultMatchers ) -> ScanQRModuleCreationResult? { let localizationManager = LocalizationManager.shared @@ -13,8 +14,12 @@ final class ScanQRAssembly { delegateQueue: nil ) + let qrService = QRServiceDefault( + matchers: matchers + ) + let interactor = ScanQRInteractor( - qrExtractionService: QRExtractionService(processingQueue: .global()), + qrService: qrService, qrScanService: qrScanService ) let router = ScanQRRouter() @@ -24,7 +29,6 @@ final class ScanQRAssembly { router: router, logger: Logger.shared, moduleOutput: moduleOutput, - matchers: matchers, localizationManager: LocalizationManager.shared ) @@ -36,9 +40,9 @@ final class ScanQRAssembly { return (view, presenter) } - static var defaultMatchers: [QRMatcherProtocol] { + static var defaultMatchers: [QRMatcher] { [ - QRInfoMatcher(decoder: QRCoderFactory().createDecoder()), + QRInfoMatcher(decoder: QRDecoderDefault()), QRUriMatcherImpl(scheme: "wc") ] } diff --git a/fearless/Modules/ScanQR/ScanQRInteractor.swift b/fearless/Modules/ScanQR/ScanQRInteractor.swift index 86e957f4c5..4ce795e1a0 100644 --- a/fearless/Modules/ScanQR/ScanQRInteractor.swift +++ b/fearless/Modules/ScanQR/ScanQRInteractor.swift @@ -1,17 +1,18 @@ import UIKit +import SSFQRService final class ScanQRInteractor { // MARK: - Private properties private weak var output: ScanQRInteractorOutput? - private let qrExtractionService: QRExtractionServiceProtocol + private let qrService: QRService private let qrScanService: QRCaptureServiceProtocol init( - qrExtractionService: QRExtractionServiceProtocol, + qrService: QRService, qrScanService: QRCaptureServiceProtocol ) { - self.qrExtractionService = qrExtractionService + self.qrService = qrService self.qrScanService = qrScanService } } @@ -25,16 +26,20 @@ extension ScanQRInteractor: ScanQRInteractorInput { } func extractQr(from image: UIImage) { - qrExtractionService.extract( - from: image, - dispatchCompletionIn: .global() - ) { [weak self] result in - switch result { - case let .success(code): - self?.output?.handleMatched(code: code) - case let .failure(error): - self?.output?.handleQRService(error: error) - } + do { + let matcher = try qrService.extractQrCode(from: image) + output?.didReceive(matcher: matcher) + } catch { + output?.handleQRService(error: error) + } + } + + func lookingMatcher(for code: String) { + do { + let matcher = try qrService.lookingMatcher(for: code) + output?.didReceive(matcher: matcher) + } catch { + output?.handleQRService(error: error) } } diff --git a/fearless/Modules/ScanQR/ScanQRPresenter.swift b/fearless/Modules/ScanQR/ScanQRPresenter.swift index 91b9073fc6..2833e3840e 100644 --- a/fearless/Modules/ScanQR/ScanQRPresenter.swift +++ b/fearless/Modules/ScanQR/ScanQRPresenter.swift @@ -1,4 +1,5 @@ import Foundation +import SSFQRService import SoraFoundation import RobinHood @@ -13,23 +14,6 @@ enum ScanState { case failed(code: String) } -enum ScanFinish { - case address(String) - case sora(SoraQRInfo) - case bokoloCash(BokoloCashQRInfo) - - var address: String { - switch self { - case let .address(address): - return address - case let .sora(soraQRInfo): - return soraQRInfo.address - case let .bokoloCash(bokoloQrInfo): - return bokoloQrInfo.address - } - } -} - final class ScanQRPresenter: NSObject { let localizationManager: LocalizationManagerProtocol? @@ -40,7 +24,6 @@ final class ScanQRPresenter: NSObject { private let router: ScanQRRouterInput private let interactor: ScanQRInteractorInput - private let matchers: [QRMatcherProtocol] private let logger: LoggerProtocol private var scanState: ScanState = .initializing(accessRequested: false) @@ -52,13 +35,11 @@ final class ScanQRPresenter: NSObject { router: ScanQRRouterInput, logger: LoggerProtocol, moduleOutput: ScanQRModuleOutput?, - matchers: [QRMatcherProtocol], localizationManager: LocalizationManagerProtocol ) { self.interactor = interactor self.router = router self.logger = logger - self.matchers = matchers self.moduleOutput = moduleOutput self.localizationManager = localizationManager @@ -123,49 +104,6 @@ final class ScanQRPresenter: NSObject { interactor.extractQr(from: image) } } - - private func handleConnect(uri: String) { - router.close(view: view) { [weak self] in - self?.moduleOutput?.didFinishWith(scanType: .uri(uri)) - } - } - - private func searchMetcher(code: String) { - let qrMatcherTypes = matchers.map { $0.match(code: code) }.compactMap { $0 } - if qrMatcherTypes.isEmpty { - DispatchQueue.main.async { - self.view?.didStartLoading() - let viewModel = SheetAlertPresentableViewModel( - title: R.string.localizable.commonUndefinedErrorMessage( - preferredLanguages: self.selectedLocale.rLanguages - ), - message: nil, - actions: [], - closeAction: nil, - dismissCompletion: { [weak self] in - self?.scanState = .initializing(accessRequested: true) - DispatchQueue.global().async { - self?.interactor.startScanning() - } - } - ) - self.router.present(viewModel: viewModel, from: self.view) - } - } - guard - qrMatcherTypes.count == 1, - let qrType = qrMatcherTypes.first - else { - logger.error("QR must have one matching") - return - } - - DispatchQueue.main.async { - self.router.close(view: self.view) { [weak self] in - self?.moduleOutput?.didFinishWith(scanType: qrType) - } - } - } } // MARK: - ScanQRViewOutput @@ -218,8 +156,12 @@ extension ScanQRPresenter: ScanQRViewOutput { // MARK: - ScanQRInteractorOutput extension ScanQRPresenter: ScanQRInteractorOutput { - func handleMatched(code: String) { - searchMetcher(code: code) + func didReceive(matcher: QRMatcherType) { + DispatchQueue.main.async { + self.router.close(view: self.view) { [weak self] in + self?.moduleOutput?.didFinishWith(scanType: matcher) + } + } } func handleQRService(error: Error) { @@ -250,7 +192,7 @@ extension ScanQRPresenter: QRCaptureServiceDelegate { } func qrCapture(service _: QRCaptureServiceProtocol, didMatch code: String) { - searchMetcher(code: code) + interactor.lookingMatcher(for: code) } func qrCapture(service _: QRCaptureServiceProtocol, didReceive error: Error) { diff --git a/fearless/Modules/ScanQR/ScanQRProtocols.swift b/fearless/Modules/ScanQR/ScanQRProtocols.swift index 8d3b9572de..9214c64753 100644 --- a/fearless/Modules/ScanQR/ScanQRProtocols.swift +++ b/fearless/Modules/ScanQR/ScanQRProtocols.swift @@ -1,6 +1,7 @@ import AVFoundation import UIKit import SSFUtils +import SSFQRService typealias ScanQRModuleCreationResult = (view: ScanQRViewInput, input: ScanQRModuleInput) @@ -24,11 +25,12 @@ protocol ScanQRInteractorInput: AnyObject { func extractQr(from image: UIImage) func startScanning() func stopScanning() + func lookingMatcher(for code: String) } protocol ScanQRInteractorOutput: AnyObject { func handleQRService(error: Error) - func handleMatched(code: String) + func didReceive(matcher: QRMatcherType) } protocol ScanQRRouterInput: ApplicationSettingsPresentable, PresentDismissable, ImageGalleryPresentable, SheetAlertPresentable { diff --git a/fearless/Modules/Send/SendFlow.swift b/fearless/Modules/Send/SendFlow.swift index f51274c8d4..2791933e10 100644 --- a/fearless/Modules/Send/SendFlow.swift +++ b/fearless/Modules/Send/SendFlow.swift @@ -1,5 +1,5 @@ - import SSFModels +import SSFQRService enum SendFlowType { case token @@ -11,6 +11,7 @@ enum SendFlowInitialData { case address(String) case soraMainnet(qrInfo: SoraQRInfo) case bokoloCash(qrInfo: BokoloCashQRInfo) + case desiredCryptocurrency(qrInfo: DesiredCryptocurrencyQRInfo) init(qrInfoType: QRInfoType) { switch qrInfoType { @@ -20,12 +21,14 @@ enum SendFlowInitialData { self = .soraMainnet(qrInfo: soraQRInfo) case let .cex(cexQRInfo): self = .address(cexQRInfo.address) + case let .desiredCryptocurrency(qrInfo): + self = .desiredCryptocurrency(qrInfo: qrInfo) } } var selectableAsset: Bool { switch self { - case .chainAsset, .address: + case .chainAsset, .address, .desiredCryptocurrency: return true case .soraMainnet, .bokoloCash: return false diff --git a/fearless/Modules/Send/SendPresenter.swift b/fearless/Modules/Send/SendPresenter.swift index ec631dea0d..7871106898 100644 --- a/fearless/Modules/Send/SendPresenter.swift +++ b/fearless/Modules/Send/SendPresenter.swift @@ -3,6 +3,7 @@ import SoraFoundation import BigInt import SSFUtils import SSFModels +import SSFQRService final class SendPresenter { enum State { @@ -33,9 +34,8 @@ final class SendPresenter { private var selectedChain: ChainModel? private var selectedChainAsset: ChainAsset? { didSet { - checkSendAllVisibility() - DispatchQueue.main.async { + self.checkSendAllVisibility() self.view?.setInputAccessoryView(visible: self.selectedChainAsset?.isBokolo == false) } } @@ -246,7 +246,7 @@ final class SendPresenter { return } interactor.didReceive(xorlessTransfer: transfer) - case .address, .chainAsset, .soraMainnet: + case .address, .chainAsset, .soraMainnet, .desiredCryptocurrency: if selectedChainAsset?.isBokolo == true { guard let transfer = prepareXorlessTransfer() else { return @@ -753,6 +753,40 @@ final class SendPresenter { } } + private func handleDesiredCrypto(qrInfo: DesiredCryptocurrencyQRInfo) { + recipientAddress = qrInfo.address + Task { + let possibleChains = await self.interactor.getPossibleChains(for: qrInfo.address) + let chainAsset = possibleChains? + .first(where: { $0.name.lowercased() == qrInfo.assetName.lowercased() })? + .chainAssets + .first(where: { $0.asset.isUtility }) + + selectedChainAsset = chainAsset + + if let qrAmount = Decimal(string: qrInfo.amount ?? "") { + inputResult = .absolute(qrAmount) + } + + let viewModel = viewModelFactory.buildRecipientViewModel( + address: qrInfo.address, + isValid: true, + canEditing: false + ) + + if let qrChainAsset = chainAsset { + interactor.updateSubscriptions(for: qrChainAsset) + } + await MainActor.run { + view?.didReceive(viewModel: viewModel) + provideInputViewModel() + if let qrChainAsset = chainAsset { + provideNetworkViewModel(for: qrChainAsset.chain, canEdit: true) + } + } + } + } + private func prepareXorlessTransfer() -> XorlessTransfer? { do { guard let selectedChainAsset = selectedChainAsset else { @@ -875,6 +909,8 @@ extension SendPresenter: SendViewOutput { handleSora(qrInfo: qrInfo) case let .bokoloCash(bokoloCashQRInfo): handleBokoloCash(qrInfo: bokoloCashQRInfo) + case let .desiredCryptocurrency(qrInfo): + handleDesiredCrypto(qrInfo: qrInfo) } } @@ -1158,6 +1194,8 @@ extension SendPresenter: ScanQRModuleOutput { handleSora(qrInfo: qrInfo) case let .cex(qrInfo): searchTextDidChanged(qrInfo.address) + case let .desiredCryptocurrency(qrInfo): + handleDesiredCrypto(qrInfo: qrInfo) } } } diff --git a/fearless/Modules/Send/SendRouter.swift b/fearless/Modules/Send/SendRouter.swift index dd8b140e1d..6048d1009f 100644 --- a/fearless/Modules/Send/SendRouter.swift +++ b/fearless/Modules/Send/SendRouter.swift @@ -1,4 +1,5 @@ import Foundation +import SSFQRService import SSFModels final class SendRouter: SendRouterInput { @@ -30,7 +31,7 @@ final class SendRouter: SendRouterInput { from view: ControllerBackedProtocol?, moduleOutput: ScanQRModuleOutput ) { - let matcher = QRInfoMatcher(decoder: QRCoderFactory().createDecoder()) + let matcher = QRInfoMatcher(decoder: QRDecoderDefault()) guard let module = ScanQRAssembly.configureModule(moduleOutput: moduleOutput, matchers: [matcher]) else { return } diff --git a/fearless/Modules/WalletConnectActiveSessions/WalletConnectActiveSessionsPresenter.swift b/fearless/Modules/WalletConnectActiveSessions/WalletConnectActiveSessionsPresenter.swift index aae6a8766c..e5def54c4d 100644 --- a/fearless/Modules/WalletConnectActiveSessions/WalletConnectActiveSessionsPresenter.swift +++ b/fearless/Modules/WalletConnectActiveSessions/WalletConnectActiveSessionsPresenter.swift @@ -1,6 +1,7 @@ import Foundation import WalletConnectSign import SoraFoundation +import SSFQRService protocol WalletConnectActiveSessionsViewInput: ControllerBackedProtocol, HiddableBarWhenPushed, LoadableViewProtocol { func didReceive(viewModels: [WalletConnectActiveSessionsViewModel]) diff --git a/fearless/Modules/WalletMainContainer/WalletMainContainerPresenter.swift b/fearless/Modules/WalletMainContainer/WalletMainContainerPresenter.swift index f42a55d9a2..179d90e075 100644 --- a/fearless/Modules/WalletMainContainer/WalletMainContainerPresenter.swift +++ b/fearless/Modules/WalletMainContainer/WalletMainContainerPresenter.swift @@ -1,4 +1,5 @@ import Foundation +import SSFQRService import SoraFoundation import SSFUtils import SSFModels @@ -258,8 +259,10 @@ extension WalletMainContainerPresenter: ScanQRModuleOutput { wallet: wallet, initialData: .init(qrInfoType: qrInfoType) ) - case let .uri(uri): + case let .walletConnect(uri): walletConnect(with: uri) + case .preinstalledWallet: + break } } } From 5ceefd6b5c394c591fd8ef092f4e9856943a5726 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Mon, 17 Jun 2024 14:57:58 +0500 Subject: [PATCH 039/115] =?UTF-8?q?[#FLW-4685]=20We=20can=E2=80=99t=20swit?= =?UTF-8?q?ch=20the=20nodes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Helpers/ChainRepositoryFactory.swift | 12 +++ .../Views/AssetNetworksTableCell.swift | 1 + .../NodeSelectionInteractor.swift | 84 ++++--------------- .../NodeSelectionProtocols.swift | 2 +- .../NodeSelectionViewController.swift | 7 ++ .../NodeSelectionViewFactory.swift | 5 +- 6 files changed, 40 insertions(+), 71 deletions(-) diff --git a/fearless/Common/Helpers/ChainRepositoryFactory.swift b/fearless/Common/Helpers/ChainRepositoryFactory.swift index abc1c5040f..eae337fa47 100644 --- a/fearless/Common/Helpers/ChainRepositoryFactory.swift +++ b/fearless/Common/Helpers/ChainRepositoryFactory.swift @@ -20,4 +20,16 @@ final class ChainRepositoryFactory { mapper: AnyCoreDataMapper(mapper) ) } + + func createAsyncRepository( + for filter: NSPredicate? = nil, + sortDescriptors: [NSSortDescriptor] = [] + ) -> AsyncCoreDataRepositoryDefault { + let mapper = ChainModelMapper() + return storageFacade.createAsyncRepository( + filter: filter, + sortDescriptors: sortDescriptors, + mapper: AnyCoreDataMapper(mapper) + ) + } } diff --git a/fearless/Modules/AssetNetworks/Views/AssetNetworksTableCell.swift b/fearless/Modules/AssetNetworks/Views/AssetNetworksTableCell.swift index f8575a1923..17920996bc 100644 --- a/fearless/Modules/AssetNetworks/Views/AssetNetworksTableCell.swift +++ b/fearless/Modules/AssetNetworks/Views/AssetNetworksTableCell.swift @@ -78,6 +78,7 @@ final class AssetNetworksTableCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() + viewModel?.iconViewModel?.cancel(on: networkIconImageView) viewModel = nil } diff --git a/fearless/Modules/NodeSelection/NodeSelectionInteractor.swift b/fearless/Modules/NodeSelection/NodeSelectionInteractor.swift index 2d94820b2a..847f8b8c88 100644 --- a/fearless/Modules/NodeSelection/NodeSelectionInteractor.swift +++ b/fearless/Modules/NodeSelection/NodeSelectionInteractor.swift @@ -5,107 +5,57 @@ import SSFModels final class NodeSelectionInteractor { weak var presenter: NodeSelectionInteractorOutputProtocol? - var chain: ChainModel - let repository: AnyDataProviderRepository - let operationManager: OperationManagerProtocol - let eventCenter: EventCenterProtocol + internal var chain: ChainModel + private let repository: AsyncAnyRepository + private let eventCenter: EventCenterProtocol private let chainRegistry: ChainRegistryProtocol init( chain: ChainModel, - repository: AnyDataProviderRepository, - operationManager: OperationManagerProtocol, + repository: AsyncAnyRepository, eventCenter: EventCenterProtocol, chainRegistry: ChainRegistryProtocol ) { self.chain = chain self.repository = repository - self.operationManager = operationManager self.eventCenter = eventCenter self.chainRegistry = chainRegistry } - private func fetchChainModel() { - let operation = repository.fetchOperation(by: chain.chainId, options: RepositoryFetchOptions()) - - operation.completionBlock = { [weak self] in - guard let chainModel = try? operation.extractNoCancellableResultData() else { - return - } - self?.chain = chainModel - DispatchQueue.main.async { - self?.presenter?.didReceive(chain: chainModel) - } - } + private func applyChanges(for chain: ChainModel) async { + self.chain = chain - operationManager.enqueue(operations: [operation], in: .transient) + let event = ChainsUpdatedEvent(updatedChains: [chain]) + eventCenter.notify(with: event) } } extension NodeSelectionInteractor: NodeSelectionInteractorInputProtocol { func setup() { presenter?.didReceive(chain: chain) - fetchChainModel() eventCenter.add(observer: self) } func deleteNode(_ node: ChainNodeModel) { - guard let customNodes = chain.customNodes else { - return - } - - let updatedChain = chain.replacingCustomNodes(customNodes.filter { $0 != node }) - - let saveOperation = repository.saveOperation { - [updatedChain] - } _: { - [] - } - - saveOperation.completionBlock = { [weak self] in - guard let self = self else { return } - - self.chain = updatedChain - - DispatchQueue.main.async { - self.presenter?.didReceive(chain: updatedChain) + Task { + guard let customNodes = chain.customNodes else { + return } - let event = ChainsUpdatedEvent(updatedChains: [updatedChain]) - self.eventCenter.notify(with: event) + let updatedChain = chain.replacingCustomNodes(customNodes.filter { $0 != node }) + await repository.save(models: [updatedChain]) + await applyChanges(for: updatedChain) } - - operationManager.enqueue(operations: [saveOperation], in: .transient) } func selectNode(_ node: ChainNodeModel?) { chainRegistry.resetConnection(for: chain.chainId) - let saveOperation = repository.saveOperation { [weak self] in - guard let self = self else { - return [] - } + Task { let updatedChain = self.chain.replacingSelectedNode(node) - return [updatedChain] - } _: { - [] + await repository.save(models: [updatedChain]) + await applyChanges(for: updatedChain) } - - saveOperation.completionBlock = { [weak self] in - guard let self = self else { return } - let updatedChain = self.chain.replacingSelectedNode(node) - - self.chain = updatedChain - - DispatchQueue.main.async { - self.presenter?.didReceive(chain: updatedChain) - } - - let event = ChainsUpdatedEvent(updatedChains: [updatedChain]) - self.eventCenter.notify(with: event) - } - - operationManager.enqueue(operations: [saveOperation], in: .transient) } func setAutomaticSwitchNodes(_ automatic: Bool) { diff --git a/fearless/Modules/NodeSelection/NodeSelectionProtocols.swift b/fearless/Modules/NodeSelection/NodeSelectionProtocols.swift index c6eb5e8f3d..6e00be09ec 100644 --- a/fearless/Modules/NodeSelection/NodeSelectionProtocols.swift +++ b/fearless/Modules/NodeSelection/NodeSelectionProtocols.swift @@ -1,7 +1,7 @@ import Foundation import SSFModels -protocol NodeSelectionViewProtocol: ControllerBackedProtocol { +protocol NodeSelectionViewProtocol: ControllerBackedProtocol, LoadableViewProtocol { func didReceive(state: NodeSelectionViewState) func didReceive(locale: Locale) } diff --git a/fearless/Modules/NodeSelection/NodeSelectionViewController.swift b/fearless/Modules/NodeSelection/NodeSelectionViewController.swift index c78a198b4b..a07021e22f 100644 --- a/fearless/Modules/NodeSelection/NodeSelectionViewController.swift +++ b/fearless/Modules/NodeSelection/NodeSelectionViewController.swift @@ -13,6 +13,10 @@ final class NodeSelectionViewController: UIViewController, ViewHolder { var tableState: NodeSelectionTableState = .normal var locale = Locale.current + var loadableContentView: UIView { + rootView.tableView + } + init(presenter: NodeSelectionPresenterProtocol) { self.presenter = presenter super.init(nibName: nil, bundle: nil) @@ -67,10 +71,12 @@ final class NodeSelectionViewController: UIViewController, ViewHolder { rootView.bind(to: viewModel) rootView.tableView.reloadData() + didStopLoading() } } @objc private func automaticNodeSwitchChangedValue(_ sender: UISwitch) { + didStartLoading() presenter.didChangeValueForAutomaticNodeSwitch(isOn: sender.isOn) } @@ -168,6 +174,7 @@ extension NodeSelectionViewController: UITableViewDelegate, UITableViewDataSourc let cellViewModel = viewModel.sections[indexPath.section].viewModels[indexPath.row] + didStartLoading() presenter.didSelectNode(cellViewModel.node) } } diff --git a/fearless/Modules/NodeSelection/NodeSelectionViewFactory.swift b/fearless/Modules/NodeSelection/NodeSelectionViewFactory.swift index a446f169ce..bebae8c8b7 100644 --- a/fearless/Modules/NodeSelection/NodeSelectionViewFactory.swift +++ b/fearless/Modules/NodeSelection/NodeSelectionViewFactory.swift @@ -5,14 +5,13 @@ import SSFModels struct NodeSelectionViewFactory { static func createView(chain: ChainModel) -> NodeSelectionViewProtocol? { - let repository: CoreDataRepository = ChainRepositoryFactory().createRepository( + let repository = ChainRepositoryFactory().createAsyncRepository( sortDescriptors: [NSSortDescriptor.chainsByAddressPrefix] ) let interactor = NodeSelectionInteractor( chain: chain, - repository: AnyDataProviderRepository(repository), - operationManager: OperationManagerFacade.sharedManager, + repository: AsyncAnyRepository(repository), eventCenter: EventCenter.shared, chainRegistry: ChainRegistryFacade.sharedRegistry ) From e65baf6ce834780c0f4bf7c0e3392364a66278ce Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Mon, 17 Jun 2024 16:07:45 +0500 Subject: [PATCH 040/115] unused code has been removed --- fearless.xcodeproj/project.pbxproj | 14 -------- .../CoreComponents/QR/QRParser.swift | 32 ------------------- fearless/Modules/Send/SendAssembly.swift | 1 - fearless/Modules/Send/SendPresenter.swift | 3 -- 4 files changed, 50 deletions(-) delete mode 100644 fearless/CoreLayer/CoreComponents/QR/QRParser.swift diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index b63bbf43e7..ad41ee0eb0 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -2686,7 +2686,6 @@ FAAA294A2B8DCF350089AFE6 /* AsyncStorageRequestFactory+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAAA29472B8DCF350089AFE6 /* AsyncStorageRequestFactory+Extension.swift */; }; FAAA294B2B8DCF350089AFE6 /* AsyncStorageRequestFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAAA29482B8DCF350089AFE6 /* AsyncStorageRequestFactory.swift */; }; FAAA29572B8DED770089AFE6 /* MapKeyType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAAA29562B8DED770089AFE6 /* MapKeyType.swift */; }; - FAAB998D27A7C79D00CD1A3B /* QRParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAAB998C27A7C79D00CD1A3B /* QRParser.swift */; }; FAABC46E2845BADA002CF40E /* CustomValidatorParachainListFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAABC46D2845BADA002CF40E /* CustomValidatorParachainListFilter.swift */; }; FAABC4702845BAEE002CF40E /* CustomValidatorParachainListComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAABC46F2845BAEE002CF40E /* CustomValidatorParachainListComposer.swift */; }; FAABC4732845C7F4002CF40E /* ValidatorListFilterParachainViewModelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAABC4722845C7F4002CF40E /* ValidatorListFilterParachainViewModelState.swift */; }; @@ -5267,7 +5266,6 @@ FA4B928E284493C60003BCEF /* DelegateCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegateCall.swift; sourceTree = ""; }; FA4B92902844CF750003BCEF /* MetaAccountModelChangedEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetaAccountModelChangedEvent.swift; sourceTree = ""; }; FA4B92922844D0060003BCEF /* Currency.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Currency.swift; sourceTree = ""; }; - FA4B92942844D0100003BCEF /* UIImageView+Shimmered.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImageView+Shimmered.swift"; sourceTree = ""; }; FA4B92952844D0100003BCEF /* ShimmeredLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShimmeredLabel.swift; sourceTree = ""; }; FA4B92962844D0100003BCEF /* ShimmeredProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShimmeredProtocol.swift; sourceTree = ""; }; FA4B929A2844D0470003BCEF /* WalletDetailsTableHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletDetailsTableHeaderView.swift; sourceTree = ""; }; @@ -5737,7 +5735,6 @@ FAAA29472B8DCF350089AFE6 /* AsyncStorageRequestFactory+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AsyncStorageRequestFactory+Extension.swift"; sourceTree = ""; }; FAAA29482B8DCF350089AFE6 /* AsyncStorageRequestFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncStorageRequestFactory.swift; sourceTree = ""; }; FAAA29562B8DED770089AFE6 /* MapKeyType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapKeyType.swift; sourceTree = ""; }; - FAAB998C27A7C79D00CD1A3B /* QRParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRParser.swift; sourceTree = ""; }; FAABC46D2845BADA002CF40E /* CustomValidatorParachainListFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomValidatorParachainListFilter.swift; sourceTree = ""; }; FAABC46F2845BAEE002CF40E /* CustomValidatorParachainListComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomValidatorParachainListComposer.swift; sourceTree = ""; }; FAABC4722845C7F4002CF40E /* ValidatorListFilterParachainViewModelState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidatorListFilterParachainViewModelState.swift; sourceTree = ""; }; @@ -9596,7 +9593,6 @@ 07DE95CD28A16C7C00E9C2CB /* SearchTextField.swift */, FA4B92952844D0100003BCEF /* ShimmeredLabel.swift */, FA4B92962844D0100003BCEF /* ShimmeredProtocol.swift */, - FA4B92942844D0100003BCEF /* UIImageView+Shimmered.swift */, FA8644502768A13300956D8E /* TwoLabelView.swift */, FA38C988275DFB8D005C5577 /* NavigationBar */, FA2569B9274CE74000875A53 /* AttentionView.swift */, @@ -14376,19 +14372,10 @@ FA3067292B6246BD006A0BA5 /* Storage */, FAFB47D52ABD588D0008F8CA /* Repository */, FA7336C52A0E3B870096A291 /* Network */, - FAAB998B27A7C79000CD1A3B /* QR */, ); path = CoreComponents; sourceTree = ""; }; - FAAB998B27A7C79000CD1A3B /* QR */ = { - isa = PBXGroup; - children = ( - FAAB998C27A7C79D00CD1A3B /* QRParser.swift */, - ); - path = QR; - sourceTree = ""; - }; FAABC4712845C7E7002CF40E /* Parachain */ = { isa = PBXGroup; children = ( @@ -16611,7 +16598,6 @@ 84C6801A24D75E2A00006BF5 /* BorderedSubtitleActionView+Inspectable.swift in Sources */, FA256A34274CE7D600875A53 /* MoonbeamJSONEncoder.swift in Sources */, FA6A6DBD27B60A84007D1A20 /* ChainNodeModelMapper.swift in Sources */, - FAAB998D27A7C79D00CD1A3B /* QRParser.swift in Sources */, FA93A2DB28323CCD0021330F /* CustomValidatorListParachainStrategy.swift in Sources */, 8430AAD42602285B005B1066 /* StakingStateCommonData.swift in Sources */, F4F2296C260DBDCE00ACFDB8 /* StakingPayoutLabelTableCell.swift in Sources */, diff --git a/fearless/CoreLayer/CoreComponents/QR/QRParser.swift b/fearless/CoreLayer/CoreComponents/QR/QRParser.swift deleted file mode 100644 index 536a0fbfa1..0000000000 --- a/fearless/CoreLayer/CoreComponents/QR/QRParser.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation -import SSFQRService - -protocol QRParser { - func extractAddress(from code: String) throws -> String -} - -final class SubstrateQRParser: QRParser { - private let prefix: String = SubstrateQRConstants.prefix - private let separator: String = SubstrateQRConstants.fieldsSeparator - - func extractAddress(from code: String) throws -> String { - let fields = code - .components(separatedBy: separator) - - if fields.count == 1 { - return code - } - - guard fields.count >= 3, fields.count <= 4 else { - throw QRDecoderError.unexpectedNumberOfFields - } - - guard fields[0] == prefix else { - throw QRDecoderError.undefinedPrefix - } - - let address = fields[1] - - return address - } -} diff --git a/fearless/Modules/Send/SendAssembly.swift b/fearless/Modules/Send/SendAssembly.swift index 02e7f7072c..702afd2d09 100644 --- a/fearless/Modules/Send/SendAssembly.swift +++ b/fearless/Modules/Send/SendAssembly.swift @@ -80,7 +80,6 @@ final class SendAssembly { localizationManager: localizationManager, viewModelFactory: viewModelFactory, dataValidatingFactory: dataValidatingFactory, - qrParser: SubstrateQRParser(), logger: Logger.shared, wallet: wallet, initialData: initialData diff --git a/fearless/Modules/Send/SendPresenter.swift b/fearless/Modules/Send/SendPresenter.swift index 7871106898..eda5127cdd 100644 --- a/fearless/Modules/Send/SendPresenter.swift +++ b/fearless/Modules/Send/SendPresenter.swift @@ -24,7 +24,6 @@ final class SendPresenter { private let dataValidatingFactory: SendDataValidatingFactory private let logger: LoggerProtocol? private let wallet: MetaAccountModel - private let qrParser: QRParser private let viewModelFactory: SendViewModelFactoryProtocol private var initialData: SendFlowInitialData @@ -92,7 +91,6 @@ final class SendPresenter { localizationManager: LocalizationManagerProtocol, viewModelFactory: SendViewModelFactoryProtocol, dataValidatingFactory: SendDataValidatingFactory, - qrParser: QRParser, logger: LoggerProtocol? = nil, wallet: MetaAccountModel, initialData: SendFlowInitialData @@ -101,7 +99,6 @@ final class SendPresenter { self.router = router self.viewModelFactory = viewModelFactory self.dataValidatingFactory = dataValidatingFactory - self.qrParser = qrParser self.logger = logger self.wallet = wallet self.initialData = initialData From f80f123322623a9e9ca54b850480cbfdd7a5f60c Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Tue, 18 Jun 2024 16:15:59 +0500 Subject: [PATCH 041/115] [#FLW-4682] The balance on the asset does not updating --- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../WalletSendConfirmInteractor.swift | 103 ++++-------------- .../WalletSendConfirmPresenter.swift | 35 +++--- .../WalletSendConfirmProtocols.swift | 1 - .../WalletSendConfirmViewFactory.swift | 38 +------ fearless/Modules/Send/SendAssembly.swift | 8 +- .../Send/SendDependencyContainer.swift | 42 +------ fearless/Modules/Send/SendInteractor.swift | 72 +++--------- fearless/Modules/Send/SendPresenter.swift | 4 +- .../Modules/Send/SendViewController.swift | 9 ++ 10 files changed, 80 insertions(+), 234 deletions(-) diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index 23c50d586a..ecf69391a0 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -133,7 +133,7 @@ "location" : "https://github.com/soramitsu/shared-features-spm.git", "state" : { "branch" : "feature/xcm-routes", - "revision" : "f3737080f73453325fe436575b0f6c04d71e9e2a" + "revision" : "ad712ce34d995276f9cae269afad7902353bd878" } }, { diff --git a/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmInteractor.swift b/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmInteractor.swift index d29b9f9ddb..2f347eb04f 100644 --- a/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmInteractor.swift +++ b/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmInteractor.swift @@ -12,48 +12,32 @@ final class WalletSendConfirmInteractor: RuntimeConstantFetching { private let priceLocalSubscriber: PriceLocalStorageSubscriber private let selectedMetaAccount: MetaAccountModel - private let feeProxy: ExtrinsicFeeProxyProtocol private let accountInfoSubscriptionAdapter: AccountInfoSubscriptionAdapterProtocol - private let operationManager: OperationManagerProtocol private let call: SendConfirmTransferCall - private let signingWrapper: SigningWrapperProtocol private let chainAsset: ChainAsset private let wallet: MetaAccountModel private var equilibriumTotalBalanceService: EquilibriumTotalBalanceServiceProtocol? let dependencyContainer: SendDepencyContainer - private let runtimeItemRepository: AnyDataProviderRepository - private let operationQueue: OperationQueue private var balanceProvider: AnyDataProvider? private var priceProvider: AnySingleValueProvider<[PriceData]>? private var utilityPriceProvider: AnySingleValueProvider<[PriceData]>? - private var runtimeItemByChainId: [ChainModel.Id: RuntimeMetadataItem] = [:] init( selectedMetaAccount: MetaAccountModel, chainAsset: ChainAsset, call: SendConfirmTransferCall, - feeProxy: ExtrinsicFeeProxyProtocol, accountInfoSubscriptionAdapter: AccountInfoSubscriptionAdapterProtocol, priceLocalSubscriber: PriceLocalStorageSubscriber, - operationManager: OperationManagerProtocol, - signingWrapper: SigningWrapperProtocol, dependencyContainer: SendDepencyContainer, - wallet: MetaAccountModel, - runtimeItemRepository: AnyDataProviderRepository, - operationQueue: OperationQueue + wallet: MetaAccountModel ) { self.selectedMetaAccount = selectedMetaAccount self.chainAsset = chainAsset - self.feeProxy = feeProxy self.accountInfoSubscriptionAdapter = accountInfoSubscriptionAdapter self.priceLocalSubscriber = priceLocalSubscriber self.call = call - self.operationManager = operationManager - self.signingWrapper = signingWrapper self.dependencyContainer = dependencyContainer self.wallet = wallet - self.runtimeItemRepository = runtimeItemRepository - self.operationQueue = operationQueue } private func subscribeToAccountInfo() { @@ -71,83 +55,42 @@ final class WalletSendConfirmInteractor: RuntimeConstantFetching { private func subscribeToPrice() { priceProvider = priceLocalSubscriber.subscribeToPrice(for: chainAsset, listener: self) - if let utilityAsset = getFeePaymentChainAsset(for: chainAsset) { + if let utilityAsset = getFeePaymentChainAsset(for: chainAsset), utilityAsset != chainAsset { utilityPriceProvider = priceLocalSubscriber.subscribeToPrice(for: utilityAsset, listener: self) } } - private func fetchCurrentRuntimeItem() async throws -> RuntimeMetadataItem? { - if let item = runtimeItemByChainId[chainAsset.chain.chainId] { - return item - } - - let currentChainId = chainAsset.chain.chainId - - return try await withUnsafeThrowingContinuation { continuation in - let runtimeItemsOperation = runtimeItemRepository.fetchAllOperation(with: RepositoryFetchOptions()) - - runtimeItemsOperation.completionBlock = { [weak self] in + private func subscribeToFee() { + switch call { + case let .transfer(transfer): + Task { do { - let items = try runtimeItemsOperation.extractNoCancellableResultData() - self?.cache(runtimeItems: items) - - let currentRuntimeItem = items.first(where: { $0.chain == currentChainId }) - continuation.resume(returning: currentRuntimeItem) + let transferService = try await dependencyContainer.prepareDepencies(chainAsset: chainAsset).transferService + transferService.subscribeForFee(transfer: transfer, listener: self) } catch { - continuation.resume(throwing: error) + await MainActor.run { + presenter?.didReceiveFee(result: .failure(error)) + } } } - - operationQueue.addOperation(runtimeItemsOperation) - } - } - - private func cache(runtimeItems: [RuntimeMetadataItem]) { - runtimeItemByChainId = runtimeItems.reduce([ChainModel.Id: RuntimeMetadataItem]()) { partialResult, currentItem in - var result = partialResult - result[currentItem.chain] = currentItem - return result + case let .xorlessTransfer(xorlessTransfer): + break } } +} +extension WalletSendConfirmInteractor: WalletSendConfirmInteractorInputProtocol { func setup() { - feeProxy.delegate = self - subscribeToPrice() subscribeToAccountInfo() provideConstants() - } -} - -extension WalletSendConfirmInteractor: WalletSendConfirmInteractorInputProtocol { - func estimateFee() { - Task { - do { - let runtimeItem = try await fetchCurrentRuntimeItem() - let transferService = try await dependencyContainer.prepareDepencies(chainAsset: chainAsset, runtimeItem: runtimeItem).transferService - let fee: BigUInt - switch call { - case let .transfer(transfer): - fee = try await transferService.estimateFee(for: transfer) - case let .xorlessTransfer(transfer): - fee = try await transferService.estimateFee(for: transfer) - } - await MainActor.run { - presenter?.didReceiveFee(result: .success(RuntimeDispatchInfo(feeValue: fee))) - } - } catch { - await MainActor.run { - presenter?.didReceiveFee(result: .failure(error)) - } - } - } + subscribeToFee() } func submitExtrinsic() { Task { do { - let runtimeItem = try await fetchCurrentRuntimeItem() - let transferService = try await dependencyContainer.prepareDepencies(chainAsset: chainAsset, runtimeItem: runtimeItem).transferService + let transferService = try await dependencyContainer.prepareDepencies(chainAsset: chainAsset).transferService let txHash: String switch call { @@ -179,9 +122,8 @@ extension WalletSendConfirmInteractor: WalletSendConfirmInteractorInputProtocol func fetchEquilibriumTotalBalance(chainAsset: ChainAsset, amount: Decimal) { if chainAsset.chain.isEquilibrium { Task { - let runtimeItem = try await fetchCurrentRuntimeItem() let service = try await dependencyContainer - .prepareDepencies(chainAsset: chainAsset, runtimeItem: runtimeItem) + .prepareDepencies(chainAsset: chainAsset) .equilibruimTotalBalanceService equilibriumTotalBalanceService = service @@ -194,8 +136,7 @@ extension WalletSendConfirmInteractor: WalletSendConfirmInteractorInputProtocol func provideConstants() { Task { - let runtimeItem = try await fetchCurrentRuntimeItem() - let dependencies = try await dependencyContainer.prepareDepencies(chainAsset: chainAsset, runtimeItem: runtimeItem) + let dependencies = try await dependencyContainer.prepareDepencies(chainAsset: chainAsset) dependencies.existentialDepositService.fetchExistentialDeposit( chainAsset: chainAsset @@ -222,12 +163,6 @@ extension WalletSendConfirmInteractor: PriceLocalSubscriptionHandler { } } -extension WalletSendConfirmInteractor: ExtrinsicFeeProxyDelegate { - func didReceiveFee(result: Swift.Result, for _: ExtrinsicFeeId) { - presenter?.didReceiveFee(result: result) - } -} - extension WalletSendConfirmInteractor: TransferFeeEstimationListener { func didReceiveFee(fee: BigUInt) { DispatchQueue.main.async { [weak self] in diff --git a/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmPresenter.swift b/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmPresenter.swift index 3653f74aff..22d7821460 100644 --- a/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmPresenter.swift +++ b/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmPresenter.swift @@ -41,7 +41,7 @@ final class WalletSendConfirmPresenter { private let wallet: MetaAccountModel private let walletSendConfirmViewModelFactory: WalletSendConfirmViewModelFactoryProtocol private let scamInfo: ScamInfo? - private let feeViewModel: BalanceViewModelProtocol? + private var feeViewModel: BalanceViewModelProtocol? private var balance: Decimal? private var utilityBalance: Decimal? @@ -79,6 +79,10 @@ final class WalletSendConfirmPresenter { self.scamInfo = scamInfo self.feeViewModel = feeViewModel self.localizationManager = localizationManager + if let feeViewModel { + fee = Decimal(string: feeViewModel.amount) + } + loadingCollector.feeReady = feeViewModel != nil } private func provideViewModel() { @@ -91,7 +95,7 @@ final class WalletSendConfirmPresenter { assetBalanceViewModel: try await provideAssetVewModel(), tipRequired: chainAsset.chain.isTipRequired, tipViewModel: try await provideTipViewModel(), - feeViewModel: try await provideFeeViewModel(), + feeViewModel: feeViewModel, wallet: wallet, locale: selectedLocale, scamInfo: scamInfo, @@ -157,22 +161,18 @@ final class WalletSendConfirmPresenter { .value(for: selectedLocale) } - private func provideFeeViewModel() async throws -> BalanceViewModelProtocol? { + private func updateFeeViewModel() { guard let utilityAsset = interactor.getFeePaymentChainAsset(for: chainAsset), - let balanceViewModelFactory = buildBalanceViewModelFactory(wallet: wallet, for: utilityAsset), - feeViewModel == nil + let balanceViewModelFactory = buildBalanceViewModelFactory(wallet: wallet, for: utilityAsset) else { - fee = Decimal(string: feeViewModel?.amount ?? "") - return feeViewModel + return } - return fee + + let viewModel = fee .map { balanceViewModelFactory.balanceFromPrice($0, priceData: utilityPriceData, usageCase: .detailsCrypto) }? .value(for: selectedLocale) - } - - private func refreshFee() { - interactor.estimateFee() + feeViewModel = viewModel } private func buildBalanceViewModelFactory( @@ -201,9 +201,6 @@ final class WalletSendConfirmPresenter { .orml(balance: balance, utilityBalance: utilityBalance) : .utility(balance: balance) DataValidationRunner(validators: [ - dataValidatingFactory.has(fee: fee, locale: selectedLocale, onError: { [weak self] in - self?.refreshFee() - }), dataValidatingFactory.canPayFeeAndAmount( balanceType: balanceType, feeAndTip: (fee ?? 0) + tip, @@ -256,8 +253,6 @@ extension WalletSendConfirmPresenter: WalletSendConfirmPresenterProtocol { func setup() { interactor.setup() provideViewModel() - refreshFee() - loadingCollector.utilityBalanceReady = chainAsset.isUtility } @@ -351,6 +346,10 @@ extension WalletSendConfirmPresenter: WalletSendConfirmInteractorOutputProtocol case let .success(priceData): if chainAsset.asset.priceId == priceId { self.priceData = priceData + let utilityChainAsset = interactor.getFeePaymentChainAsset(for: chainAsset) + if utilityChainAsset?.chainAssetId == chainAsset.chainAssetId { + utilityPriceData = priceData + } } else { utilityPriceData = priceData } @@ -367,7 +366,7 @@ extension WalletSendConfirmPresenter: WalletSendConfirmInteractorOutputProtocol fee = BigUInt(string: dispatchInfo.fee).map { Decimal.fromSubstrateAmount($0, precision: Int16(utilityAsset.asset.precision)) } ?? nil - + updateFeeViewModel() provideViewModel() let amount = Decimal.fromSubstrateAmount(call.amount, precision: Int16(chainAsset.asset.precision)) ?? .zero let tipPaymentChainAsset = interactor.getFeePaymentChainAsset(for: chainAsset) diff --git a/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmProtocols.swift b/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmProtocols.swift index 3536c9ca57..e4a8fd9efa 100644 --- a/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmProtocols.swift +++ b/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmProtocols.swift @@ -19,7 +19,6 @@ protocol WalletSendConfirmInteractorInputProtocol: AnyObject { func setup() func submitExtrinsic() - func estimateFee() func getFeePaymentChainAsset(for chainAsset: ChainAsset?) -> ChainAsset? func fetchEquilibriumTotalBalance(chainAsset: ChainAsset, amount: Decimal) func provideConstants() diff --git a/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmViewFactory.swift b/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmViewFactory.swift index 7c426c1b74..91163627b0 100644 --- a/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmViewFactory.swift +++ b/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmViewFactory.swift @@ -52,13 +52,11 @@ enum WalletSendConfirmViewFactory { scamInfo: ScamInfo?, feeViewModel: BalanceViewModelProtocol? ) -> WalletSendConfirmViewProtocol? { - guard let interactor = createInteractor( + let interactor = createInteractor( wallet: wallet, chainAsset: chainAsset, call: call - ) else { - return nil - } + ) let wireframe = WalletSendConfirmWireframe() @@ -103,48 +101,24 @@ enum WalletSendConfirmViewFactory { wallet: MetaAccountModel, chainAsset: ChainAsset, call: SendConfirmTransferCall - ) -> WalletSendConfirmInteractor? { - guard let selectedMetaAccount = SelectedWalletSettings.shared.value else { - return nil - } - + ) -> WalletSendConfirmInteractor { let operationManager = OperationManagerFacade.sharedManager - - let feeProxy = ExtrinsicFeeProxy() let priceLocalSubscriber = PriceLocalStorageSubscriberImpl.shared - - guard let accountResponse = selectedMetaAccount.fetch(for: chainAsset.chain.accountRequest()) else { - return nil - } - - let keystore = Keychain() - let signingWrapper = SigningWrapper( - keystore: keystore, - metaId: selectedMetaAccount.metaId, - accountResponse: accountResponse - ) let dependencyContainer = SendDepencyContainer( wallet: wallet, operationManager: operationManager ) - let runtimeMetadataRepository: CoreDataRepository = - SubstrateDataStorageFacade.shared.createRepository() return WalletSendConfirmInteractor( - selectedMetaAccount: selectedMetaAccount, + selectedMetaAccount: wallet, chainAsset: chainAsset, call: call, - feeProxy: feeProxy, accountInfoSubscriptionAdapter: AccountInfoSubscriptionAdapter( walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, - selectedMetaAccount: selectedMetaAccount + selectedMetaAccount: wallet ), priceLocalSubscriber: priceLocalSubscriber, - operationManager: operationManager, - signingWrapper: signingWrapper, dependencyContainer: dependencyContainer, - wallet: wallet, - runtimeItemRepository: AnyDataProviderRepository(runtimeMetadataRepository), - operationQueue: OperationQueue() + wallet: wallet ) } } diff --git a/fearless/Modules/Send/SendAssembly.swift b/fearless/Modules/Send/SendAssembly.swift index 02e7f7072c..2c79160d45 100644 --- a/fearless/Modules/Send/SendAssembly.swift +++ b/fearless/Modules/Send/SendAssembly.swift @@ -53,10 +53,9 @@ final class SendAssembly { chainModelRepository: AnyDataProviderRepository(chainRepository), wallet: wallet ) - let runtimeMetadataRepository: CoreDataRepository = - SubstrateDataStorageFacade.shared.createRepository() + let runtimeMetadataRepository: AsyncCoreDataRepositoryDefault = + SubstrateDataStorageFacade.shared.createAsyncRepository() let interactor = SendInteractor( - feeProxy: ExtrinsicFeeProxy(), accountInfoSubscriptionAdapter: AccountInfoSubscriptionAdapter( walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, selectedMetaAccount: wallet @@ -67,8 +66,7 @@ final class SendAssembly { chainAssetFetching: chainAssetFetching, dependencyContainer: dependencyContainer, addressChainDefiner: addressChainDefiner, - runtimeItemRepository: AnyDataProviderRepository(runtimeMetadataRepository), - operationQueue: OperationQueue() + runtimeItemRepository: AsyncAnyRepository(runtimeMetadataRepository) ) let router = SendRouter() diff --git a/fearless/Modules/Send/SendDependencyContainer.swift b/fearless/Modules/Send/SendDependencyContainer.swift index e9359f129f..1053e5c923 100644 --- a/fearless/Modules/Send/SendDependencyContainer.swift +++ b/fearless/Modules/Send/SendDependencyContainer.swift @@ -34,10 +34,7 @@ final class SendDepencyContainer { self.operationManager = operationManager } - func prepareDepencies( - chainAsset: ChainAsset, - runtimeItem: RuntimeMetadataItem? - ) async throws -> SendDependencies { + func prepareDepencies(chainAsset: ChainAsset) async throws -> SendDependencies { guard let accountResponse = wallet.fetch(for: chainAsset.chain.accountRequest()) else { throw ChainAccountFetchingError.accountNotExists } @@ -60,7 +57,7 @@ final class SendDepencyContainer { let equilibruimTotalBalanceService = createEqTotalBalanceService(chainAsset: chainAsset) - let transferService = try await createTransferService(for: chainAsset, runtimeItem: runtimeItem) + let transferService = try await createTransferService(for: chainAsset) let polkaswapService = createPolkaswapService(chainAsset: chainAsset, chainRegistry: chainRegistry) let accountInfoFetching = createAccountInfoFetching(for: chainAsset) @@ -105,7 +102,7 @@ final class SendDepencyContainer { return substrateAccountInfoFetching } - private func createTransferService(for chainAsset: ChainAsset, runtimeItem: RuntimeMetadataItem?) async throws -> TransferServiceProtocol { + private func createTransferService(for chainAsset: ChainAsset) async throws -> TransferServiceProtocol { guard let accountResponse = wallet.fetch(for: chainAsset.chain.accountRequest()) else { @@ -118,42 +115,15 @@ final class SendDepencyContainer { throw ChainRegistryError.runtimeMetadaUnavailable } - let chainSyncService = SSFChainRegistry.ChainSyncService( - chainsUrl: ApplicationConfig.shared.chainsSourceUrl, - operationQueue: OperationQueue(), - dataFetchFactory: SSFNetwork.NetworkOperationFactory() - ) - - let chainsTypesSyncService = SSFChainRegistry.ChainsTypesSyncService( - url: ApplicationConfig.shared.chainTypesSourceUrl, - dataOperationFactory: SSFNetwork.NetworkOperationFactory(), - operationQueue: OperationQueue() - ) - - let runtimeSyncService = SSFChainRegistry.RuntimeSyncService(dataOperationFactory: NetworkOperationFactory()) - - let chainRegistry = SSFChainRegistry.ChainRegistry( - runtimeProviderPool: SSFChainRegistry.RuntimeProviderPool(), - connectionPool: SSFChainRegistry.ConnectionPool(), - chainSyncService: chainSyncService, - chainsTypesSyncService: chainsTypesSyncService, - runtimeSyncService: runtimeSyncService - ) - let connection = try await chainRegistry.getSubstrateConnection(for: chainAsset.chain) - - let runtimeService = try await chainRegistry.getRuntimeProvider( - chainId: chainAsset.chain.chainId, - usedRuntimePaths: [:], - runtimeItem: runtimeItem - ) - + let chainRegistry = ChainRegistryFacade.sharedRegistry + let connection = try chainRegistry.getSubstrateConnection(for: chainAsset.chain) let operationManager = OperationManagerFacade.sharedManager let extrinsicService = SSFExtrinsicKit.ExtrinsicService( accountId: accountResponse.accountId, chainFormat: chainAsset.chain.chainFormat.asSfCrypto(), cryptoType: accountResponse.cryptoType, - runtimeRegistry: runtimeService, + runtimeRegistry: nativeRuntimeService, engine: connection, operationManager: operationManager ) diff --git a/fearless/Modules/Send/SendInteractor.swift b/fearless/Modules/Send/SendInteractor.swift index 1145364655..36ffc5bb54 100644 --- a/fearless/Modules/Send/SendInteractor.swift +++ b/fearless/Modules/Send/SendInteractor.swift @@ -10,15 +10,13 @@ final class SendInteractor: RuntimeConstantFetching { private weak var output: SendInteractorOutput? private let priceLocalSubscriber: PriceLocalStorageSubscriber - private let feeProxy: ExtrinsicFeeProxyProtocol private let accountInfoSubscriptionAdapter: AccountInfoSubscriptionAdapterProtocol private let operationManager: OperationManagerProtocol private let scamServiceOperationFactory: ScamServiceOperationFactoryProtocol private let chainAssetFetching: ChainAssetFetchingProtocol private let addressChainDefiner: AddressChainDefiner private var equilibriumTotalBalanceService: EquilibriumTotalBalanceServiceProtocol? - private let runtimeItemRepository: AnyDataProviderRepository - private let operationQueue: OperationQueue + private let runtimeItemRepository: AsyncAnyRepository let dependencyContainer: SendDepencyContainer @@ -30,7 +28,6 @@ final class SendInteractor: RuntimeConstantFetching { private var runtimeItemByChainId: [ChainModel.Id: RuntimeMetadataItem] = [:] init( - feeProxy: ExtrinsicFeeProxyProtocol, accountInfoSubscriptionAdapter: AccountInfoSubscriptionAdapterProtocol, priceLocalSubscriber: PriceLocalStorageSubscriber, operationManager: OperationManagerProtocol, @@ -38,10 +35,8 @@ final class SendInteractor: RuntimeConstantFetching { chainAssetFetching: ChainAssetFetchingProtocol, dependencyContainer: SendDepencyContainer, addressChainDefiner: AddressChainDefiner, - runtimeItemRepository: AnyDataProviderRepository, - operationQueue: OperationQueue + runtimeItemRepository: AsyncAnyRepository ) { - self.feeProxy = feeProxy self.accountInfoSubscriptionAdapter = accountInfoSubscriptionAdapter self.priceLocalSubscriber = priceLocalSubscriber self.operationManager = operationManager @@ -50,7 +45,6 @@ final class SendInteractor: RuntimeConstantFetching { self.dependencyContainer = dependencyContainer self.addressChainDefiner = addressChainDefiner self.runtimeItemRepository = runtimeItemRepository - self.operationQueue = operationQueue } // MARK: - Private methods @@ -89,24 +83,10 @@ final class SendInteractor: RuntimeConstantFetching { } let currentChainId = currentChainAsset.chain.chainId - - return try await withUnsafeThrowingContinuation { continuation in - let runtimeItemsOperation = runtimeItemRepository.fetchAllOperation(with: RepositoryFetchOptions()) - - runtimeItemsOperation.completionBlock = { [weak self] in - do { - let items = try runtimeItemsOperation.extractNoCancellableResultData() - self?.cache(runtimeItems: items) - - let currentRuntimeItem = items.first(where: { $0.chain == currentChainId }) - continuation.resume(returning: currentRuntimeItem) - } catch { - continuation.resume(throwing: error) - } - } - - operationQueue.addOperation(runtimeItemsOperation) - } + let items = try await runtimeItemRepository.fetchAll() + cache(runtimeItems: items) + let currentRuntimeItem = items.first(where: { $0.chain == currentChainId }) + return currentRuntimeItem } private func cache(runtimeItems: [RuntimeMetadataItem]) { @@ -119,8 +99,7 @@ final class SendInteractor: RuntimeConstantFetching { private func updateDependencies(for chainAsset: ChainAsset) { Task { - let runtimeItem = try await fetchCurrentRuntimeItem(currentChainAsset: chainAsset) - let dependencies = try await dependencyContainer.prepareDepencies(chainAsset: chainAsset, runtimeItem: runtimeItem) + let dependencies = try await dependencyContainer.prepareDepencies(chainAsset: chainAsset) self.dependencies = dependencies getTokensStatus(for: chainAsset) @@ -170,7 +149,6 @@ final class SendInteractor: RuntimeConstantFetching { extension SendInteractor: SendInteractorInput { func setup(with output: SendInteractorOutput) { self.output = output - feeProxy.delegate = self } func updateSubscriptions(for chainAsset: ChainAsset) { @@ -205,31 +183,17 @@ extension SendInteractor: SendInteractorInput { return } - Task { - do { - let address = address ?? senderAddress - let appId: BigUInt? = chainAsset.chain.options?.contains(.checkAppId) == true ? .zero : nil - let transfer = Transfer( - chainAsset: chainAsset, - amount: amount, - receiver: address, - tip: tip, - appId: appId - ) - - let fee = try await dependencies.transferService.estimateFee(for: transfer) - - await MainActor.run(body: { - output?.didReceiveFee(result: .success(RuntimeDispatchInfo(feeValue: fee))) - }) - - dependencies.transferService.subscribeForFee(transfer: transfer, listener: self) - } catch { - await MainActor.run(body: { - output?.didReceiveFee(result: .failure(error)) - }) - } - } + let address = address ?? senderAddress + let appId: BigUInt? = chainAsset.chain.options?.contains(.checkAppId) == true ? .zero : nil + let transfer = Transfer( + chainAsset: chainAsset, + amount: amount, + receiver: address, + tip: tip, + appId: appId + ) + + dependencies.transferService.subscribeForFee(transfer: transfer, listener: self) } func fetchScamInfo(for address: String) { diff --git a/fearless/Modules/Send/SendPresenter.swift b/fearless/Modules/Send/SendPresenter.swift index ec631dea0d..d1256dbb56 100644 --- a/fearless/Modules/Send/SendPresenter.swift +++ b/fearless/Modules/Send/SendPresenter.swift @@ -577,7 +577,7 @@ final class SendPresenter { chainAsset: chainAsset, call: .transfer(transfer), scamInfo: strongSelf.scamInfo, - feeViewModel: nil + feeViewModel: strongSelf.feeViewModel ) } } @@ -888,7 +888,6 @@ extension SendPresenter: SendViewOutput { self?.inputResult = .rate(Decimal(Double(percentage))) self?.provideAssetVewModel() self?.provideInputViewModel() - self?.refreshFee(for: chainAsset, address: self?.recipientAddress) }) ) } @@ -907,7 +906,6 @@ extension SendPresenter: SendViewOutput { chainAsset: chainAsset, validationCase: .validateAmount(completionHandler: { [weak self] in self?.provideAssetVewModel() - self?.refreshFee(for: chainAsset, address: self?.recipientAddress) }) ) } diff --git a/fearless/Modules/Send/SendViewController.swift b/fearless/Modules/Send/SendViewController.swift index 6fcc1e3c07..7146b1126c 100644 --- a/fearless/Modules/Send/SendViewController.swift +++ b/fearless/Modules/Send/SendViewController.swift @@ -297,6 +297,15 @@ extension SendViewController: AmountInputViewModelObserver { func amountInputDidChange() { rootView.amountView.inputFieldText = amountInputViewModel?.displayAmount + NSObject.cancelPreviousPerformRequests( + withTarget: self, + selector: #selector(updateAmount), + object: nil + ) + perform(#selector(updateAmount), with: nil, afterDelay: 0.75) + } + + @objc private func updateAmount() { let amount = amountInputViewModel?.decimalAmount ?? 0.0 output.updateAmount(amount) } From 36300516a60e677990fc6c81f3ff29160ebbeb1e Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Wed, 19 Jun 2024 10:34:57 +0500 Subject: [PATCH 042/115] [#FLW-4699] Correct Moonpay onramp provider links and logic --- fearless/Common/PurchaseProvider/MoonpayProvider.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fearless/Common/PurchaseProvider/MoonpayProvider.swift b/fearless/Common/PurchaseProvider/MoonpayProvider.swift index c6ebeaed31..2ecd88653f 100644 --- a/fearless/Common/PurchaseProvider/MoonpayProvider.swift +++ b/fearless/Common/PurchaseProvider/MoonpayProvider.swift @@ -33,7 +33,7 @@ final class MoonpayProvider: PurchaseProviderProtocol { } func buildPurchaseActions(asset: AssetModel, address: String) -> [PurchaseAction] { - if let url = buildURLForToken(asset.id, address: address) { + if let url = buildURLForToken(asset.symbol, address: address) { return [PurchaseAction(title: Constants.title, url: url, icon: Constants.icon!)] } return [] From 7dbd38c7a30217edc61f68a1a281a27f9333a5dd Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Wed, 19 Jun 2024 11:00:49 +0500 Subject: [PATCH 043/115] [#FLW-4699] Correct Moonpay onramp provider links and logic --- .../PurchaseProvider/PurchaseAggregator+Default.swift | 6 +++--- .../NewWallet/ChainAccount/ChainAccountPresenter.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fearless/Common/PurchaseProvider/PurchaseAggregator+Default.swift b/fearless/Common/PurchaseProvider/PurchaseAggregator+Default.swift index 4cd3c7b41c..cf3a21610c 100644 --- a/fearless/Common/PurchaseProvider/PurchaseAggregator+Default.swift +++ b/fearless/Common/PurchaseProvider/PurchaseAggregator+Default.swift @@ -1,19 +1,19 @@ import Foundation extension PurchaseAggregator { - static func defaultAggregator() -> PurchaseAggregator { + static func defaultAggregator(with purchaseProviders: [PurchaseProviderProtocol]?) -> PurchaseAggregator { let config: ApplicationConfigProtocol = ApplicationConfig.shared let moonpaySecretKeyData = Data(MoonPayKeys.secretKey.utf8) - let purchaseProviders: [PurchaseProviderProtocol] = [ + let defaultProviders: [PurchaseProviderProtocol] = [ RampProvider(), MoonpayProviderFactory().createProvider( with: moonpaySecretKeyData, apiKey: config.moonPayApiKey ) ] - return PurchaseAggregator(providers: purchaseProviders) + return PurchaseAggregator(providers: purchaseProviders ?? defaultProviders) .with(appName: config.purchaseAppName) .with(logoUrl: config.logoURL) .with(colorCode: R.color.colorPink()!.hexRGB) diff --git a/fearless/Modules/NewWallet/ChainAccount/ChainAccountPresenter.swift b/fearless/Modules/NewWallet/ChainAccount/ChainAccountPresenter.swift index d4e39233a2..d0895b57bb 100644 --- a/fearless/Modules/NewWallet/ChainAccount/ChainAccountPresenter.swift +++ b/fearless/Modules/NewWallet/ChainAccount/ChainAccountPresenter.swift @@ -132,7 +132,7 @@ final class ChainAccountPresenter { } } - let providersAggregator = PurchaseAggregator(providers: availableProviders) + let providersAggregator = PurchaseAggregator.defaultAggregator(with: availableProviders) actions = providersAggregator.buildPurchaseActions(asset: chainAsset.asset, address: address) } return actions From f6dd997266837a968945be04d028fe22fa71c33b Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Wed, 19 Jun 2024 15:34:28 +0500 Subject: [PATCH 044/115] Sync chains.json when did become active --- .../Common/Helpers/WalletAssetsObserver.swift | 68 +++++++++++++------ .../ChainRegistry/ChainRegistryFactory.swift | 4 +- .../ChainRegistry/ChainSyncService.swift | 53 +++++++++++---- .../AccountInfoUpdatingService.swift | 6 +- 4 files changed, 93 insertions(+), 38 deletions(-) diff --git a/fearless/Common/Helpers/WalletAssetsObserver.swift b/fearless/Common/Helpers/WalletAssetsObserver.swift index d207f9a8a7..efa70c1c0a 100644 --- a/fearless/Common/Helpers/WalletAssetsObserver.swift +++ b/fearless/Common/Helpers/WalletAssetsObserver.swift @@ -67,14 +67,28 @@ final class WalletAssetsObserverImpl: WalletAssetsObserver { // MARK: - ApplicationServiceProtocol func setup() { - guard wallet.assetsVisibility.isEmpty else { - return - } chainRegistry.chainsSubscribe( self, runningInQueue: walletAssetsObserverQueue ) { [weak self] changes in - self?.handleChains(changes: changes, accounts: nil) + guard self?.wallet.assetsVisibility.isEmpty == true else { + let updateChanges = changes.filter { + switch $0 { + case .update: return true + default: return false + } + } + self?.handleChains(changes: updateChanges, accounts: nil) + return + } + + let insertChanges = changes.filter { + switch $0 { + case .insert: return true + default: return false + } + } + self?.handleChains(changes: insertChanges, accounts: nil) } } @@ -90,27 +104,14 @@ final class WalletAssetsObserverImpl: WalletAssetsObserver { let result = await withTaskGroup( of: (ChainModel, [ChainAssetId: AccountInfo?]).self, returning: [ChainModel: [ChainAssetId: AccountInfo?]].self, - body: { [wallet] group in + body: { group in changes.forEach { change in switch change { case let .insert(chain): - guard !chain.disabled else { - return - } - if let accounts { - if accounts.contains(where: { $0.chainId == chain.chainId }) { - group.addTask { - await self.fetchAccountInfos(chain: chain, wallet: wallet) - } - } else { - return - } - } else { - group.addTask { - await self.fetchAccountInfos(chain: chain, wallet: wallet) - } - } - case .update, .delete: + self.handleChange(for: chain, accounts: accounts, group: &group) + case let .update(chain): + self.handleChange(for: chain, accounts: accounts, group: &group) + case .delete: break } } @@ -130,6 +131,29 @@ final class WalletAssetsObserverImpl: WalletAssetsObserver { } } + private func handleChange( + for chain: ChainModel, + accounts: [ChainAccountModel]?, + group: inout TaskGroup<(ChainModel, [ChainAssetId: AccountInfo?])> + ) { + guard !chain.disabled else { + return + } + if let accounts { + if accounts.contains(where: { $0.chainId == chain.chainId }) { + group.addTask { [wallet] in + await self.fetchAccountInfos(chain: chain, wallet: wallet) + } + } else { + return + } + } else { + group.addTask { [wallet] in + await self.fetchAccountInfos(chain: chain, wallet: wallet) + } + } + } + private func updateVisibility(for chains: [ChainModel]) async -> MetaAccountModel { let result = await withTaskGroup( of: (ChainModel, [ChainAssetId: AccountInfo?])?.self, diff --git a/fearless/Common/Services/ChainRegistry/ChainRegistryFactory.swift b/fearless/Common/Services/ChainRegistry/ChainRegistryFactory.swift index 198034c4d0..227b385e33 100644 --- a/fearless/Common/Services/ChainRegistry/ChainRegistryFactory.swift +++ b/fearless/Common/Services/ChainRegistry/ChainRegistryFactory.swift @@ -1,4 +1,5 @@ import Foundation +import SoraFoundation import RobinHood import SSFModels import SSFNetwork @@ -82,7 +83,8 @@ final class ChainRegistryFactory { repository: AnyDataProviderRepository(chainRepository), eventCenter: EventCenter.shared, operationQueue: OperationManagerFacade.syncQueue, - logger: Logger.shared + logger: Logger.shared, + applicationHandler: ApplicationHandler() ) let specVersionSubscriptionFactory = SpecVersionSubscriptionFactory( diff --git a/fearless/Common/Services/ChainRegistry/ChainSyncService.swift b/fearless/Common/Services/ChainRegistry/ChainSyncService.swift index b6d2e8e33b..a3b389cd04 100644 --- a/fearless/Common/Services/ChainRegistry/ChainSyncService.swift +++ b/fearless/Common/Services/ChainRegistry/ChainSyncService.swift @@ -1,4 +1,5 @@ import Foundation +import SoraFoundation import RobinHood import SSFUtils import SSFModels @@ -24,12 +25,14 @@ final class ChainSyncService { private let repository: AnyDataProviderRepository private let eventCenter: EventCenterProtocol private let retryStrategy: ReconnectionStrategyProtocol + private let applicationHandler: ApplicationHandlerProtocol private let operationQueue: OperationQueue private let logger: LoggerProtocol? private var retryAttempt: Int = 0 private var isSyncing: Bool = false private let mutex = NSLock() + private var timer = CountdownTimer(notificationInterval: 300) private lazy var scheduler = Scheduler(with: self, callbackQueue: DispatchQueue.global()) @@ -39,7 +42,8 @@ final class ChainSyncService { eventCenter: EventCenterProtocol, operationQueue: OperationQueue, retryStrategy: ReconnectionStrategyProtocol = ExponentialReconnection(), - logger: LoggerProtocol? = nil + logger: LoggerProtocol? = nil, + applicationHandler: ApplicationHandlerProtocol ) { self.syncService = syncService self.repository = repository @@ -47,6 +51,8 @@ final class ChainSyncService { self.operationQueue = operationQueue self.retryStrategy = retryStrategy self.logger = logger + self.applicationHandler = applicationHandler + timer.delegate = self } private func performSyncUpIfNeeded() { @@ -55,7 +61,9 @@ final class ChainSyncService { return } - isSyncing = true + DispatchQueue.main.sync { + self.timer.start(with: 300) + } retryAttempt += 1 logger?.debug("Will start chain sync with attempt \(retryAttempt)") @@ -66,6 +74,13 @@ final class ChainSyncService { executeSync() } + private func setApplicationDelegateIfNeeded() { + guard applicationHandler.delegate == nil else { + return + } + applicationHandler.delegate = self + } + private func executeSync() { if Self.fetchLocalData { do { @@ -186,9 +201,7 @@ final class ChainSyncService { return try JSONDecoder().decode([ChainModel].self, from: data) } - private func complete(result: Result?) { - isSyncing = false - + private func complete(result: Result) { switch result { case let .success(changes): if changes.newOrUpdatedItems.isNotEmpty { @@ -221,17 +234,10 @@ final class ChainSyncService { eventCenter.notify(with: event) case let .failure(error): logger?.error("Sync failed with error: \(error)") - + timer.stop() let event = ChainSyncDidFail(error: error) eventCenter.notify(with: event) - retry() - case .none: - logger?.error("Sync failed with no result") - - let event = ChainSyncDidFail(error: BaseOperationError.unexpectedDependentResult) - eventCenter.notify(with: event) - retry() } } @@ -257,6 +263,7 @@ extension ChainSyncService: ChainSyncServiceProtocol { scheduler.cancel() } + setApplicationDelegateIfNeeded() performSyncUpIfNeeded() } } @@ -272,3 +279,23 @@ extension ChainSyncService: SchedulerDelegate { performSyncUpIfNeeded() } } + +extension ChainSyncService: CountdownTimerDelegate { + func didStart(with interval: TimeInterval) { + isSyncing = true + print("CountdownTimerDelegate didStart", interval) + } + + func didCountdown(remainedInterval _: TimeInterval) {} + + func didStop(with remainedInterval: TimeInterval) { + isSyncing = false + print("CountdownTimerDelegate didStop", remainedInterval) + } +} + +extension ChainSyncService: ApplicationHandlerDelegate { + func didReceiveDidBecomeActive(notification _: Notification) { + performSyncUpIfNeeded() + } +} diff --git a/fearless/Common/Services/RemoteSubscription/AccountInfoUpdatingService.swift b/fearless/Common/Services/RemoteSubscription/AccountInfoUpdatingService.swift index 3d76591cf5..70ec72604b 100644 --- a/fearless/Common/Services/RemoteSubscription/AccountInfoUpdatingService.swift +++ b/fearless/Common/Services/RemoteSubscription/AccountInfoUpdatingService.swift @@ -79,8 +79,10 @@ final class AccountInfoUpdatingService { } else { chains[newItem.chainId] = newItem } - case .update: - break + case let .update(chain): + chain.chainAssets.forEach { + addSubscriptionIfNeeded(for: $0) + } case let .delete(deletedIdentifier): removeSubscription(for: deletedIdentifier) } From 038c7732be73fca6a516029f675108e597c6ecfb Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Wed, 19 Jun 2024 17:38:31 +0500 Subject: [PATCH 045/115] =?UTF-8?q?[#FLW-4683]=20We=20can=E2=80=99t=20read?= =?UTF-8?q?=20ETH=20QR=20with=20amount=20in=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ChainRegistry/MetaAccountModel.swift | 4 +++ .../ChainRegistry/ChainSyncService.swift | 2 +- fearless/Modules/Send/SendPresenter.swift | 27 ++++++++++++------- fearless/Modules/Send/SendProtocols.swift | 4 +++ fearless/Modules/Send/SendRouter.swift | 13 +++++++++ fearless/en.lproj/Localizable.strings | 2 ++ fearless/id.lproj/Localizable.strings | 2 ++ fearless/ja.lproj/Localizable.strings | 2 ++ fearless/pt-PT.lproj/Localizable.strings | 2 ++ fearless/ru.lproj/Localizable.strings | 2 ++ fearless/tr.lproj/Localizable.strings | 2 ++ fearless/vi.lproj/Localizable.strings | 2 ++ fearless/zh-Hans.lproj/Localizable.strings | 2 ++ 13 files changed, 56 insertions(+), 10 deletions(-) diff --git a/fearless/Common/Model/ChainRegistry/MetaAccountModel.swift b/fearless/Common/Model/ChainRegistry/MetaAccountModel.swift index 2a8ae48abf..c66040fa1d 100644 --- a/fearless/Common/Model/ChainRegistry/MetaAccountModel.swift +++ b/fearless/Common/Model/ChainRegistry/MetaAccountModel.swift @@ -34,6 +34,10 @@ extension MetaAccountModel: Identifiable { } extension MetaAccountModel { + func isVisible(chainAsset: ChainAsset) -> Bool { + assetsVisibility.first(where: { $0.assetId == chainAsset.identifier })?.hidden == false + } + func insertingChainAccount(_ newChainAccount: ChainAccountModel) -> MetaAccountModel { var newChainAccounts = chainAccounts.filter { $0.chainId != newChainAccount.chainId diff --git a/fearless/Common/Services/ChainRegistry/ChainSyncService.swift b/fearless/Common/Services/ChainRegistry/ChainSyncService.swift index a3b389cd04..1f241670da 100644 --- a/fearless/Common/Services/ChainRegistry/ChainSyncService.swift +++ b/fearless/Common/Services/ChainRegistry/ChainSyncService.swift @@ -61,7 +61,7 @@ final class ChainSyncService { return } - DispatchQueue.main.sync { + DispatchQueue.main.async { self.timer.start(with: 300) } retryAttempt += 1 diff --git a/fearless/Modules/Send/SendPresenter.swift b/fearless/Modules/Send/SendPresenter.swift index e328070bf3..cd25c99328 100644 --- a/fearless/Modules/Send/SendPresenter.swift +++ b/fearless/Modules/Send/SendPresenter.swift @@ -411,10 +411,17 @@ final class SendPresenter { ) { [weak self] in self?.router.dismiss(view: self?.view) } + let assetManagementAction = SheetAlertPresentableAction( + title: R.string.localizable.walletManageAssets(preferredLanguages: selectedLocale.rLanguages), + style: .pinkBackgroundWhiteText + ) { [weak self] in + guard let self else { return } + self.router.showManageAsset(from: self.view, wallet: self.wallet) + } let alertViewModel = SheetAlertPresentableViewModel( - title: R.string.localizable.commonWarning(preferredLanguages: selectedLocale.rLanguages), - message: R.string.localizable.errorUnsupportedAsset(preferredLanguages: selectedLocale.rLanguages), - actions: [dissmissAction], + title: R.string.localizable.commonActionReceive(preferredLanguages: selectedLocale.rLanguages), + message: R.string.localizable.errorScanQrDisabledAsset(preferredLanguages: selectedLocale.rLanguages), + actions: [assetManagementAction, dissmissAction], closeAction: nil, dismissCompletion: { [weak self] in self?.router.dismiss(view: self?.view) @@ -764,6 +771,12 @@ final class SendPresenter { if let qrAmount = Decimal(string: qrInfo.amount ?? "") { inputResult = .absolute(qrAmount) } + guard let chainAsset, wallet.isVisible(chainAsset: chainAsset) else { + await MainActor.run { + showUnsupportedAssetAlert() + } + return + } let viewModel = viewModelFactory.buildRecipientViewModel( address: qrInfo.address, @@ -771,15 +784,11 @@ final class SendPresenter { canEditing: false ) - if let qrChainAsset = chainAsset { - interactor.updateSubscriptions(for: qrChainAsset) - } + interactor.updateSubscriptions(for: chainAsset) await MainActor.run { view?.didReceive(viewModel: viewModel) provideInputViewModel() - if let qrChainAsset = chainAsset { - provideNetworkViewModel(for: qrChainAsset.chain, canEdit: true) - } + provideNetworkViewModel(for: chainAsset.chain, canEdit: true) } } } diff --git a/fearless/Modules/Send/SendProtocols.swift b/fearless/Modules/Send/SendProtocols.swift index 8aa401153b..30cb15a880 100644 --- a/fearless/Modules/Send/SendProtocols.swift +++ b/fearless/Modules/Send/SendProtocols.swift @@ -108,6 +108,10 @@ protocol SendRouterInput: SheetAlertPresentable, ErrorPresentable, BaseErrorPres chainAssets: [ChainAsset]?, output: SelectAssetModuleOutput ) + func showManageAsset( + from view: ControllerBackedProtocol?, + wallet: MetaAccountModel + ) } protocol SendModuleInput: AnyObject {} diff --git a/fearless/Modules/Send/SendRouter.swift b/fearless/Modules/Send/SendRouter.swift index 6048d1009f..6c09574ee0 100644 --- a/fearless/Modules/Send/SendRouter.swift +++ b/fearless/Modules/Send/SendRouter.swift @@ -99,4 +99,17 @@ final class SendRouter: SendRouterInput { } view?.controller.present(module.view.controller, animated: true) } + + func showManageAsset( + from view: ControllerBackedProtocol?, + wallet: MetaAccountModel + ) { + let module = AssetManagementAssembly.configureModule(networkFilter: nil, wallet: wallet) + + guard let controller = module?.view.controller else { + return + } + + view?.controller.present(controller, animated: true) + } } diff --git a/fearless/en.lproj/Localizable.strings b/fearless/en.lproj/Localizable.strings index 2948bdd748..0f43c872a9 100644 --- a/fearless/en.lproj/Localizable.strings +++ b/fearless/en.lproj/Localizable.strings @@ -1272,3 +1272,5 @@ To find out more, contact %@"; "nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; "wallet.all.assets.hidden" = "You have hidden all assets"; "xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; +"common.activation.required" = "Activation Required"; +"error.scan.qr.disabled.asset" = "The asset you want to transfer is either unsupported or turned off. Go to Asset Management, activate the asset, and then scan the QR code again."; diff --git a/fearless/id.lproj/Localizable.strings b/fearless/id.lproj/Localizable.strings index 8b24f35b52..54662c6d7e 100644 --- a/fearless/id.lproj/Localizable.strings +++ b/fearless/id.lproj/Localizable.strings @@ -1256,3 +1256,5 @@ To find out more, contact %@"; "nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; "wallet.all.assets.hidden" = "You have hidden all assets"; "xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; +"common.activation.required" = "Activation Required"; +"error.scan.qr.disabled.asset" = "The asset you want to transfer is either unsupported or turned off. Go to Asset Management, activate the asset, and then scan the QR code again."; diff --git a/fearless/ja.lproj/Localizable.strings b/fearless/ja.lproj/Localizable.strings index 8927551d50..a835e396af 100644 --- a/fearless/ja.lproj/Localizable.strings +++ b/fearless/ja.lproj/Localizable.strings @@ -1141,3 +1141,5 @@ "nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; "wallet.all.assets.hidden" = "You have hidden all assets"; "xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; +"common.activation.required" = "Activation Required"; +"error.scan.qr.disabled.asset" = "The asset you want to transfer is either unsupported or turned off. Go to Asset Management, activate the asset, and then scan the QR code again."; diff --git a/fearless/pt-PT.lproj/Localizable.strings b/fearless/pt-PT.lproj/Localizable.strings index 6032be88fa..ecb00d5aef 100644 --- a/fearless/pt-PT.lproj/Localizable.strings +++ b/fearless/pt-PT.lproj/Localizable.strings @@ -1277,3 +1277,5 @@ To find out more, contact %@"; "nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; "wallet.all.assets.hidden" = "You have hidden all assets"; "xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; +"common.activation.required" = "Activation Required"; +"error.scan.qr.disabled.asset" = "The asset you want to transfer is either unsupported or turned off. Go to Asset Management, activate the asset, and then scan the QR code again."; diff --git a/fearless/ru.lproj/Localizable.strings b/fearless/ru.lproj/Localizable.strings index e37d11b9e5..cb77b15e5e 100644 --- a/fearless/ru.lproj/Localizable.strings +++ b/fearless/ru.lproj/Localizable.strings @@ -1270,3 +1270,5 @@ Euro cash"; "nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; "wallet.all.assets.hidden" = "You have hidden all assets"; "xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; +"common.activation.required" = "Activation Required"; +"error.scan.qr.disabled.asset" = "The asset you want to transfer is either unsupported or turned off. Go to Asset Management, activate the asset, and then scan the QR code again."; diff --git a/fearless/tr.lproj/Localizable.strings b/fearless/tr.lproj/Localizable.strings index 153d24e036..ec57ba8436 100644 --- a/fearless/tr.lproj/Localizable.strings +++ b/fearless/tr.lproj/Localizable.strings @@ -1145,3 +1145,5 @@ ait olduğundan emin olun."; "nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; "wallet.all.assets.hidden" = "You have hidden all assets"; "xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; +"common.activation.required" = "Activation Required"; +"error.scan.qr.disabled.asset" = "The asset you want to transfer is either unsupported or turned off. Go to Asset Management, activate the asset, and then scan the QR code again."; diff --git a/fearless/vi.lproj/Localizable.strings b/fearless/vi.lproj/Localizable.strings index e746b4dd57..19add718db 100644 --- a/fearless/vi.lproj/Localizable.strings +++ b/fearless/vi.lproj/Localizable.strings @@ -1270,3 +1270,5 @@ Giữ, stake hoặc cung cấp thanh khoản cho XOR trị giá ít nhất €%@ "nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; "wallet.all.assets.hidden" = "You have hidden all assets"; "xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; +"common.activation.required" = "Activation Required"; +"error.scan.qr.disabled.asset" = "The asset you want to transfer is either unsupported or turned off. Go to Asset Management, activate the asset, and then scan the QR code again."; diff --git a/fearless/zh-Hans.lproj/Localizable.strings b/fearless/zh-Hans.lproj/Localizable.strings index 61655f1d71..2862c83485 100644 --- a/fearless/zh-Hans.lproj/Localizable.strings +++ b/fearless/zh-Hans.lproj/Localizable.strings @@ -1268,3 +1268,5 @@ "nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; "wallet.all.assets.hidden" = "You have hidden all assets"; "xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; +"common.activation.required" = "Activation Required"; +"error.scan.qr.disabled.asset" = "The asset you want to transfer is either unsupported or turned off. Go to Asset Management, activate the asset, and then scan the QR code again."; From 095d5392d27391ffde2f3cef19d5dc86e3f0ea21 Mon Sep 17 00:00:00 2001 From: Alex Lebedko Date: Thu, 20 Jun 2024 14:27:06 +0700 Subject: [PATCH 046/115] FLW-4696, FLW-4697, FLW-4698, FLW-4700, FLW-4701, FLW-4702, FLW-4703, FLW-4704, FLW-4705, FLW-4706, FLW-4707, FLW-4708, FLW-4709, FLW-4710, FLW-4711, FLW-4712 --- fearless.xcodeproj/project.pbxproj | 4 + .../View/NavigationBar/Base/BaseTopBar.swift | 2 +- fearless/Common/View/UIFactory.swift | 41 +++++++++ .../Common/LiquidityPoolsConstants.swift | 6 ++ .../LiquidityPoolDetailsViewLayout.swift | 19 ++++- .../LiquidityPoolDetailsViewModel.swift | 1 + ...LiquidityPoolDetailsViewModelFactory.swift | 3 +- ...iquidityPoolRemoveLiquidityPresenter.swift | 84 +++++++++++++++++-- ...ityPoolRemoveLiquidityViewController.swift | 32 +++++-- ...quidityPoolRemoveLiquidityViewLayout.swift | 6 +- ...RemoveLiquidityConfirmViewController.swift | 19 +++-- ...PoolRemoveLiquidityConfirmViewLayout.swift | 34 +++++++- .../LiquidityPoolSupplyPresenter.swift | 61 ++++++++++---- .../LiquidityPoolSupplyViewController.swift | 25 +++++- .../LiquidityPoolSupplyViewLayout.swift | 31 ++----- .../LiquidityPoolSupplyConfirmPresenter.swift | 56 +++++++++++-- .../LiquidityPoolSupplyConfirmRouter.swift | 8 +- ...idityPoolSupplyConfirmViewController.swift | 19 +++-- ...LiquidityPoolSupplyConfirmViewLayout.swift | 62 ++++++++++---- ...leLiquidityPoolsListViewModelFactory.swift | 8 +- .../UserLiquidityPoolsListPresenter.swift | 2 + ...erLiquidityPoolsListViewModelFactory.swift | 3 +- .../LiquidityPoolsListProtocols.swift | 1 + .../LiquidityPoolsListViewLayout.swift | 39 +++++++-- .../LiquidityPoolListViewModel.swift | 1 + .../LiquidityPoolsOverviewPresenter.swift | 4 + .../LiquidityPoolsOverviewProtocols.swift | 1 + ...LiquidityPoolsOverviewViewController.swift | 4 + .../LiquidityPoolsOverviewViewLayout.swift | 70 ++++++++++++---- .../PolkaswapAdjustmentViewLayout.swift | 2 +- 30 files changed, 516 insertions(+), 132 deletions(-) create mode 100644 fearless/Modules/LiquidityPools/Common/LiquidityPoolsConstants.swift diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index eac1b9c924..8dff55c58a 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -2906,6 +2906,7 @@ FAD067D02C20453E0050291F /* LegacyEraStakersFetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD067CD2C20453E0050291F /* LegacyEraStakersFetching.swift */; }; FAD067D12C20453E0050291F /* DefaultEraStakersFetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD067CE2C20453E0050291F /* DefaultEraStakersFetching.swift */; }; FAD067D32C20550B0050291F /* UIImageView+Shimmered.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD067D22C20550B0050291F /* UIImageView+Shimmered.swift */; }; + FAD067D52C214E7C0050291F /* LiquidityPoolsConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD067D42C214E7C0050291F /* LiquidityPoolsConstants.swift */; }; FAD428942A834A8E001D6A16 /* TopViewControllerHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD428932A834A8E001D6A16 /* TopViewControllerHelper.swift */; }; FAD428962A834BA8001D6A16 /* UIApplication+TopViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD428952A834BA8001D6A16 /* UIApplication+TopViewController.swift */; }; FAD428982A860C9B001D6A16 /* EthereumNodeFetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD428972A860C9B001D6A16 /* EthereumNodeFetching.swift */; }; @@ -6044,6 +6045,7 @@ FAD067CD2C20453E0050291F /* LegacyEraStakersFetching.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyEraStakersFetching.swift; sourceTree = ""; }; FAD067CE2C20453E0050291F /* DefaultEraStakersFetching.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultEraStakersFetching.swift; sourceTree = ""; }; FAD067D22C20550B0050291F /* UIImageView+Shimmered.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImageView+Shimmered.swift"; sourceTree = ""; }; + FAD067D42C214E7C0050291F /* LiquidityPoolsConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsConstants.swift; sourceTree = ""; }; FAD428932A834A8E001D6A16 /* TopViewControllerHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopViewControllerHelper.swift; sourceTree = ""; }; FAD428952A834BA8001D6A16 /* UIApplication+TopViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+TopViewController.swift"; sourceTree = ""; }; FAD428972A860C9B001D6A16 /* EthereumNodeFetching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthereumNodeFetching.swift; sourceTree = ""; }; @@ -12676,6 +12678,7 @@ isa = PBXGroup; children = ( FA1D51D62BCFD445001353E7 /* LiquidityPools+ViewModel.swift */, + FAD067D42C214E7C0050291F /* LiquidityPoolsConstants.swift */, ); path = Common; sourceTree = ""; @@ -19106,6 +19109,7 @@ 02EC6059AEE8C92B4EDD09C0 /* LiquidityPoolsOverviewViewLayout.swift in Sources */, 61B5A91FBEF633FCC8D965B6 /* LiquidityPoolsOverviewAssembly.swift in Sources */, CF96BE0B636DA8227E689DDA /* LiquidityPoolDetailsProtocols.swift in Sources */, + FAD067D52C214E7C0050291F /* LiquidityPoolsConstants.swift in Sources */, 23E30A21620831C600CBC1D6 /* LiquidityPoolDetailsRouter.swift in Sources */, D2A85A5EE89EAAA856EA5C0F /* LiquidityPoolDetailsPresenter.swift in Sources */, 7D8D644C5E1695288A0E86C0 /* LiquidityPoolDetailsInteractor.swift in Sources */, diff --git a/fearless/Common/View/NavigationBar/Base/BaseTopBar.swift b/fearless/Common/View/NavigationBar/Base/BaseTopBar.swift index 144418b34b..2de9a78df1 100644 --- a/fearless/Common/View/NavigationBar/Base/BaseTopBar.swift +++ b/fearless/Common/View/NavigationBar/Base/BaseTopBar.swift @@ -1,7 +1,7 @@ import UIKit class BaseTopBar: UIView { - var leftStackView: UIStackView = UIFactory.default.createHorizontalStackView() + var leftStackView: UIStackView = UIFactory.default.createHorizontalStackView(spacing: 8) var centerStackView: UIStackView = UIFactory.default.createHorizontalStackView() var rightStackView: UIStackView = UIFactory.default.createHorizontalStackView() diff --git a/fearless/Common/View/UIFactory.swift b/fearless/Common/View/UIFactory.swift index cbb27818be..5289e5301d 100644 --- a/fearless/Common/View/UIFactory.swift +++ b/fearless/Common/View/UIFactory.swift @@ -105,6 +105,10 @@ protocol UIFactoryProtocol { func createDisabledButton() -> TriangularedButton func createRoundedButton() -> UIButton func createWarningView(title: String, text: String) -> UIView + func createDoneAccessoryView( + for delegate: AmountInputAccessoryViewDelegate?, + locale: Locale + ) -> UIToolbar } extension UIFactoryProtocol { @@ -371,6 +375,43 @@ final class UIFactory: UIFactoryProtocol { ) } + func createDoneAccessoryView( + for delegate: AmountInputAccessoryViewDelegate?, + locale: Locale + ) -> UIToolbar { + let frame = CGRect( + x: 0.0, + y: 0.0, + width: UIScreen.main.bounds.width, + height: UIConstants.accessoryBarHeight + ) + + let toolBar = AmountInputAccessoryView(frame: frame) + toolBar.actionDelegate = delegate + + let doneTitle = R.string.localizable.commonDone(preferredLanguages: locale.rLanguages) + let doneAction = ViewSelectorAction( + title: doneTitle, + selector: #selector(toolBar.actionSelectDone) + ) + + let spacing: CGFloat + + if toolBar.isAdaptiveWidthDecreased { + spacing = UIConstants.accessoryItemsSpacing * toolBar.designScaleRatio.width + } else { + spacing = UIConstants.accessoryItemsSpacing + } + + return createActionsAccessoryView( + for: toolBar, + actions: [], + doneAction: doneAction, + target: toolBar, + spacing: spacing + ) + } + func createCommonInputView() -> CommonInputView { let inputView = CommonInputView() inputView.defaultSetup() diff --git a/fearless/Modules/LiquidityPools/Common/LiquidityPoolsConstants.swift b/fearless/Modules/LiquidityPools/Common/LiquidityPoolsConstants.swift new file mode 100644 index 0000000000..bb855f369e --- /dev/null +++ b/fearless/Modules/LiquidityPools/Common/LiquidityPoolsConstants.swift @@ -0,0 +1,6 @@ +import Foundation + +enum LiquidityPoolConstants { + static let liquidityPoolsListHeaderHeight = 58.0 + static let liquidityPoolsListCellHeight = 44.0 +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift index 478f10f762..195a2bd6b5 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift @@ -54,6 +54,8 @@ final class LiquidityPoolDetailsViewLayout: UIView { return button }() + let tokenIconImageView = UIImageView() + var locale: Locale = .current { didSet { applyLocalization() @@ -126,6 +128,12 @@ final class LiquidityPoolDetailsViewLayout: UIView { if let targetAssetName = viewModel?.targetAssetName.uppercased() { targetAssetPooledView.titleLabel.text = "Your \(targetAssetName) Pooled" } + + viewModel?.rewardTokenIconViewModel?.loadImage( + on: tokenIconImageView, + targetSize: CGSize(width: 16, height: 16), + animated: true + ) } private func drawSubviews() { @@ -143,6 +151,7 @@ final class LiquidityPoolDetailsViewLayout: UIView { infoViewsStackView.addArrangedSubview(targetAssetPooledView) contentView.stackView.addArrangedSubview(supplyButton) contentView.stackView.addArrangedSubview(removeButton) + rewardTokenView.addSubview(tokenIconImageView) } private func setupConstraints() { @@ -168,6 +177,14 @@ final class LiquidityPoolDetailsViewLayout: UIView { make.top.equalToSuperview().inset(24) } + rewardTokenView.valueTop.snp.makeConstraints { make in + make.leading.equalTo(tokenIconImageView.snp.trailing).offset(4) + } + tokenIconImageView.snp.makeConstraints { make in + make.size.equalTo(16) + make.centerY.equalToSuperview() + } + [ reservesView, apyView, @@ -208,9 +225,9 @@ final class LiquidityPoolDetailsViewLayout: UIView { view.valueBottom.font = .p2Paragraph view.valueBottom.textColor = R.color.colorStrokeGray() view.borderView.isHidden = true - view.equalsLabelsWidth = true view.valueTop.lineBreakMode = .byTruncatingTail view.valueBottom.lineBreakMode = .byTruncatingMiddle + return view } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/ViewModel/LiquidityPoolDetailsViewModel.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/ViewModel/LiquidityPoolDetailsViewModel.swift index 040191a953..3db1d23692 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/ViewModel/LiquidityPoolDetailsViewModel.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/ViewModel/LiquidityPoolDetailsViewModel.swift @@ -11,4 +11,5 @@ struct LiquidityPoolDetailsViewModel { let targetAssetViewModel: BalanceViewModelProtocol? let tokenPairIconsViewModel: PolkaswapDoubleSymbolViewModel? let userPoolFieldsHidden: Bool + let rewardTokenIconViewModel: RemoteImageViewModel? } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/ViewModel/LiquidityPoolDetailsViewModelFactory.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/ViewModel/LiquidityPoolDetailsViewModelFactory.swift index 2f04abe96c..b7be5521d4 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/ViewModel/LiquidityPoolDetailsViewModelFactory.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/ViewModel/LiquidityPoolDetailsViewModelFactory.swift @@ -95,7 +95,8 @@ final class LiquidityPoolDetailsViewModelFactoryDefault: LiquidityPoolDetailsVie baseAssetViewModel: baseAssetViewModel?.value(for: locale), targetAssetViewModel: targetAssetViewModel?.value(for: locale), tokenPairIconsViewModel: tokenPairsIconViewModel, - userPoolFieldsHidden: !input.isUserPool && accountPoolInfo == nil + userPoolFieldsHidden: !input.isUserPool && accountPoolInfo == nil, + rewardTokenIconViewModel: RemoteImageViewModel(url: rewardAsset.icon) ) } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityPresenter.swift index c4a4a9ca11..954c577216 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityPresenter.swift @@ -5,10 +5,27 @@ import BigInt import SSFModels import SSFPolkaswap +struct RemoveLiquidityLoadingCollector { + var dexIdReady: Bool + var totalIssuanceReady: Bool + var reservesReady: Bool + var feeReady: Bool + + init() { + dexIdReady = false + totalIssuanceReady = false + reservesReady = false + feeReady = false + } + + var isReady: Bool { + dexIdReady && totalIssuanceReady && reservesReady && feeReady + } +} + protocol LiquidityPoolRemoveLiquidityConfirmViewInput: ControllerBackedProtocol { func didReceiveNetworkFee(fee: BalanceViewModelProtocol?) func setButtonLoadingState(isLoading: Bool) - func didUpdating() func didReceiveConfirmViewModel(_ viewModel: LiquidityPoolSupplyConfirmViewModel?) } @@ -21,7 +38,6 @@ protocol LiquidityPoolRemoveLiquidityViewInput: ControllerBackedProtocol { func didReceiveSwapQuoteReady() func didReceiveNetworkFee(fee: BalanceViewModelProtocol?) func setButtonLoadingState(isLoading: Bool) - func didUpdating() } protocol LiquidityPoolRemoveLiquidityInteractorInput: AnyObject { @@ -67,6 +83,8 @@ final class LiquidityPoolRemoveLiquidityPresenter { private var baseTargetRate: Decimal? private var dexId: String? + private var loadingCollector = RemoveLiquidityLoadingCollector() + private var baseAssetResultAmount: Decimal { guard let baseAssetInputResult else { return .zero @@ -153,6 +171,13 @@ final class LiquidityPoolRemoveLiquidityPresenter { interactor.estimateFee(removeLiquidityInfo: info) } + private func checkLoadingState() { + DispatchQueue.main.async { [weak self] in + self?.setupView?.setButtonLoadingState(isLoading: self?.loadingCollector.isReady == false) + self?.confirmView?.setButtonLoadingState(isLoading: self?.loadingCollector.isReady == false) + } + } + private func runLoadingState() { DispatchQueue.main.async { [weak self] in self?.setupView?.setButtonLoadingState(isLoading: true) @@ -160,7 +185,7 @@ final class LiquidityPoolRemoveLiquidityPresenter { } } - private func checkLoadingState() { + private func resetLoadingState() { DispatchQueue.main.async { [weak self] in self?.setupView?.setButtonLoadingState(isLoading: false) self?.confirmView?.setButtonLoadingState(isLoading: false) @@ -322,6 +347,15 @@ final class LiquidityPoolRemoveLiquidityPresenter { self?.setupView?.didReceiveXorBalanceViewModel(balanceViewModel: viewModel) } } + + private func handleRemovePool() { + baseAssetInputResult = .absolute((accountPoolInfo?.baseAssetPooled).or(0)) + targetAssetInputResult = .absolute((accountPoolInfo?.targetAssetPooled).or(0)) + provideFromAssetVewModel() + provideToAssetVewModel() + + refreshFee() + } } // MARK: - LiquidityPoolRemoveLiquidityConfirmViewOutput @@ -340,6 +374,20 @@ extension LiquidityPoolRemoveLiquidityPresenter: LiquidityPoolRemoveLiquidityCon interactor.submit(removeLiquidityInfo: removeInfo) } + + func didTapFeeInfo() { + var infoText: String + var infoTitle: String + infoTitle = "Network fee" + infoText = "Network fee is used to ensure SORA system’s growth and stable performance." + + let view = setupView ?? confirmView + router.presentInfo( + message: infoText, + title: infoTitle, + from: view + ) + } } // MARK: - LiquidityPoolRemoveLiquidityViewOutput @@ -357,6 +405,7 @@ extension LiquidityPoolRemoveLiquidityPresenter: LiquidityPoolRemoveLiquidityVie provideToAssetVewModel() provideFromAssetVewModel() refreshFee() + checkLoadingState() } func didLoad(view: LiquidityPoolRemoveLiquidityViewInput) { @@ -442,11 +491,16 @@ extension LiquidityPoolRemoveLiquidityPresenter: LiquidityPoolRemoveLiquidityVie } func selectFromAmountPercentage(_ percentage: Float) { + if percentage == 1.0 { + handleRemovePool() + return + } + runLoadingState() baseAssetInputResult = .rate(Decimal(Double(percentage))) - let baseAssetAbsolulteValue = baseAssetInputResult?.absoluteValue(from: baseAssetBalance.or(.zero)) + let baseAssetAbsolulteValue = baseAssetInputResult?.absoluteValue(from: (accountPoolInfo?.baseAssetPooled).or(.zero)) let targetAssetAbsoluteValue = baseAssetAbsolulteValue.or(.zero) * baseTargetRate.or(.zero) targetAssetInputResult = .absolute(targetAssetAbsoluteValue) @@ -461,7 +515,7 @@ extension LiquidityPoolRemoveLiquidityPresenter: LiquidityPoolRemoveLiquidityVie baseAssetInputResult = .absolute(newValue) - let baseAssetAbsolulteValue = baseAssetInputResult?.absoluteValue(from: baseAssetBalance.or(.zero)) + let baseAssetAbsolulteValue = baseAssetInputResult?.absoluteValue(from: (accountPoolInfo?.baseAssetPooled).or(.zero)) let targetAssetAbsoluteValue = baseAssetAbsolulteValue.or(.zero) * baseTargetRate.or(.zero) targetAssetInputResult = .absolute(targetAssetAbsoluteValue) @@ -472,11 +526,16 @@ extension LiquidityPoolRemoveLiquidityPresenter: LiquidityPoolRemoveLiquidityVie } func selectToAmountPercentage(_ percentage: Float) { + if percentage == 1.0 { + handleRemovePool() + return + } + runLoadingState() targetAssetInputResult = .rate(Decimal(Double(percentage))) - let targetAssetAbsoluteValue = targetAssetInputResult?.absoluteValue(from: targetAssetBalance.or(.zero)) + let targetAssetAbsoluteValue = targetAssetInputResult?.absoluteValue(from: (accountPoolInfo?.targetAssetPooled).or(.zero)) let baseAssetAbsolulteValue = targetAssetAbsoluteValue.or(.zero) / baseTargetRate.or(1) baseAssetInputResult = .absolute(baseAssetAbsolulteValue) @@ -491,7 +550,7 @@ extension LiquidityPoolRemoveLiquidityPresenter: LiquidityPoolRemoveLiquidityVie targetAssetInputResult = .absolute(newValue) - let targetAssetAbsoluteValue = targetAssetInputResult?.absoluteValue(from: targetAssetBalance.or(.zero)) + let targetAssetAbsoluteValue = targetAssetInputResult?.absoluteValue(from: (accountPoolInfo?.targetAssetPooled).or(.zero)) let baseAssetAbsolulteValue = targetAssetAbsoluteValue.or(.zero) / baseTargetRate.or(1) baseAssetInputResult = .absolute(baseAssetAbsolulteValue) @@ -510,16 +569,21 @@ extension LiquidityPoolRemoveLiquidityPresenter: LiquidityPoolRemoveLiquidityInt return } + resetLoadingState() + router.complete(on: confirmView, title: hash, chainAsset: utilityChainAsset) } func didReceiveSubmitError(error: Error) { + resetLoadingState() router.present(error: error, from: setupView, locale: selectedLocale) } func didReceiveTotalIssuance(totalIssuance: BigUInt?) { self.totalIssuance = totalIssuance refreshFee() + loadingCollector.totalIssuanceReady = true + checkLoadingState() } func didReceiveTotalIssuanceError(error: Error) { @@ -529,6 +593,8 @@ extension LiquidityPoolRemoveLiquidityPresenter: LiquidityPoolRemoveLiquidityInt func didReceiveDexId(_ dexId: String) { self.dexId = dexId refreshFee() + loadingCollector.dexIdReady = true + checkLoadingState() } func didReceiveDexIdError(_ error: Error) { @@ -549,6 +615,8 @@ extension LiquidityPoolRemoveLiquidityPresenter: LiquidityPoolRemoveLiquidityInt networkFee = Decimal.fromSubstrateAmount(fee, precision: Int16(utilityAsset.precision)) provideFeeViewModel() + loadingCollector.feeReady = true + checkLoadingState() } func didReceiveFeeError(_ error: Error) { @@ -624,6 +692,8 @@ extension LiquidityPoolRemoveLiquidityPresenter: LiquidityPoolRemoveLiquidityInt func didReceivePoolReserves(reserves: PolkaswapPoolReservesInfo?) { self.reserves = reserves refreshFee() + loadingCollector.reservesReady = true + checkLoadingState() } func didReceiveUserPoolError(error: Error) { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityViewController.swift index 79cc0c22c2..4eceeae0ef 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityViewController.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityViewController.swift @@ -11,6 +11,7 @@ protocol LiquidityPoolRemoveLiquidityViewOutput: AnyObject { func updateFromAmount(_ newValue: Decimal) func selectToAmountPercentage(_ percentage: Float) func updateToAmount(_ newValue: Decimal) + func didTapFeeInfo() } final class LiquidityPoolRemoveLiquidityViewController: UIViewController, ViewHolder, HiddableBarWhenPushed { @@ -31,6 +32,8 @@ final class LiquidityPoolRemoveLiquidityViewController: UIViewController, ViewHo private var assetFromViewModel: AssetBalanceViewModelProtocol? private var assetToViewModel: AssetBalanceViewModelProtocol? + private var feeViewModel: BalanceViewModelProtocol? + // MARK: - Constructor init( @@ -67,6 +70,15 @@ final class LiquidityPoolRemoveLiquidityViewController: UIViewController, ViewHo if isBeingPresented || isMovingToParent { output.handleViewAppeared() } + + if keyboardHandler == nil { + setupKeyboardHandler() + } + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + clearKeyboardHandler() } // MARK: - Private methods @@ -83,6 +95,8 @@ final class LiquidityPoolRemoveLiquidityViewController: UIViewController, ViewHo let locale = localizationManager?.selectedLocale ?? Locale.current let accessoryView = UIFactory.default.createAmountAccessoryView(for: self, locale: locale) rootView.swapFromInputView.textField.inputAccessoryView = accessoryView + rootView.swapToInputView.textField.inputAccessoryView = accessoryView + updatePreviewButton() } @@ -103,6 +117,13 @@ final class LiquidityPoolRemoveLiquidityViewController: UIViewController, ViewHo action: #selector(handleTapPreviewButton), for: .touchUpInside ) + + let tapFeeInfo = UITapGestureRecognizer( + target: self, + action: #selector(handleTapFeeInfo) + ) + rootView.networkFeeView + .addGestureRecognizer(tapFeeInfo) } // MARK: - Private actions @@ -114,6 +135,10 @@ final class LiquidityPoolRemoveLiquidityViewController: UIViewController, ViewHo @objc private func handleTapPreviewButton() { output.didTapPreviewButton() } + + @objc private func handleTapFeeInfo() { + output.didTapFeeInfo() + } } // MARK: - LiquidityPoolRemoveLiquidityViewInput @@ -158,18 +183,11 @@ extension LiquidityPoolRemoveLiquidityViewController: LiquidityPoolRemoveLiquidi func didReceiveNetworkFee(fee: BalanceViewModelProtocol?) { rootView.bind(fee: fee) - updatePreviewButton() } func setButtonLoadingState(isLoading: Bool) { rootView.previewButton.set(loading: isLoading) } - - func didUpdating() { - DispatchQueue.main.async { - self.rootView.previewButton.set(enabled: false) - } - } } // MARK: - Localizable diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityViewLayout.swift index 107812b166..200adfd303 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityViewLayout.swift @@ -42,8 +42,8 @@ final class LiquidityPoolRemoveLiquidityViewLayout: UIView { return view }() - let swapFromInputView = AmountInputViewV2() - let swapToInputView = AmountInputViewV2() + let swapFromInputView = AmountInputViewV2(type: .available) + let swapToInputView = AmountInputViewV2(type: .available) let switchSwapButton: UIButton = { let button = UIButton() button.setImage(R.image.iconAddTokenPair(), for: .normal) @@ -162,7 +162,7 @@ final class LiquidityPoolRemoveLiquidityViewLayout: UIView { warningView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(16) - make.bottom.equalTo(previewButton.snp.top).offset(-16) + make.bottom.equalToSuperview().inset(UIConstants.actionHeight + UIConstants.bigOffset * 2) } let switchInputsView = createSwitchInputsView() diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmViewController.swift index 0c55f61718..8afecb0538 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmViewController.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmViewController.swift @@ -6,6 +6,7 @@ protocol LiquidityPoolRemoveLiquidityConfirmViewOutput: AnyObject { func handleViewAppeared() func didTapBackButton() func didTapConfirmButton() + func didTapFeeInfo() } final class LiquidityPoolRemoveLiquidityConfirmViewController: UIViewController, ViewHolder, HiddableBarWhenPushed { @@ -65,6 +66,13 @@ final class LiquidityPoolRemoveLiquidityConfirmViewController: UIViewController, action: #selector(handleTapConfirmButton), for: .touchUpInside ) + + let tapFeeInfo = UITapGestureRecognizer( + target: self, + action: #selector(handleTapFeeInfo) + ) + rootView.networkFeeView + .addGestureRecognizer(tapFeeInfo) } // MARK: - Private actions @@ -76,6 +84,10 @@ final class LiquidityPoolRemoveLiquidityConfirmViewController: UIViewController, @objc private func handleTapConfirmButton() { output.didTapConfirmButton() } + + @objc private func handleTapFeeInfo() { + output.didTapFeeInfo() + } } // MARK: - LiquidityPoolSupplyConfirmViewInput @@ -87,12 +99,7 @@ extension LiquidityPoolRemoveLiquidityConfirmViewController: LiquidityPoolRemove func setButtonLoadingState(isLoading: Bool) { rootView.confirmButton.set(loading: isLoading) - } - - func didUpdating() { - DispatchQueue.main.async { - self.rootView.confirmButton.set(enabled: false) - } + rootView.confirmButton.set(enabled: !isLoading) } func didReceiveConfirmViewModel(_ viewModel: LiquidityPoolSupplyConfirmViewModel?) { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmViewLayout.swift index 93c8bd95df..9263615326 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmViewLayout.swift @@ -4,6 +4,9 @@ final class LiquidityPoolRemoveLiquidityConfirmViewLayout: UIView { private enum Constants { static let navigationBarHeight: CGFloat = 56.0 static let backButtonSize = CGSize(width: 32, height: 32) + static let imageWidth: CGFloat = 12 + static let imageHeight: CGFloat = 12 + static let imageVerticalPosition: CGFloat = 2 } let navigationViewContainer = UIView() @@ -103,8 +106,6 @@ final class LiquidityPoolRemoveLiquidityConfirmViewLayout: UIView { private func applyLocalization() { titleLabel.text = "Remove Liquidity" - networkFeeView.titleLabel.text = R.string.localizable - .commonNetworkFee(preferredLanguages: locale.rLanguages) confirmButton.imageWithTitleView?.title = R.string.localizable .commonConfirm(preferredLanguages: locale.rLanguages) } @@ -165,5 +166,34 @@ final class LiquidityPoolRemoveLiquidityConfirmViewLayout: UIView { make.bottom.equalToSuperview().inset(UIConstants.bigOffset) make.height.equalTo(UIConstants.actionHeight) } + let texts = [ + R.string.localizable + .polkaswapNetworkFee(preferredLanguages: locale.rLanguages) + ] + + [ + networkFeeView.titleLabel + ].enumerated().forEach { index, label in + setInfoImage(for: label, text: texts[index]) + } + } + + private func setInfoImage(for label: UILabel, text: String) { + let attributedString = NSMutableAttributedString(string: text) + + let imageAttachment = NSTextAttachment() + imageAttachment.image = R.image.iconInfoFilled() + imageAttachment.bounds = CGRect( + x: 0, + y: -Constants.imageVerticalPosition, + width: Constants.imageWidth, + height: Constants.imageHeight + ) + + let imageString = NSAttributedString(attachment: imageAttachment) + attributedString.append(NSAttributedString(string: " ")) + attributedString.append(imageString) + + label.attributedText = attributedString } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift index 392d58c60f..ac16ff3f0a 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift @@ -5,6 +5,20 @@ import SSFPools import BigInt import SSFPolkaswap +struct SupplyLiquidityLoadingCollector { + var dexIdReady: Bool + var feeReady: Bool + + init() { + dexIdReady = false + feeReady = false + } + + var isReady: Bool { + dexIdReady && feeReady + } +} + protocol LiquidityPoolSupplyViewInput: ControllerBackedProtocol { func didReceiveSwapFrom(viewModel: AssetBalanceViewModelProtocol?) func didReceiveSwapTo(viewModel: AssetBalanceViewModelProtocol?) @@ -66,6 +80,8 @@ final class LiquidityPoolSupplyPresenter { private var dexId: String? + private var loadingCollector = SupplyLiquidityLoadingCollector() + private var baseAssetResultAmount: Decimal { guard let baseAssetInputResult else { return .zero @@ -146,13 +162,19 @@ final class LiquidityPoolSupplyPresenter { interactor.estimateFee(supplyLiquidityInfo: supplyLiquidityInfo) } + private func checkLoadingState() { + DispatchQueue.main.async { [weak self] in + self?.view?.setButtonLoadingState(isLoading: self?.loadingCollector.isReady == false) + } + } + private func runLoadingState() { DispatchQueue.main.async { [weak self] in self?.view?.setButtonLoadingState(isLoading: true) } } - private func checkLoadingState() { + private func resetLoadingState() { DispatchQueue.main.async { [weak self] in self?.view?.setButtonLoadingState(isLoading: false) } @@ -185,8 +207,6 @@ final class LiquidityPoolSupplyPresenter { self?.view?.didReceiveSwapFrom(amountInputViewModel: inputViewModel) } } - - checkLoadingState() } private func provideToAssetVewModel(updateAmountInput: Bool = true) { @@ -216,8 +236,6 @@ final class LiquidityPoolSupplyPresenter { self?.view?.didReceiveSwapTo(amountInputViewModel: inputViewModel) } } - - checkLoadingState() } private func provideFeeViewModel() { @@ -239,8 +257,6 @@ final class LiquidityPoolSupplyPresenter { } networkFeeViewModel = feeViewModel - - checkLoadingState() } private func buildBalanceSwapToViewModelFactory( @@ -293,8 +309,20 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { func didTapApyInfo() { var infoText: String var infoTitle: String - infoTitle = "Strategic Bonus APY" - infoText = "Farming reward for liquidity provision" + infoTitle = "Strategic bonus APY" + infoText = "APY is a figure that represents the actual amount of interest earned on investments in Liquidity pool." + router.presentInfo( + message: infoText, + title: infoTitle, + from: view + ) + } + + func didTapFeeInfo() { + var infoText: String + var infoTitle: String + infoTitle = "Network fee" + infoText = "Network fee is used to ensure SORA system’s growth and stable performance." router.presentInfo( message: infoText, title: infoTitle, @@ -350,8 +378,6 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { } func selectFromAmountPercentage(_ percentage: Float) { - runLoadingState() - baseAssetInputResult = .rate(Decimal(Double(percentage))) let baseAssetAbsolulteValue = baseAssetInputResult?.absoluteValue(from: baseAssetBalance.or(.zero)) @@ -365,8 +391,6 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { } func updateFromAmount(_ newValue: Decimal) { - runLoadingState() - baseAssetInputResult = .absolute(newValue) let baseAssetAbsolulteValue = baseAssetInputResult?.absoluteValue(from: baseAssetBalance.or(.zero)) @@ -380,8 +404,6 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { } func selectToAmountPercentage(_ percentage: Float) { - runLoadingState() - targetAssetInputResult = .rate(Decimal(Double(percentage))) let targetAssetAbsoluteValue = targetAssetInputResult?.absoluteValue(from: targetAssetBalance.or(.zero)) @@ -395,8 +417,6 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { } func updateToAmount(_ newValue: Decimal) { - runLoadingState() - targetAssetInputResult = .absolute(newValue) let targetAssetAbsoluteValue = targetAssetInputResult?.absoluteValue(from: targetAssetBalance.or(.zero)) @@ -441,6 +461,7 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { } func handleViewAppeared() { + checkLoadingState() provideInitialData() } } @@ -460,6 +481,9 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyInteractorOutput { func didReceiveDexId(_ dexId: String) { self.dexId = dexId refreshFee() + + loadingCollector.dexIdReady = true + checkLoadingState() } func didReceiveDexIdError(_ error: Error) { @@ -473,6 +497,9 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyInteractorOutput { networkFee = Decimal.fromSubstrateAmount(fee, precision: Int16(utilityAsset.precision)) provideFeeViewModel() + + loadingCollector.feeReady = true + checkLoadingState() } func didReceiveFeeError(_ error: Error) { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift index b19c37661c..2ffcee6435 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift @@ -12,6 +12,7 @@ protocol LiquidityPoolSupplyViewOutput: AnyObject { func updateFromAmount(_ newValue: Decimal) func selectToAmountPercentage(_ percentage: Float) func updateToAmount(_ newValue: Decimal) + func didTapFeeInfo() } final class LiquidityPoolSupplyViewController: UIViewController, ViewHolder, HiddableBarWhenPushed { @@ -68,6 +69,15 @@ final class LiquidityPoolSupplyViewController: UIViewController, ViewHolder, Hid if isBeingPresented || isMovingToParent { output.handleViewAppeared() } + + if keyboardHandler == nil { + setupKeyboardHandler() + } + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + clearKeyboardHandler() } // MARK: - Private methods @@ -82,7 +92,7 @@ final class LiquidityPoolSupplyViewController: UIViewController, ViewHolder, Hid rootView.swapToInputView.textField.isUserInteractionEnabled = false let locale = localizationManager?.selectedLocale ?? Locale.current - let accessoryView = UIFactory.default.createAmountAccessoryView(for: self, locale: locale) + let accessoryView = UIFactory.default.createDoneAccessoryView(for: self, locale: locale) rootView.swapFromInputView.textField.inputAccessoryView = accessoryView updatePreviewButton() } @@ -111,6 +121,13 @@ final class LiquidityPoolSupplyViewController: UIViewController, ViewHolder, Hid ) rootView.apyView .addGestureRecognizer(tapApyInfo) + + let tapFeeInfo = UITapGestureRecognizer( + target: self, + action: #selector(handleTapFeeInfo) + ) + rootView.networkFeeView + .addGestureRecognizer(tapFeeInfo) } // MARK: - Private actions @@ -126,14 +143,16 @@ final class LiquidityPoolSupplyViewController: UIViewController, ViewHolder, Hid @objc private func handleTapPreviewButton() { output.didTapPreviewButton() } + + @objc private func handleTapFeeInfo() { + output.didTapFeeInfo() + } } // MARK: - LiquidityPoolSupplyViewInput extension LiquidityPoolSupplyViewController: LiquidityPoolSupplyViewInput { func didReceiveSwapQuoteReady() { - print("Swap quotes ready") - rootView.swapFromInputView.textField.isUserInteractionEnabled = true rootView.swapToInputView.textField.isUserInteractionEnabled = true } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift index 40796af79d..fc00eafa46 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift @@ -42,8 +42,8 @@ final class LiquidityPoolSupplyViewLayout: UIView { return view }() - let swapFromInputView = AmountInputViewV2(type: .bonded) - let swapToInputView = AmountInputViewV2(type: .bonded) + let swapFromInputView = AmountInputViewV2(type: .available) + let swapToInputView = AmountInputViewV2(type: .available) let switchSwapButton: UIButton = { let button = UIButton() button.setImage(R.image.iconAddTokenPair(), for: .normal) @@ -55,10 +55,10 @@ final class LiquidityPoolSupplyViewLayout: UIView { return view }() - let slippageView = UIFactory.default.createMultiView() - let apyView = UIFactory.default.createMultiView() - let rewardTokenView = UIFactory.default.createMultiView() - let networkFeeView = UIFactory.default.createMultiView() + let slippageView = UIFactory.default.createConfirmationMultiView() + let apyView = UIFactory.default.createConfirmationMultiView() + let rewardTokenView = UIFactory.default.createConfirmationMultiView() + let networkFeeView = UIFactory.default.createConfirmationMultiView() let apyInfoButton: UIButton = { let button = UIButton() button.isUserInteractionEnabled = false @@ -135,7 +135,7 @@ final class LiquidityPoolSupplyViewLayout: UIView { viewModel.rewardTokenIconViewModel?.loadImage( on: tokenIconImageView, - targetSize: CGSize(width: 12, height: 12), + targetSize: CGSize(width: 16, height: 16), animated: true ) } @@ -258,28 +258,13 @@ final class LiquidityPoolSupplyViewLayout: UIView { make.leading.equalTo(tokenIconImageView.snp.trailing).offset(4) } tokenIconImageView.snp.makeConstraints { make in - make.size.equalTo(12) + make.size.equalTo(16) make.centerY.equalToSuperview() } return backgroundView } - private func createMultiView() -> TitleMultiValueView { - let view = TitleMultiValueView() - view.titleLabel.font = .h6Title - view.titleLabel.textColor = R.color.colorWhite50() - view.valueTop.font = .p1Paragraph - view.valueTop.textColor = R.color.colorWhite() - view.valueBottom.font = .p2Paragraph - view.valueBottom.textColor = R.color.colorStrokeGray() - view.borderView.isHidden = true - view.equalsLabelsWidth = true - view.valueTop.lineBreakMode = .byTruncatingTail - view.valueBottom.lineBreakMode = .byTruncatingMiddle - return view - } - private func applyLocalization() { titleLabel.text = "Supply Liquidity" diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmPresenter.swift index 55483c2d88..74cffd38e2 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmPresenter.swift @@ -5,10 +5,23 @@ import SSFPolkaswap import SSFModels import BigInt +struct SupplyLiquidityConfirmLoadingCollector { + var dexIdReady: Bool + var feeReady: Bool + + init() { + dexIdReady = false + feeReady = false + } + + var isReady: Bool { + dexIdReady && feeReady + } +} + protocol LiquidityPoolSupplyConfirmViewInput: ControllerBackedProtocol { func didReceiveNetworkFee(fee: BalanceViewModelProtocol?) func setButtonLoadingState(isLoading: Bool) - func didUpdating() func didReceiveViewModel(_ viewModel: LiquidityPoolSupplyViewModel) func didReceiveConfirmationViewModel(_ viewModel: LiquidityPoolSupplyConfirmViewModel?) } @@ -49,6 +62,8 @@ final class LiquidityPoolSupplyConfirmPresenter { private var dexId: String? + private var loadingCollector = SupplyLiquidityConfirmLoadingCollector() + // MARK: - Constructors init( @@ -102,13 +117,19 @@ final class LiquidityPoolSupplyConfirmPresenter { interactor.estimateFee(supplyLiquidityInfo: supplyLiquidityInfo) } + private func checkLoadingState() { + DispatchQueue.main.async { [weak self] in + self?.view?.setButtonLoadingState(isLoading: (self?.loadingCollector.isReady) == false) + } + } + private func runLoadingState() { DispatchQueue.main.async { [weak self] in self?.view?.setButtonLoadingState(isLoading: true) } } - private func checkLoadingState() { + private func resetLoadingState() { DispatchQueue.main.async { [weak self] in self?.view?.setButtonLoadingState(isLoading: false) } @@ -134,7 +155,7 @@ final class LiquidityPoolSupplyConfirmPresenter { networkFeeViewModel = feeViewModel - checkLoadingState() + resetLoadingState() } private func buildBalanceSwapToViewModelFactory( @@ -214,8 +235,20 @@ extension LiquidityPoolSupplyConfirmPresenter: LiquidityPoolSupplyConfirmViewOut func didTapApyInfo() { var infoText: String var infoTitle: String - infoTitle = "Strategic Bonus APY" - infoText = "Farming reward for liquidity provision" + infoTitle = "Strategic bonus APY" + infoText = "APY is a figure that represents the actual amount of interest earned on investments in Liquidity pool." + router.presentInfo( + message: infoText, + title: infoTitle, + from: view + ) + } + + func didTapFeeInfo() { + var infoText: String + var infoTitle: String + infoTitle = "Network fee" + infoText = "Network fee is used to ensure SORA system’s growth and stable performance." router.presentInfo( message: infoText, title: infoTitle, @@ -245,6 +278,8 @@ extension LiquidityPoolSupplyConfirmPresenter: LiquidityPoolSupplyConfirmViewOut ) interactor.submit(supplyLiquidityInfo: supplyLiquidityInfo) + + runLoadingState() } func didLoad(view: LiquidityPoolSupplyConfirmViewInput) { @@ -253,6 +288,8 @@ extension LiquidityPoolSupplyConfirmPresenter: LiquidityPoolSupplyConfirmViewOut } func handleViewAppeared() { + checkLoadingState() + DispatchQueue.main.async { [weak self] in self?.view?.didReceiveNetworkFee(fee: nil) self?.provideViewModel() @@ -278,6 +315,9 @@ extension LiquidityPoolSupplyConfirmPresenter: LiquidityPoolSupplyConfirmInterac func didReceiveDexId(_ dexId: String) { self.dexId = dexId refreshFee() + + loadingCollector.dexIdReady = true + checkLoadingState() } func didReceiveDexIdError(_ error: Error) { @@ -291,6 +331,9 @@ extension LiquidityPoolSupplyConfirmPresenter: LiquidityPoolSupplyConfirmInterac networkFee = Decimal.fromSubstrateAmount(fee, precision: Int16(utilityAsset.precision)) provideFeeViewModel() + + loadingCollector.feeReady = true + checkLoadingState() } func didReceiveFeeError(_ error: Error) { @@ -341,6 +384,8 @@ extension LiquidityPoolSupplyConfirmPresenter: LiquidityPoolSupplyConfirmInterac } func didReceiveTransactionHash(_ hash: String) { + resetLoadingState() + guard let utilityChainAsset = chain.utilityChainAssets().first else { return } @@ -349,6 +394,7 @@ extension LiquidityPoolSupplyConfirmPresenter: LiquidityPoolSupplyConfirmInterac } func didReceiveSubmitError(error: Error) { + resetLoadingState() router.present(error: error, from: view, locale: selectedLocale) } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmRouter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmRouter.swift index 0db7a44e76..61c3e3a977 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmRouter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmRouter.swift @@ -18,11 +18,9 @@ final class LiquidityPoolSupplyConfirmRouter: LiquidityPoolSupplyConfirmRouterIn ) controller?.modalTransitioningFactory = factory - view?.controller.navigationController?.dismiss(animated: true) { - if let presenter = presenter as? ControllerBackedProtocol, - let controller = controller { - presenter.controller.present(controller, animated: true) - } + view?.controller.navigationController?.popToRootViewController(animated: true) + if let controller = controller { + view?.controller.navigationController?.present(controller, animated: true) } } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewController.swift index 3b01e73457..1bf3028c90 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewController.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewController.swift @@ -7,6 +7,7 @@ protocol LiquidityPoolSupplyConfirmViewOutput: AnyObject { func didTapBackButton() func didTapApyInfo() func didTapConfirmButton() + func didTapFeeInfo() } final class LiquidityPoolSupplyConfirmViewController: UIViewController, ViewHolder, HiddableBarWhenPushed { @@ -73,6 +74,13 @@ final class LiquidityPoolSupplyConfirmViewController: UIViewController, ViewHold ) rootView.apyView .addGestureRecognizer(tapApyInfo) + + let tapNetworkFeeInfo = UITapGestureRecognizer( + target: self, + action: #selector(handleTapNetworkFeeInfo) + ) + rootView.networkFeeView + .addGestureRecognizer(tapNetworkFeeInfo) } // MARK: - Private actions @@ -88,6 +96,10 @@ final class LiquidityPoolSupplyConfirmViewController: UIViewController, ViewHold @objc private func handleTapConfirmButton() { output.didTapConfirmButton() } + + @objc private func handleTapNetworkFeeInfo() { + output.didTapFeeInfo() + } } // MARK: - LiquidityPoolSupplyConfirmViewInput @@ -99,12 +111,7 @@ extension LiquidityPoolSupplyConfirmViewController: LiquidityPoolSupplyConfirmVi func setButtonLoadingState(isLoading: Bool) { rootView.confirmButton.set(loading: isLoading) - } - - func didUpdating() { - DispatchQueue.main.async { - self.rootView.confirmButton.set(enabled: false) - } + rootView.confirmButton.set(enabled: !isLoading) } func didReceiveViewModel(_ viewModel: LiquidityPoolSupplyViewModel) { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewLayout.swift index 289c44c658..36b793e40e 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewLayout.swift @@ -4,6 +4,9 @@ final class LiquidityPoolSupplyConfirmViewLayout: UIView { private enum Constants { static let navigationBarHeight: CGFloat = 56.0 static let backButtonSize = CGSize(width: 32, height: 32) + static let imageWidth: CGFloat = 12 + static let imageHeight: CGFloat = 12 + static let imageVerticalPosition: CGFloat = 2 } let navigationViewContainer = UIView() @@ -81,6 +84,7 @@ final class LiquidityPoolSupplyConfirmViewLayout: UIView { let confirmButton: TriangularedButton = { let button = TriangularedButton() + button.isEnabled = true button.applyEnabledStyle() return button }() @@ -123,7 +127,7 @@ final class LiquidityPoolSupplyConfirmViewLayout: UIView { viewModel.rewardTokenIconViewModel?.loadImage( on: tokenIconImageView, - targetSize: CGSize(width: 12, height: 12), + targetSize: CGSize(width: 16, height: 16), animated: true ) } @@ -143,15 +147,11 @@ final class LiquidityPoolSupplyConfirmViewLayout: UIView { // MARK: - Private methods private func applyLocalization() { - titleLabel.text = R.string.localizable - .commonPreview(preferredLanguages: locale.rLanguages) - networkFeeView.titleLabel.text = R.string.localizable - .commonNetworkFee(preferredLanguages: locale.rLanguages) + titleLabel.text = "Confirm Liquidity" confirmButton.imageWithTitleView?.title = R.string.localizable .commonConfirm(preferredLanguages: locale.rLanguages) swapStubTitle.text = "Output is estimated. If the price changes more than 0.5% your transaction will revert." - slippageView.titleLabel.text = R.string.localizable.polkaswapSettingsSlippageTitle(preferredLanguages: locale.rLanguages) - apyView.titleLabel.text = "Strategic Bonus APY" + slippageView.titleLabel.text = "Slippage" rewardTokenView.titleLabel.text = "Rewards Payout In" } @@ -213,13 +213,13 @@ final class LiquidityPoolSupplyConfirmViewLayout: UIView { make.height.equalTo(UIConstants.actionHeight) } - apyView.addSubview(apyInfoButton) - - apyInfoButton.snp.makeConstraints { make in - make.leading.equalTo(apyView.titleLabel.snp.trailing).offset(4) - make.centerY.equalToSuperview() - make.size.equalTo(12) - } +// apyView.addSubview(apyInfoButton) +// +// apyInfoButton.snp.makeConstraints { make in +// make.leading.equalTo(apyView.titleLabel.snp.trailing).offset(4) +// make.centerY.equalToSuperview() +// make.size.equalTo(12) +// } rewardTokenView.addSubview(tokenIconImageView) @@ -227,8 +227,40 @@ final class LiquidityPoolSupplyConfirmViewLayout: UIView { make.leading.equalTo(tokenIconImageView.snp.trailing).offset(4) } tokenIconImageView.snp.makeConstraints { make in - make.size.equalTo(12) + make.size.equalTo(16) make.centerY.equalToSuperview() } + + let texts = [ + R.string.localizable + .polkaswapNetworkFee(preferredLanguages: locale.rLanguages), + "Strategic Bonus APY" + ] + + [ + networkFeeView.titleLabel, + apyView.titleLabel + ].enumerated().forEach { index, label in + setInfoImage(for: label, text: texts[index]) + } + } + + private func setInfoImage(for label: UILabel, text: String) { + let attributedString = NSMutableAttributedString(string: text) + + let imageAttachment = NSTextAttachment() + imageAttachment.image = R.image.iconInfoFilled() + imageAttachment.bounds = CGRect( + x: 0, + y: -Constants.imageVerticalPosition, + width: Constants.imageWidth, + height: Constants.imageHeight + ) + + let imageString = NSAttributedString(attachment: imageAttachment) + attributedString.append(NSAttributedString(string: " ")) + attributedString.append(imageString) + + label.attributedText = attributedString } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListViewModelFactory.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListViewModelFactory.swift index 6c77773bdb..e4b760a7b6 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListViewModelFactory.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListViewModelFactory.swift @@ -94,13 +94,13 @@ final class AvailableLiquidityPoolsListViewModelFactoryDefault: AvailableLiquidi return $0.tokenPairNameLabelText.lowercased().contains(searchText.lowercased()) } - let filteredViewModels = type == .embed ? Array(poolViewModels.or([]).prefix(10)) : poolViewModels.or([]) return LiquidityPoolListViewModel( - poolViewModels: filteredViewModels, + poolViewModels: poolViewModels, titleLabelText: "Available pools", - moreButtonVisible: type == .embed && (filteredViewModels.count < pairs?.count ?? 0), + moreButtonVisible: type == .embed, backgroundVisible: type == .full, - refreshAvailable: type == .full + refreshAvailable: type == .full, + isEmbed: type == .embed ) } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift index d8be3f9bd7..47ad7eec84 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift @@ -67,6 +67,8 @@ final class UserLiquidityPoolsListPresenter { ) view?.didReceive(viewModel: viewModel) + + moduleOutput?.didReceiveUserPoolCount((viewModel.poolViewModels?.count).or(0)) } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListViewModelFactory.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListViewModelFactory.swift index 3929807a22..54e6290126 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListViewModelFactory.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListViewModelFactory.swift @@ -100,7 +100,8 @@ final class UserLiquidityPoolsListViewModelFactoryDefault: UserLiquidityPoolsLis titleLabelText: "User pools", moreButtonVisible: type == .embed && (poolViewModels?.count ?? 0 < pools?.count ?? 0), backgroundVisible: type == .full, - refreshAvailable: type == .full + refreshAvailable: type == .full, + isEmbed: type == .embed ) } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListProtocols.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListProtocols.swift index 6ac651d76d..3a3255ea0b 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListProtocols.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListProtocols.swift @@ -37,4 +37,5 @@ protocol LiquidityPoolsListModuleOutput: AnyObject { func didTapMoreUserPools() func didTapMoreAvailablePools() func shouldShowUserPools(_ shouldShow: Bool) + func didReceiveUserPoolCount(_ userPoolsCount: Int) } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift index 166a6624d7..afcf7fcfc8 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift @@ -30,15 +30,17 @@ final class LiquidityPoolsListViewLayout: UIView { let backButton: UIButton = { let button = UIButton() - button.setImage(R.image.iconClose(), for: .normal) + button.setImage(R.image.iconBack(), for: .normal) button.layer.masksToBounds = true button.backgroundColor = R.color.colorWhite8() button.isHidden = true return button }() - let tableView: SelfSizingTableView = { - let view = SelfSizingTableView(frame: .zero) + let separatorView = UIFactory.default.createSeparatorView() + + let tableView: UITableView = { + let view = UITableView(frame: .zero) view.backgroundColor = .clear view.separatorStyle = .none view.contentInset = .zero @@ -82,9 +84,22 @@ final class LiquidityPoolsListViewLayout: UIView { moreButton.isHidden = !viewModel.moreButtonVisible backButton.isHidden = !viewModel.backgroundVisible searchTextField.isHidden = !viewModel.refreshAvailable + separatorView.isHidden = viewModel.refreshAvailable tableView.refreshControl = viewModel.refreshAvailable ? UIRefreshControl() : nil tableView.isScrollEnabled = viewModel.refreshAvailable + + backgroundColor = viewModel.backgroundVisible ? R.color.colorBlack19() : .clear + + titleLabel.snp.remakeConstraints { make in + make.centerY.equalToSuperview() + + if viewModel.isEmbed { + make.leading.equalToSuperview().inset(12) + } else { + make.centerX.equalToSuperview() + } + } } override func layoutSubviews() { @@ -98,8 +113,9 @@ final class LiquidityPoolsListViewLayout: UIView { vStackView.addArrangedSubview(topBar) vStackView.addArrangedSubview(searchTextField) - vStackView.addArrangedSubview(tableView) + vStackView.addArrangedSubview(separatorView) + addSubview(tableView) topBar.addSubview(titleLabel) topBar.addSubview(moreButton) topBar.addSubview(backButton) @@ -107,22 +123,27 @@ final class LiquidityPoolsListViewLayout: UIView { private func setupConstraints() { vStackView.snp.makeConstraints { make in - keyboardAdoptableConstraint = make.bottom.equalToSuperview().constraint + keyboardAdoptableConstraint = make.bottom.lessThanOrEqualToSuperview().constraint make.leading.trailing.top.equalToSuperview() } + separatorView.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(16) + make.height.equalTo(1.0 / UIScreen.main.scale) + } + topBar.snp.makeConstraints { make in make.height.equalTo(42) make.leading.trailing.equalToSuperview() } tableView.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() + make.top.equalTo(vStackView.snp.bottom).offset(8) + make.leading.trailing.bottom.equalToSuperview() } titleLabel.snp.makeConstraints { make in - make.leading.equalToSuperview().inset(12) - make.centerY.equalToSuperview() + make.centerX.centerY.equalToSuperview() } searchTextField.snp.makeConstraints { make in @@ -139,7 +160,7 @@ final class LiquidityPoolsListViewLayout: UIView { backButton.snp.makeConstraints { make in make.size.equalTo(32) - make.trailing.equalToSuperview().inset(16) + make.leading.equalToSuperview().inset(16) make.centerY.equalToSuperview() } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListViewModel.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListViewModel.swift index 44bcd9ffc4..d3f0444643 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListViewModel.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListViewModel.swift @@ -6,4 +6,5 @@ struct LiquidityPoolListViewModel { let moreButtonVisible: Bool let backgroundVisible: Bool let refreshAvailable: Bool + let isEmbed: Bool } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewPresenter.swift index 7b643645d2..8c8e604050 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewPresenter.swift @@ -68,4 +68,8 @@ extension LiquidityPoolsOverviewPresenter: LiquidityPoolsListModuleOutput { func shouldShowUserPools(_ shouldShow: Bool) { view?.changeUserPoolsVisibility(visible: shouldShow) } + + func didReceiveUserPoolCount(_ userPoolsCount: Int) { + view?.didReceiveUserPoolsCount(count: userPoolsCount) + } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewProtocols.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewProtocols.swift index d90b93c364..74e0767e5e 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewProtocols.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewProtocols.swift @@ -4,6 +4,7 @@ typealias LiquidityPoolsOverviewModuleCreationResult = (view: LiquidityPoolsOver protocol LiquidityPoolsOverviewViewInput: ControllerBackedProtocol { func changeUserPoolsVisibility(visible: Bool) + func didReceiveUserPoolsCount(count: Int) } protocol LiquidityPoolsOverviewViewOutput: AnyObject { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewController.swift index 9219cbfe26..8ef11feac7 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewController.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewController.swift @@ -80,6 +80,10 @@ extension LiquidityPoolsOverviewViewController: LiquidityPoolsOverviewViewInput func changeUserPoolsVisibility(visible: Bool) { rootView.userPoolsContainerView.isHidden = !visible } + + func didReceiveUserPoolsCount(count: Int) { + rootView.bind(userPoolsCount: count) + } } // MARK: - Localizable diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewLayout.swift index d90769cbbe..e8c1b11717 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewLayout.swift @@ -1,9 +1,18 @@ import UIKit +import SnapKit final class LiquidityPoolsOverviewViewLayout: UIView { + enum Constants { + static let topInset: CGFloat = 16 + static let bottomInset: CGFloat = 16 + static let sectionsOffset: CGFloat = 16 + static let navigationBarHeight: CGFloat = 62 + } + let navigationBar: BaseNavigationBar = { let bar = BaseNavigationBar() - bar.backgroundColor = R.color.colorBlack19() + bar.backgroundColor = .clear + bar.set(.present) return bar }() @@ -17,15 +26,22 @@ final class LiquidityPoolsOverviewViewLayout: UIView { let imageView = UIImageView() imageView.contentMode = .scaleAspectFill imageView.image = R.image.backgroundImage() - imageView.isHidden = true return imageView }() let scrollView = UIScrollView() - let scrollViewBackgroundView = UIFactory.default.createVerticalStackView(spacing: 16) + let containerStackView: UIStackView = { + let stackView = UIFactory.default.createVerticalStackView(spacing: 16) + stackView.distribution = .fill + return stackView + }() + let userPoolsContainerView = TriangularedBlurView() let availablePoolsContainerView = TriangularedBlurView() + var userPoolsHeightConstraint: Constraint? + var availablePoolsHeightConstraint: Constraint? + override init(frame: CGRect) { super.init(frame: frame) backgroundColor = R.color.colorBlack19() @@ -39,6 +55,10 @@ final class LiquidityPoolsOverviewViewLayout: UIView { fatalError("init(coder:) has not been implemented") } + func bind(userPoolsCount: Int) { + updateConstraints(with: userPoolsCount) + } + func addUserPoolsView(_ view: UIView) { userPoolsContainerView.addSubview(view) view.snp.makeConstraints { make in @@ -53,15 +73,34 @@ final class LiquidityPoolsOverviewViewLayout: UIView { } } + private func updateConstraints(with userPoolsCount: Int) { + let totalHeight = frame.size.height - Constants.navigationBarHeight + let isThereUserPoolsSection = userPoolsCount > 0 + + let userSectionHeight = isThereUserPoolsSection ? LiquidityPoolConstants.liquidityPoolsListHeaderHeight + LiquidityPoolConstants.liquidityPoolsListCellHeight * Double(userPoolsCount) + 12 : 0 + let sectionOffset = isThereUserPoolsSection ? Constants.sectionsOffset : 0 + let totalAvailableSectionHeight = totalHeight - Constants.topInset - Constants.bottomInset - userSectionHeight - sectionOffset + let availableSectionRowsCount = Int((totalAvailableSectionHeight - LiquidityPoolConstants.liquidityPoolsListHeaderHeight) / LiquidityPoolConstants.liquidityPoolsListCellHeight) + let availableSectionHeight: CGFloat = LiquidityPoolConstants.liquidityPoolsListHeaderHeight + LiquidityPoolConstants.liquidityPoolsListCellHeight * Double(availableSectionRowsCount) + 4 + + userPoolsContainerView.snp.updateConstraints { make in + make.height.equalTo(userSectionHeight) + make.leading.trailing.equalToSuperview().inset(16) + } + + availablePoolsContainerView.snp.updateConstraints { make in + make.height.equalTo(availableSectionHeight) + } + } + private func drawSubviews() { addSubview(backgroundImageView) addSubview(navigationBar) - addSubview(scrollView) - scrollView.addSubview(scrollViewBackgroundView) - scrollViewBackgroundView.addArrangedSubview(userPoolsContainerView) - scrollViewBackgroundView.addArrangedSubview(availablePoolsContainerView) + addSubview(containerStackView) + containerStackView.addArrangedSubview(userPoolsContainerView) + containerStackView.addArrangedSubview(availablePoolsContainerView) - navigationBar.setCenterViews([polkaswapImageView]) + navigationBar.setLeftViews([navigationBar.backButton, polkaswapImageView]) } private func setupConstraints() { @@ -73,22 +112,23 @@ final class LiquidityPoolsOverviewViewLayout: UIView { make.leading.trailing.top.equalToSuperview() } - scrollView.snp.makeConstraints { make in - make.leading.trailing.bottom.equalToSuperview() + containerStackView.snp.makeConstraints { make in make.top.equalTo(navigationBar.snp.bottom) - } - - scrollViewBackgroundView.snp.makeConstraints { make in - make.edges.equalToSuperview() - make.width.height.equalTo(self) + make.bottom.lessThanOrEqualToSuperview() + make.width.equalTo(self) + make.centerX.equalToSuperview() } userPoolsContainerView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(16) + userPoolsHeightConstraint = make.height.equalTo(0).constraint } availablePoolsContainerView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(16) + availablePoolsHeightConstraint = make.height.equalTo(0).constraint } + + updateConstraints(with: 0) } } diff --git a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewLayout.swift b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewLayout.swift index 3315d38c60..70d29a9c37 100644 --- a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewLayout.swift +++ b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewLayout.swift @@ -203,7 +203,7 @@ final class PolkaswapAdjustmentViewLayout: UIView { bannerButton.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(16) make.height.equalTo(139) - make.bottom.equalTo(previewButton.snp.top).inset(-16) + make.bottom.equalToSuperview().inset(UIConstants.actionHeight + UIConstants.bigOffset * 2) } let switchInputsView = createSwitchInputsView() From 4409996a4190fd083b29c358d8f6acbb34862d54 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Thu, 20 Jun 2024 12:27:18 +0500 Subject: [PATCH 047/115] Asset management. Logic for updating chains.json --- fearless.xcodeproj/project.pbxproj | 4 + .../xcshareddata/swiftpm/Package.resolved | 2 +- .../DataProvider/PriceProviderFactory.swift | 5 +- .../Common/Extension/SafeDictionary.swift | 74 +++++++++++++++++++ .../Common/Helpers/WalletAssetsObserver.swift | 30 ++------ .../ChainRegistry/ChainRegistry.swift | 8 +- 6 files changed, 94 insertions(+), 29 deletions(-) create mode 100644 fearless/Common/Extension/SafeDictionary.swift diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index ec5f761731..0bc8c2e8a0 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -222,6 +222,7 @@ 07F44F032BE4C7FF00570143 /* Web3 in Frameworks */ = {isa = PBXBuildFile; productRef = 07F44F022BE4C7FF00570143 /* Web3 */; }; 07F44F052BE4C7FF00570143 /* Web3ContractABI in Frameworks */ = {isa = PBXBuildFile; productRef = 07F44F042BE4C7FF00570143 /* Web3ContractABI */; }; 07F44F072BE4C7FF00570143 /* Web3PromiseKit in Frameworks */ = {isa = PBXBuildFile; productRef = 07F44F062BE4C7FF00570143 /* Web3PromiseKit */; }; + 07F65DEB2C240A19004A1728 /* SafeDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F65DEA2C240A19004A1728 /* SafeDictionary.swift */; }; 07F67CA32ACFC4910044047D /* WalletConnectCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F67CA22ACFC4910044047D /* WalletConnectCoordinator.swift */; }; 07F67CA52ACFEADB0044047D /* BigUInt+EthereumQuantity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F67CA42ACFEADB0044047D /* BigUInt+EthereumQuantity.swift */; }; 07F817F72AD4051A00B87358 /* Interaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F817F62AD4051A00B87358 /* Interaction.swift */; }; @@ -3281,6 +3282,7 @@ 07F2B75E28AA183C00280C38 /* PrintTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrintTimer.swift; sourceTree = ""; }; 07F2B76028AB806400280C38 /* SnapshotHotBootBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotHotBootBuilder.swift; sourceTree = ""; }; 07F2B76228ACDA7800280C38 /* RuntimeHotBootSnapshotFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeHotBootSnapshotFactory.swift; sourceTree = ""; }; + 07F65DEA2C240A19004A1728 /* SafeDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeDictionary.swift; sourceTree = ""; }; 07F67CA22ACFC4910044047D /* WalletConnectCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnectCoordinator.swift; sourceTree = ""; }; 07F67CA42ACFEADB0044047D /* BigUInt+EthereumQuantity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BigUInt+EthereumQuantity.swift"; sourceTree = ""; }; 07F817F62AD4051A00B87358 /* Interaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Interaction.swift; sourceTree = ""; }; @@ -9173,6 +9175,7 @@ FAA9BC402B8F17BA00A875BF /* Collection+Average.swift */, FA24FEFD2B95C32200CD9E04 /* Decimal+DoubleValue.swift */, FAC6CDA82BA814F20013A17E /* UIControl+Disable.swift */, + 07F65DEA2C240A19004A1728 /* SafeDictionary.swift */, ); path = Extension; sourceTree = ""; @@ -18409,6 +18412,7 @@ 4D822D169784790EBF173EEE /* WalletMainContainerPresenter.swift in Sources */, CFAC59D34B15A2FAC4CD8256 /* WalletMainContainerInteractor.swift in Sources */, 6FA6FA6944AD6519FB8A2AC0 /* WalletMainContainerViewController.swift in Sources */, + 07F65DEB2C240A19004A1728 /* SafeDictionary.swift in Sources */, FA7254412AC2E48500EC47A6 /* ABIParsing.swift in Sources */, D403B7725ED25E20A20F9D6A /* WalletMainContainerViewLayout.swift in Sources */, FA9A8F1B2A72573C008FA99F /* AlchemyRequest.swift in Sources */, diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index ecf69391a0..3ac93e9288 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -133,7 +133,7 @@ "location" : "https://github.com/soramitsu/shared-features-spm.git", "state" : { "branch" : "feature/xcm-routes", - "revision" : "ad712ce34d995276f9cae269afad7902353bd878" + "revision" : "1781f7d9a8ea8eccccfcfd21d8c951f65cff480a" } }, { diff --git a/fearless/Common/DataProvider/PriceProviderFactory.swift b/fearless/Common/DataProvider/PriceProviderFactory.swift index 530a5efa72..460fe220d8 100644 --- a/fearless/Common/DataProvider/PriceProviderFactory.swift +++ b/fearless/Common/DataProvider/PriceProviderFactory.swift @@ -10,7 +10,7 @@ protocol PriceProviderFactoryProtocol { class PriceProviderFactory { static let shared = PriceProviderFactory(storageFacade: SubstrateDataStorageFacade.shared) - private var providers: [AssetModel.PriceId: WeakWrapper] = [:] + private var providers: SafeDictionary = SafeDictionary(dict: [:]) private var remoteFetchTimer: Timer? private let storageFacade: StorageFacadeProtocol @@ -25,7 +25,8 @@ class PriceProviderFactory { } private func clearIfNeeded() { - providers = providers.filter { $0.value.target != nil } + let filtred = providers.dictionary.filter { $0.value.target != nil } + providers = SafeDictionary(dict: filtred) } } diff --git a/fearless/Common/Extension/SafeDictionary.swift b/fearless/Common/Extension/SafeDictionary.swift new file mode 100644 index 0000000000..1f7bac4a57 --- /dev/null +++ b/fearless/Common/Extension/SafeDictionary.swift @@ -0,0 +1,74 @@ +import Foundation + +final class SafeDictionary: Collection { + private(set) var dictionary: [V: T] + private let concurrentQueue = DispatchQueue( + label: "jp.co.soramitsu.fearless.safe.dict.queue.\(UUID().uuidString)", + attributes: .concurrent + ) + + var keys: Dictionary.Keys { + concurrentQueue.sync { + self.dictionary.keys + } + } + + var values: Dictionary.Values { + concurrentQueue.sync { + self.dictionary.values + } + } + + var startIndex: Dictionary.Index { + concurrentQueue.sync { + self.dictionary.startIndex + } + } + + var endIndex: Dictionary.Index { + concurrentQueue.sync { + self.dictionary.endIndex + } + } + + init(dict: [V: T] = [V: T]()) { + dictionary = dict + } + + func index(after i: Dictionary.Index) -> Dictionary.Index { + concurrentQueue.sync { + self.dictionary.index(after: i) + } + } + + func removeValue(forKey key: V) { + concurrentQueue.async(flags: .barrier) { [weak self] in + self?.dictionary.removeValue(forKey: key) + } + } + + func removeAll() { + concurrentQueue.async(flags: .barrier) { [weak self] in + self?.dictionary.removeAll() + } + } + + subscript(key: V) -> T? { + set(newValue) { + concurrentQueue.async(flags: .barrier) { [weak self] in + self?.dictionary[key] = newValue + } + } + get { + concurrentQueue.sync { + self.dictionary[key] + } + } + } + + subscript(index: Dictionary.Index) -> Dictionary.Element { + concurrentQueue.sync { + self.dictionary[index] + } + } +} diff --git a/fearless/Common/Helpers/WalletAssetsObserver.swift b/fearless/Common/Helpers/WalletAssetsObserver.swift index efa70c1c0a..f1fbfa2a48 100644 --- a/fearless/Common/Helpers/WalletAssetsObserver.swift +++ b/fearless/Common/Helpers/WalletAssetsObserver.swift @@ -23,8 +23,6 @@ final class WalletAssetsObserverImpl: WalletAssetsObserver { DispatchQueue(label: "co.jp.soramitsu.asset.observer.deliveryQueue") }() - private var currentTask: Task? - init( wallet: MetaAccountModel, chainRegistry: ChainRegistryProtocol, @@ -71,36 +69,18 @@ final class WalletAssetsObserverImpl: WalletAssetsObserver { self, runningInQueue: walletAssetsObserverQueue ) { [weak self] changes in - guard self?.wallet.assetsVisibility.isEmpty == true else { - let updateChanges = changes.filter { - switch $0 { - case .update: return true - default: return false - } - } - self?.handleChains(changes: updateChanges, accounts: nil) - return - } - - let insertChanges = changes.filter { - switch $0 { - case .insert: return true - default: return false - } - } - self?.handleChains(changes: insertChanges, accounts: nil) + self?.handleChains(changes: changes, accounts: nil) } } func throttle() { - currentTask?.cancel() chainRegistry.chainsUnsubscribe(self) } // MARK: - Private methods private func handleChains(changes: [DataProviderChange], accounts: [ChainAccountModel]?) { - currentTask = Task { + Task { let result = await withTaskGroup( of: (ChainModel, [ChainAssetId: AccountInfo?]).self, returning: [ChainModel: [ChainAssetId: AccountInfo?]].self, @@ -136,7 +116,11 @@ final class WalletAssetsObserverImpl: WalletAssetsObserver { accounts: [ChainAccountModel]?, group: inout TaskGroup<(ChainModel, [ChainAssetId: AccountInfo?])> ) { - guard !chain.disabled else { + let chainAssetsIds = chain.chainAssets.map { $0.identifier } + let existVisibility = wallet.assetsVisibility.map { $0.assetId } + let perhapsExistVisibility = existVisibility.filter { chainAssetsIds.contains($0) } + + guard !chain.disabled, chainAssetsIds.count > perhapsExistVisibility.count else { return } if let accounts { diff --git a/fearless/Common/Services/ChainRegistry/ChainRegistry.swift b/fearless/Common/Services/ChainRegistry/ChainRegistry.swift index dc6d316f70..b89ef5234b 100644 --- a/fearless/Common/Services/ChainRegistry/ChainRegistry.swift +++ b/fearless/Common/Services/ChainRegistry/ChainRegistry.swift @@ -461,10 +461,12 @@ extension ChainRegistry: SSFChainRegistry.ChainRegistryProtocol { usedRuntimePaths _: [String: [String]], runtimeItem _: SSFModels.RuntimeMetadataItemProtocol? ) async throws -> SSFRuntimeCodingService.RuntimeProviderProtocol { - let runtimeProvider = readLock.concurrentlyRead { runtimeProviderPool.getRuntimeProvider(for: chainId) } - guard let runtimeProvider else { - throw ChainRegistryError.runtimeMetadaUnavailable + guard let chain = chains.first(where: { $0.chainId == chainId }) else { + throw ChainRegistryError.chainUnavailable(chainId: chainId) } + let chainTypes = chainsTypesMap[chainId] + + let runtimeProvider = runtimeProviderPool.setupRuntimeProvider(for: chain, chainTypes: chainTypes) return runtimeProvider } From fd3651ab5e724efd1b9db249a5c11e039d1b5dd6 Mon Sep 17 00:00:00 2001 From: Alex Lebedko Date: Mon, 24 Jun 2024 17:43:54 +0700 Subject: [PATCH 048/115] lp apy cache, bug fix --- fearless.xcodeproj/project.pbxproj | 16 +----- .../Model/WalletConnectExtrinsic.swift | 1 + .../Model/WalletConnectPayload.swift | 1 + .../Requests/StakingRewardsRequest.swift | 1 + .../iconLpBanner.imageset/Banner.pdf | Bin 0 -> 131841 bytes .../iconLpBanner.imageset/Contents.json | 2 +- .../iconLpBanner.imageset/lp_banner.pdf | Bin 180797 -> 0 bytes .../Rewards/SubqueryRewardsSource.swift | 1 + .../Sources/Rewards/SubqueryStakeSource.swift | 1 + .../SubqueryEraStakersInfoSource.swift | 1 + .../Subquery/Data/GraphQLResponse.swift | 34 ------------ .../Data/SubqueryEraValidatorInfo.swift | 1 + .../Subquery/Data/SubqueryRewardData.swift | 1 + .../Subquery/Data/SubqueryStakeData.swift | 1 + .../Data/SubscanRawExtrinsicData.swift | 1 + .../Operation/StorageDecodingOperation.swift | 1 + .../ChainsTypesSyncService.swift | 1 + .../PayoutRewardsService/BatchMapper.swift | 1 + .../ControllerMapper.swift | 1 + .../PayoutRewardsService/NominateMapper.swift | 1 + .../SubscanQueryService.swift | 1 + .../Substrate/Calls/PolkaswapSwapCall.swift | 1 + .../Common/Substrate/Types/AccountInfo.swift | 1 + .../Types/CrowdloanContributionMapper.swift | 1 + .../Types/CrowdloanLastContribution.swift | 1 + .../Common/Substrate/Types/EventRecord.swift | 1 + ...chainSubqueryHistoryOperationFactory.swift | 1 + ...chainSubsquidHistoryOperationFactory.swift | 1 + .../SubqueryRewardOperationFactory.swift | 1 + .../SubsquidRewardOperationFactory.swift | 1 + .../Banners/BannerCollectionViewCell.swift | 52 ++++++++++++++---- .../Modules/Banners/BannersAssembly.swift | 10 +++- .../Modules/Banners/BannersInteractor.swift | 51 +++++++++-------- .../Modules/Banners/BannersPresenter.swift | 43 +++++++++++---- .../Modules/Banners/BannersProtocols.swift | 7 +++ fearless/Modules/Banners/BannersRouter.swift | 13 +++++ .../Banners/BannersViewController.swift | 18 ++---- .../Banners/BannersViewModelFactory.swift | 46 ++++++++++++---- .../Bifrost/BifrostBonusService.swift | 1 + .../LiquidityPoolDetailsInteractor.swift | 10 ++-- .../LiquidityPoolSupplyInteractor.swift | 10 ++-- ...LiquidityPoolSupplyConfirmInteractor.swift | 10 ++-- ...vailableLiquidityPoolsListInteractor.swift | 40 ++++++++++++-- ...AvailableLiquidityPoolsListPresenter.swift | 1 - .../UserLiquidityPoolsListInteractor.swift | 44 ++++++++++++--- .../UserLiquidityPoolsListPresenter.swift | 3 +- ...erLiquidityPoolsListViewModelFactory.swift | 34 +++++++++++- .../LiquidityPoolsListViewController.swift | 40 ++++++++++++++ .../LiquidityPoolsListViewLayout.swift | 11 +++- .../ChainAssetListAssembly.swift | 2 +- .../ChainAssetListPresenter.swift | 2 + .../PolkaswapAdjustmentAssembly.swift | 17 ++++++ .../PolkaswapAdjustmentPresenter.swift | 13 +++++ .../PolkaswapAdjustmentProtocols.swift | 1 + .../PolkaswapAdjustmentViewController.swift | 23 ++++++-- .../PolkaswapAdjustmentViewLayout.swift | 27 ++++++--- .../Modules/RawData/RawDataAssembly.swift | 1 + .../Modules/RawData/RawDataPresenter.swift | 1 + 58 files changed, 442 insertions(+), 166 deletions(-) create mode 100644 fearless/Assets.xcassets/iconLpBanner.imageset/Banner.pdf delete mode 100644 fearless/Assets.xcassets/iconLpBanner.imageset/lp_banner.pdf delete mode 100644 fearless/Common/Network/BlockExplorer/Subquery/Data/GraphQLResponse.swift diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index 8dff55c58a..6b85a909c9 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -3075,7 +3075,6 @@ FAFFAE7C29AC84B10074AF1F /* KmmCallCodingPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFFAE4D29AC84B10074AF1F /* KmmCallCodingPath.swift */; }; FAFFAE7D29AC84B10074AF1F /* SubqueryPageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFFAE4F29AC84B10074AF1F /* SubqueryPageInfo.swift */; }; FAFFAE7E29AC84B10074AF1F /* SubqueryDelegatorHistoryData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFFAE5029AC84B10074AF1F /* SubqueryDelegatorHistoryData.swift */; }; - FAFFAE7F29AC84B10074AF1F /* GraphQLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFFAE5129AC84B10074AF1F /* GraphQLResponse.swift */; }; FAFFAE8029AC84B10074AF1F /* SubqueryTransfer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFFAE5229AC84B10074AF1F /* SubqueryTransfer.swift */; }; FAFFAE8129AC84B10074AF1F /* SubqueryDelegatorHistoryElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFFAE5329AC84B10074AF1F /* SubqueryDelegatorHistoryElement.swift */; }; FAFFAE8229AC84B10074AF1F /* SubqueryDelegatorHistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFFAE5429AC84B10074AF1F /* SubqueryDelegatorHistoryItem.swift */; }; @@ -5642,7 +5641,6 @@ FA887A442C107A1100CA720F /* LiquidityPoolSupplyConfirmViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyConfirmViewModel.swift; sourceTree = ""; }; FA887A462C107A4300CA720F /* LiquidityPoolSupplyConfirmViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyConfirmViewModelFactory.swift; sourceTree = ""; }; FA887A482C1C19DB00CA720F /* WarningView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WarningView.swift; sourceTree = ""; }; - FA887A4B2C1FFFD400CA720F /* shared-features-spm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "shared-features-spm"; path = "../shared-features-spm"; sourceTree = ""; }; FA8C34B051607218638BA851 /* FiltersProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FiltersProtocols.swift; sourceTree = ""; }; FA8ED43228FD960F00EBB712 /* YourValidatorListFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YourValidatorListFlow.swift; sourceTree = ""; }; FA8ED43528FD983A00EBB712 /* YourValidatorListRelaychainStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YourValidatorListRelaychainStrategy.swift; sourceTree = ""; }; @@ -6218,7 +6216,6 @@ FAFFAE4D29AC84B10074AF1F /* KmmCallCodingPath.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KmmCallCodingPath.swift; sourceTree = ""; }; FAFFAE4F29AC84B10074AF1F /* SubqueryPageInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubqueryPageInfo.swift; sourceTree = ""; }; FAFFAE5029AC84B10074AF1F /* SubqueryDelegatorHistoryData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubqueryDelegatorHistoryData.swift; sourceTree = ""; }; - FAFFAE5129AC84B10074AF1F /* GraphQLResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphQLResponse.swift; sourceTree = ""; }; FAFFAE5229AC84B10074AF1F /* SubqueryTransfer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubqueryTransfer.swift; sourceTree = ""; }; FAFFAE5329AC84B10074AF1F /* SubqueryDelegatorHistoryElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubqueryDelegatorHistoryElement.swift; sourceTree = ""; }; FAFFAE5429AC84B10074AF1F /* SubqueryDelegatorHistoryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubqueryDelegatorHistoryItem.swift; sourceTree = ""; }; @@ -9154,7 +9151,6 @@ 8490139F24A80984008F705E = { isa = PBXGroup; children = ( - FA887A4A2C1FFFD400CA720F /* Packages */, 849013AA24A80984008F705E /* fearless */, 849013C124A80986008F705E /* fearlessTests */, 8438E1D024BFAAD2001BDB13 /* fearlessIntegrationTests */, @@ -14084,14 +14080,6 @@ path = ViewModel; sourceTree = ""; }; - FA887A4A2C1FFFD400CA720F /* Packages */ = { - isa = PBXGroup; - children = ( - FA887A4B2C1FFFD400CA720F /* shared-features-spm */, - ); - name = Packages; - sourceTree = ""; - }; FA8ED43128FD8D6200EBB712 /* Flows */ = { isa = PBXGroup; children = ( @@ -15706,7 +15694,6 @@ children = ( FAFFAE4F29AC84B10074AF1F /* SubqueryPageInfo.swift */, FAFFAE5029AC84B10074AF1F /* SubqueryDelegatorHistoryData.swift */, - FAFFAE5129AC84B10074AF1F /* GraphQLResponse.swift */, FAFFAE5229AC84B10074AF1F /* SubqueryTransfer.swift */, FAFFAE5329AC84B10074AF1F /* SubqueryDelegatorHistoryElement.swift */, FAFFAE5429AC84B10074AF1F /* SubqueryDelegatorHistoryItem.swift */, @@ -16764,7 +16751,6 @@ 8406B5AF26FBE7EF00635B61 /* SelectionIconDetailsTableViewCell.swift in Sources */, FA256A23274CE7D600875A53 /* MoonbeamAgreeRemarkInfo.swift in Sources */, 849014BE24AA87E4008F705E /* PinSetupPresenter.swift in Sources */, - FAFFAE7F29AC84B10074AF1F /* GraphQLResponse.swift in Sources */, 849ABE63262785F200011A2A /* ControllerMapper.swift in Sources */, FAC0BBD9291D0EB000E6F106 /* SendViewController.swift in Sources */, FAA0139D28DA131B000A5230 /* StakingBondMorePoolViewModelState.swift in Sources */, @@ -19867,7 +19853,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/soramitsu/shared-features-spm.git"; requirement = { - branch = "storage-requests-update"; + branch = "liquidity-pools-fw"; kind = branch; }; }; diff --git a/fearless/ApplicationLayer/Services/WalletConnect/Model/WalletConnectExtrinsic.swift b/fearless/ApplicationLayer/Services/WalletConnect/Model/WalletConnectExtrinsic.swift index ee2414c74f..df6a4d079c 100644 --- a/fearless/ApplicationLayer/Services/WalletConnect/Model/WalletConnectExtrinsic.swift +++ b/fearless/ApplicationLayer/Services/WalletConnect/Model/WalletConnectExtrinsic.swift @@ -1,6 +1,7 @@ import Foundation import SSFUtils import BigInt +import SSFModels struct WalletConnectExtrinsic: Codable { let address: String diff --git a/fearless/ApplicationLayer/Services/WalletConnect/Model/WalletConnectPayload.swift b/fearless/ApplicationLayer/Services/WalletConnect/Model/WalletConnectPayload.swift index 3fb2172e4c..714bef2f89 100644 --- a/fearless/ApplicationLayer/Services/WalletConnect/Model/WalletConnectPayload.swift +++ b/fearless/ApplicationLayer/Services/WalletConnect/Model/WalletConnectPayload.swift @@ -1,6 +1,7 @@ import Foundation import Commons import SSFUtils +import SSFModels struct WalletConnectPayload { let address: String? diff --git a/fearless/ApplicationLayer/StakingRewards/Requests/StakingRewardsRequest.swift b/fearless/ApplicationLayer/StakingRewards/Requests/StakingRewardsRequest.swift index e1a5e1b846..bc23af7a6d 100644 --- a/fearless/ApplicationLayer/StakingRewards/Requests/StakingRewardsRequest.swift +++ b/fearless/ApplicationLayer/StakingRewards/Requests/StakingRewardsRequest.swift @@ -1,6 +1,7 @@ import Foundation import SSFUtils import RobinHood +import SSFModels class StakingRewardsRequest: RequestConfig { init( diff --git a/fearless/Assets.xcassets/iconLpBanner.imageset/Banner.pdf b/fearless/Assets.xcassets/iconLpBanner.imageset/Banner.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8cab036f3fa20c6bc95e41b98c1178fbe2e140e1 GIT binary patch literal 131841 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 LH)c#!+@${iIPZWb literal 0 HcmV?d00001 diff --git a/fearless/Assets.xcassets/iconLpBanner.imageset/Contents.json b/fearless/Assets.xcassets/iconLpBanner.imageset/Contents.json index 348e8808b6..b6a4904df5 100644 --- a/fearless/Assets.xcassets/iconLpBanner.imageset/Contents.json +++ b/fearless/Assets.xcassets/iconLpBanner.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "lp_banner.pdf", + "filename" : "Banner.pdf", "idiom" : "universal" } ], diff --git a/fearless/Assets.xcassets/iconLpBanner.imageset/lp_banner.pdf b/fearless/Assets.xcassets/iconLpBanner.imageset/lp_banner.pdf deleted file mode 100644 index 696df1f940e514a085000f2f76c20fbfe3b09511..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 180797 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 u!4aea0@~KVx%%6VT~VyN@OO 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 SYr6;n6eA#%e0+*pO8*C0GP>me diff --git a/fearless/Common/DataProvider/Sources/Rewards/SubqueryRewardsSource.swift b/fearless/Common/DataProvider/Sources/Rewards/SubqueryRewardsSource.swift index d4f0f5590e..18d7970842 100644 --- a/fearless/Common/DataProvider/Sources/Rewards/SubqueryRewardsSource.swift +++ b/fearless/Common/DataProvider/Sources/Rewards/SubqueryRewardsSource.swift @@ -2,6 +2,7 @@ import Foundation import RobinHood import SSFUtils import BigInt +import SSFModels final class ParachainSubqueryRewardsSource { typealias Model = [SubqueryRewardItemData] diff --git a/fearless/Common/DataProvider/Sources/Rewards/SubqueryStakeSource.swift b/fearless/Common/DataProvider/Sources/Rewards/SubqueryStakeSource.swift index de45574d58..ed26b04fdc 100644 --- a/fearless/Common/DataProvider/Sources/Rewards/SubqueryStakeSource.swift +++ b/fearless/Common/DataProvider/Sources/Rewards/SubqueryStakeSource.swift @@ -2,6 +2,7 @@ import Foundation import RobinHood import SSFUtils import BigInt +import SSFModels final class SubqueryStakeSource { typealias Model = [SubqueryStakeChangeData] diff --git a/fearless/Common/DataProvider/Sources/SubqueryEraStakersInfoSource.swift b/fearless/Common/DataProvider/Sources/SubqueryEraStakersInfoSource.swift index 133839e8e9..3e6f7997e4 100644 --- a/fearless/Common/DataProvider/Sources/SubqueryEraStakersInfoSource.swift +++ b/fearless/Common/DataProvider/Sources/SubqueryEraStakersInfoSource.swift @@ -1,6 +1,7 @@ import RobinHood import Foundation import SSFUtils +import SSFModels final class SubqueryEraStakersInfoSource { let url: URL diff --git a/fearless/Common/Network/BlockExplorer/Subquery/Data/GraphQLResponse.swift b/fearless/Common/Network/BlockExplorer/Subquery/Data/GraphQLResponse.swift deleted file mode 100644 index 1abc64c72d..0000000000 --- a/fearless/Common/Network/BlockExplorer/Subquery/Data/GraphQLResponse.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Foundation -import SSFUtils - -struct SubqueryErrors: Error, Decodable { - struct SubqueryError: Error, Decodable { - let message: String - } - - let errors: [SubqueryError] -} - -enum GraphQLResponse: Decodable { - case data(_ value: D) - case errors(_ value: SubqueryErrors) - - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - - let json = try container.decode(JSON.self) - - if let data = json.data { - let value = try data.map(to: D.self) - self = .data(value) - } else if let errors = json.errors { - let values = try errors.map(to: [SubqueryErrors.SubqueryError].self) - self = .errors(SubqueryErrors(errors: values)) - } else { - throw DecodingError.dataCorruptedError( - in: container, - debugDescription: "unexpected value" - ) - } - } -} diff --git a/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryEraValidatorInfo.swift b/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryEraValidatorInfo.swift index f4b3b27b27..a3fea03ee2 100644 --- a/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryEraValidatorInfo.swift +++ b/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryEraValidatorInfo.swift @@ -1,4 +1,5 @@ import SSFUtils +import SSFModels struct SubqueryEraValidatorInfo { let address: AccountAddress diff --git a/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryRewardData.swift b/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryRewardData.swift index 769b538409..f3607b7f37 100644 --- a/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryRewardData.swift +++ b/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryRewardData.swift @@ -1,6 +1,7 @@ import Foundation import SSFUtils import BigInt +import SSFModels struct SubqueryRewardItemData: Equatable, Codable { let eventId: String diff --git a/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryStakeData.swift b/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryStakeData.swift index 5c53c19c98..ecc78b605a 100644 --- a/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryStakeData.swift +++ b/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryStakeData.swift @@ -1,6 +1,7 @@ import Foundation import SSFUtils import BigInt +import SSFModels struct SubqueryStakeChangeData { let eventId: String diff --git a/fearless/Common/Network/Subscan/Data/SubscanRawExtrinsicData.swift b/fearless/Common/Network/Subscan/Data/SubscanRawExtrinsicData.swift index 324c4f0f5e..47c88adca7 100644 --- a/fearless/Common/Network/Subscan/Data/SubscanRawExtrinsicData.swift +++ b/fearless/Common/Network/Subscan/Data/SubscanRawExtrinsicData.swift @@ -1,5 +1,6 @@ import Foundation import SSFUtils +import SSFModels struct SubscanRawExtrinsicsData: Decodable { let count: Int diff --git a/fearless/Common/Operation/StorageDecodingOperation.swift b/fearless/Common/Operation/StorageDecodingOperation.swift index b76fade666..3834e7a05b 100644 --- a/fearless/Common/Operation/StorageDecodingOperation.swift +++ b/fearless/Common/Operation/StorageDecodingOperation.swift @@ -2,6 +2,7 @@ import Foundation import SSFUtils import RobinHood import SSFRuntimeCodingService +import SSFModels enum StorageDecodingOperationError: Error { case missingRequiredParams diff --git a/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/ChainsTypesSyncService.swift b/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/ChainsTypesSyncService.swift index 3a72e80207..3719d2409d 100644 --- a/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/ChainsTypesSyncService.swift +++ b/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/ChainsTypesSyncService.swift @@ -1,6 +1,7 @@ import Foundation import SSFUtils import RobinHood +import SSFModels enum ChainsTypesSyncError: Error { case missingChainId diff --git a/fearless/Common/Services/PayoutRewardsService/BatchMapper.swift b/fearless/Common/Services/PayoutRewardsService/BatchMapper.swift index 199de1462b..260800f86c 100644 --- a/fearless/Common/Services/PayoutRewardsService/BatchMapper.swift +++ b/fearless/Common/Services/PayoutRewardsService/BatchMapper.swift @@ -1,5 +1,6 @@ import Foundation import SSFUtils +import SSFModels final class BatchMapper: Mapping { typealias InputType = JSON diff --git a/fearless/Common/Services/PayoutRewardsService/ControllerMapper.swift b/fearless/Common/Services/PayoutRewardsService/ControllerMapper.swift index edf0342e8c..08ceb2ad81 100644 --- a/fearless/Common/Services/PayoutRewardsService/ControllerMapper.swift +++ b/fearless/Common/Services/PayoutRewardsService/ControllerMapper.swift @@ -1,5 +1,6 @@ import Foundation import SSFUtils +import SSFModels final class ControllerMapper: Mapping { typealias InputType = JSON diff --git a/fearless/Common/Services/PayoutRewardsService/NominateMapper.swift b/fearless/Common/Services/PayoutRewardsService/NominateMapper.swift index cdafb4e58a..851a032095 100644 --- a/fearless/Common/Services/PayoutRewardsService/NominateMapper.swift +++ b/fearless/Common/Services/PayoutRewardsService/NominateMapper.swift @@ -1,5 +1,6 @@ import Foundation import SSFUtils +import SSFModels final class NominateMapper: Mapping { typealias InputType = JSON diff --git a/fearless/Common/Services/SubscanQueryService/SubscanQueryService.swift b/fearless/Common/Services/SubscanQueryService/SubscanQueryService.swift index 37f042f6b6..67f07c9ad9 100644 --- a/fearless/Common/Services/SubscanQueryService/SubscanQueryService.swift +++ b/fearless/Common/Services/SubscanQueryService/SubscanQueryService.swift @@ -1,6 +1,7 @@ import Foundation import RobinHood import SSFUtils +import SSFModels final class SubscanQueryService: Longrunable { typealias ResultType = R diff --git a/fearless/Common/Substrate/Calls/PolkaswapSwapCall.swift b/fearless/Common/Substrate/Calls/PolkaswapSwapCall.swift index a6b4e63a9b..25d2f667e3 100644 --- a/fearless/Common/Substrate/Calls/PolkaswapSwapCall.swift +++ b/fearless/Common/Substrate/Calls/PolkaswapSwapCall.swift @@ -1,6 +1,7 @@ import SSFUtils import BigInt import Foundation +import SSFModels class SwapAmount: Codable { var desired: BigUInt diff --git a/fearless/Common/Substrate/Types/AccountInfo.swift b/fearless/Common/Substrate/Types/AccountInfo.swift index f647fc2d46..c122bd0b0f 100644 --- a/fearless/Common/Substrate/Types/AccountInfo.swift +++ b/fearless/Common/Substrate/Types/AccountInfo.swift @@ -2,6 +2,7 @@ import Foundation import SSFUtils import RobinHood import BigInt +import SSFModels // MARK: - Normal diff --git a/fearless/Common/Substrate/Types/CrowdloanContributionMapper.swift b/fearless/Common/Substrate/Types/CrowdloanContributionMapper.swift index 259ac8ef5c..e26f07d203 100644 --- a/fearless/Common/Substrate/Types/CrowdloanContributionMapper.swift +++ b/fearless/Common/Substrate/Types/CrowdloanContributionMapper.swift @@ -1,6 +1,7 @@ import Foundation import SSFUtils import BigInt +import SSFModels struct CrowdloanContribution: Decodable { enum CodingKeys: String, CodingKey { diff --git a/fearless/Common/Substrate/Types/CrowdloanLastContribution.swift b/fearless/Common/Substrate/Types/CrowdloanLastContribution.swift index f67961b499..03b1367df3 100644 --- a/fearless/Common/Substrate/Types/CrowdloanLastContribution.swift +++ b/fearless/Common/Substrate/Types/CrowdloanLastContribution.swift @@ -1,5 +1,6 @@ import Foundation import SSFUtils +import SSFModels enum CrowdloanLastContribution: Equatable { static let neverField = "Never" diff --git a/fearless/Common/Substrate/Types/EventRecord.swift b/fearless/Common/Substrate/Types/EventRecord.swift index df3592aa32..c53997a663 100644 --- a/fearless/Common/Substrate/Types/EventRecord.swift +++ b/fearless/Common/Substrate/Types/EventRecord.swift @@ -1,5 +1,6 @@ import Foundation import SSFUtils +import SSFModels struct EventRecord: Decodable { enum CodingKeys: String, CodingKey { diff --git a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Staking/ParachainSubqueryHistoryOperationFactory.swift b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Staking/ParachainSubqueryHistoryOperationFactory.swift index 768144ef93..142689607b 100644 --- a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Staking/ParachainSubqueryHistoryOperationFactory.swift +++ b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Staking/ParachainSubqueryHistoryOperationFactory.swift @@ -1,6 +1,7 @@ import Foundation import RobinHood import SSFUtils +import SSFModels enum SubqueryHistoryOperationFactoryError: Error { case urlMissing diff --git a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Staking/ParachainSubsquidHistoryOperationFactory.swift b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Staking/ParachainSubsquidHistoryOperationFactory.swift index 44b8d018e0..5610d4a805 100644 --- a/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Staking/ParachainSubsquidHistoryOperationFactory.swift +++ b/fearless/CoreLayer/OperationFactory/BlockExplorer/History/Staking/ParachainSubsquidHistoryOperationFactory.swift @@ -1,6 +1,7 @@ import Foundation import RobinHood import SSFUtils +import SSFModels enum SubsquidHistoryOperationFactoryError: Error { case urlMissing diff --git a/fearless/CoreLayer/OperationFactory/BlockExplorer/Rewards/SubqueryRewardOperationFactory.swift b/fearless/CoreLayer/OperationFactory/BlockExplorer/Rewards/SubqueryRewardOperationFactory.swift index df2dd22369..188cbca4e4 100644 --- a/fearless/CoreLayer/OperationFactory/BlockExplorer/Rewards/SubqueryRewardOperationFactory.swift +++ b/fearless/CoreLayer/OperationFactory/BlockExplorer/Rewards/SubqueryRewardOperationFactory.swift @@ -1,6 +1,7 @@ import Foundation import RobinHood import SSFUtils +import SSFModels enum SubqueryRewardOperationFactoryError: Error { case urlMissing diff --git a/fearless/CoreLayer/OperationFactory/BlockExplorer/Rewards/SubsquidRewardOperationFactory.swift b/fearless/CoreLayer/OperationFactory/BlockExplorer/Rewards/SubsquidRewardOperationFactory.swift index 4806a9e56d..a620884abe 100644 --- a/fearless/CoreLayer/OperationFactory/BlockExplorer/Rewards/SubsquidRewardOperationFactory.swift +++ b/fearless/CoreLayer/OperationFactory/BlockExplorer/Rewards/SubsquidRewardOperationFactory.swift @@ -2,6 +2,7 @@ import Foundation import RobinHood import SSFUtils import SoraFoundation +import SSFModels enum ArrosquidRewardOperationFactoryError: Error { case urlMissing diff --git a/fearless/Modules/Banners/BannerCollectionViewCell.swift b/fearless/Modules/Banners/BannerCollectionViewCell.swift index 163ff179fb..57fc8d210c 100644 --- a/fearless/Modules/Banners/BannerCollectionViewCell.swift +++ b/fearless/Modules/Banners/BannerCollectionViewCell.swift @@ -7,15 +7,18 @@ struct BannerCellViewModel { let buttonTitle: String let image: UIImage let dismissable: Bool + let fullsizeImage: Bool + let bannerType: Banners } protocol BannerCellectionCellDelegate: AnyObject { - func didActionButtonTapped(indexPath: IndexPath?) - func didCloseButtonTapped(indexPath: IndexPath?) + func didActionButtonTapped(banner: Banners) + func didCloseButtonTapped(banner: Banners) } final class BannerCollectionViewCell: UICollectionViewCell { weak var delegate: BannerCellectionCellDelegate? + private var viewModel: BannerCellViewModel? let titleLabel: UILabel = { let label = UILabel() @@ -79,20 +82,45 @@ final class BannerCollectionViewCell: UICollectionViewCell { } func bind(viewModel: BannerCellViewModel) { + self.viewModel = viewModel + titleLabel.text = viewModel.title subtitleLabel.text = viewModel.subtitle actionButton.imageWithTitleView?.title = viewModel.buttonTitle imageView.image = viewModel.image closeButton.isHidden = !viewModel.dismissable + + updateImageConstraints(fullsizeImage: viewModel.fullsizeImage) } private func bindActions() { actionButton.addAction { [weak self] in - self?.delegate?.didActionButtonTapped(indexPath: self?.indexPath) + guard let banner = self?.viewModel?.bannerType else { + return + } + + self?.delegate?.didActionButtonTapped(banner: banner) } closeButton.addAction { [weak self] in - self?.delegate?.didCloseButtonTapped(indexPath: self?.indexPath) + + guard let banner = self?.viewModel?.bannerType else { + return + } + + self?.delegate?.didCloseButtonTapped(banner: banner) + } + } + + private func updateImageConstraints(fullsizeImage: Bool) { + if fullsizeImage { + imageView.snp.remakeConstraints { make in + make.leading.trailing.bottom.equalToSuperview() + } + } else { + imageView.snp.remakeConstraints { make in + make.trailing.bottom.equalToSuperview() + } } } @@ -102,19 +130,25 @@ final class BannerCollectionViewCell: UICollectionViewCell { make.trailing.bottom.equalToSuperview() } + addSubview(closeButton) + closeButton.snp.makeConstraints { make in + make.top.trailing.equalToSuperview().inset(UIConstants.defaultOffset) + make.size.equalTo(UIConstants.roundedCloseButtonSize) + } + addSubview(titleLabel) titleLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) titleLabel.snp.makeConstraints { make in make.top.equalToSuperview().offset(16) make.leading.equalToSuperview().offset(UIConstants.bigOffset) - make.trailing.equalTo(imageView.snp.leading).inset(16) + make.trailing.equalTo(closeButton.snp.leading).inset(16) } addSubview(subtitleLabel) subtitleLabel.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(UIConstants.defaultOffset) make.leading.equalToSuperview().offset(UIConstants.bigOffset) - make.trailing.equalTo(imageView.snp.leading).inset(16) + make.trailing.equalTo(closeButton.snp.leading).inset(16) } addSubview(actionButton) @@ -125,11 +159,5 @@ final class BannerCollectionViewCell: UICollectionViewCell { make.height.equalTo(32) make.width.greaterThanOrEqualTo(102) } - - addSubview(closeButton) - closeButton.snp.makeConstraints { make in - make.top.trailing.equalToSuperview().inset(UIConstants.defaultOffset) - make.size.equalTo(UIConstants.roundedCloseButtonSize) - } } } diff --git a/fearless/Modules/Banners/BannersAssembly.swift b/fearless/Modules/Banners/BannersAssembly.swift index 4cda660b7f..1993938261 100644 --- a/fearless/Modules/Banners/BannersAssembly.swift +++ b/fearless/Modules/Banners/BannersAssembly.swift @@ -3,7 +3,11 @@ import SoraFoundation import RobinHood final class BannersAssembly { - static func configureModule(output: BannersModuleOutput?) -> BannersModuleCreationResult? { + static func configureModule( + output: BannersModuleOutput?, + type: BannersModuleType, + wallet: MetaAccountModel? + ) -> BannersModuleCreationResult? { let localizationManager = LocalizationManager.shared let walletProvider = UserDataStorageFacade.shared @@ -29,7 +33,9 @@ final class BannersAssembly { moduleOutput: output, interactor: interactor, router: router, - localizationManager: localizationManager + localizationManager: localizationManager, + type: type, + wallet: wallet ) let view = BannersViewController( diff --git a/fearless/Modules/Banners/BannersInteractor.swift b/fearless/Modules/Banners/BannersInteractor.swift index 5355fd7a85..4fcc15c092 100644 --- a/fearless/Modules/Banners/BannersInteractor.swift +++ b/fearless/Modules/Banners/BannersInteractor.swift @@ -26,31 +26,6 @@ final class BannersInteractor { } // MARK: - Private methods - - private func subscribeToWallet() { - let updateClosure: ([DataProviderChange]) -> Void = { [weak self] changes in - guard let selectedWallet = changes.firstToLastChange(filter: { wallet in - wallet.isSelected - }) else { - return - } - self?.output?.didReceive(wallet: selectedWallet.info) - } - - let failureClosure: (Error) -> Void = { [weak self] error in - self?.output?.didReceive(error: error) - } - - let options = StreamableProviderObserverOptions() - - walletProvider.addObserver( - self, - deliverOn: .global(), - executing: updateClosure, - failing: failureClosure, - options: options - ) - } } // MARK: - BannersInteractorInput @@ -58,7 +33,6 @@ final class BannersInteractor { extension BannersInteractor: BannersInteractorInput { func setup(with output: BannersInteractorOutput) { self.output = output - subscribeToWallet() } func markWalletAsBackedUp(_ wallet: MetaAccountModel) { @@ -85,4 +59,29 @@ extension BannersInteractor: BannersInteractorInput { operationQueue.addOperation(operation) } + + func subscribeToWallet() { + let updateClosure: ([DataProviderChange]) -> Void = { [weak self] changes in + guard let selectedWallet = changes.firstToLastChange(filter: { wallet in + wallet.isSelected + }) else { + return + } + self?.output?.didReceive(wallet: selectedWallet.info) + } + + let failureClosure: (Error) -> Void = { [weak self] error in + self?.output?.didReceive(error: error) + } + + let options = StreamableProviderObserverOptions() + + walletProvider.addObserver( + self, + deliverOn: .global(), + executing: updateClosure, + failing: failureClosure, + options: options + ) + } } diff --git a/fearless/Modules/Banners/BannersPresenter.swift b/fearless/Modules/Banners/BannersPresenter.swift index 030b4aaa22..1036ea3282 100644 --- a/fearless/Modules/Banners/BannersPresenter.swift +++ b/fearless/Modules/Banners/BannersPresenter.swift @@ -1,6 +1,11 @@ import Foundation import SoraFoundation +enum BannersModuleType { + case independent + case embed +} + protocol BannersViewInput: ControllerBackedProtocol { func didReceive(viewModel: BannersViewModel) } @@ -8,6 +13,7 @@ protocol BannersViewInput: ControllerBackedProtocol { protocol BannersInteractorInput: AnyObject { func setup(with output: BannersInteractorOutput) func markWalletAsBackedUp(_ wallet: MetaAccountModel) + func subscribeToWallet() } final class BannersPresenter { @@ -17,6 +23,7 @@ final class BannersPresenter { private let router: BannersRouterInput private let interactor: BannersInteractorInput private weak var moduleOutput: BannersModuleOutput? + private let type: BannersModuleType private let logger: LoggerProtocol private lazy var viewModelFactory: BannersViewModelFactoryProtocol = { @@ -32,12 +39,17 @@ final class BannersPresenter { moduleOutput: BannersModuleOutput?, interactor: BannersInteractorInput, router: BannersRouterInput, - localizationManager: LocalizationManagerProtocol + localizationManager: LocalizationManagerProtocol, + type: BannersModuleType, + wallet: MetaAccountModel? ) { self.logger = logger self.moduleOutput = moduleOutput self.interactor = interactor self.router = router + self.type = type + self.wallet = wallet + self.localizationManager = localizationManager } @@ -88,39 +100,43 @@ final class BannersPresenter { // MARK: - BannersViewOutput extension BannersPresenter: BannersViewOutput { - func didTapOnCell(at indexPath: IndexPath) { - guard - let wallet = wallet, - let tappedOption = Banners(rawValue: indexPath.row) else { + func didTapOnBanner(_ banner: Banners) { + guard let wallet = wallet else { return } - switch tappedOption { + switch banner { case .backup: router.showWalletBackupScreen(for: wallet, from: view) case .buyXor: break + case .liquidityPools: + router.presentLiquidityPools(on: view, wallet: wallet) } } - func didCloseCell(at indexPath: IndexPath) { - guard - let wallet = wallet, - let tappedOption = Banners(rawValue: indexPath.row) else { + func didCloseBanner(_ banner: Banners) { + guard let wallet = wallet else { return } - switch tappedOption { + switch banner { case .backup: showNotBackedUpAlert(wallet: wallet) case .buyXor: break + case .liquidityPools: + moduleOutput?.didTapCloseBanners() } } func didLoad(view: BannersViewInput) { self.view = view interactor.setup(with: self) + + if type == .independent { + interactor.subscribeToWallet() + } } } @@ -148,4 +164,9 @@ extension BannersPresenter: BannersModuleInput { self.wallet = wallet provideViewModel() } + + func update(banners: [Banners]) { + let viewModel = viewModelFactory.createViewModel(banners: banners, locale: selectedLocale) + view?.didReceive(viewModel: viewModel) + } } diff --git a/fearless/Modules/Banners/BannersProtocols.swift b/fearless/Modules/Banners/BannersProtocols.swift index 71f4a3ddfa..bd5779163b 100644 --- a/fearless/Modules/Banners/BannersProtocols.swift +++ b/fearless/Modules/Banners/BannersProtocols.swift @@ -8,12 +8,19 @@ protocol BannersRouterInput: AnyObject, SheetAlertPresentable { for wallet: MetaAccountModel, from view: ControllerBackedProtocol? ) + + func presentLiquidityPools( + on view: ControllerBackedProtocol?, + wallet: MetaAccountModel + ) } protocol BannersModuleInput: AnyObject { func reload(with wallet: MetaAccountModel) + func update(banners: [Banners]) } protocol BannersModuleOutput: AnyObject { func reloadBannersView() + func didTapCloseBanners() } diff --git a/fearless/Modules/Banners/BannersRouter.swift b/fearless/Modules/Banners/BannersRouter.swift index db70aac5e1..dc50f4899d 100644 --- a/fearless/Modules/Banners/BannersRouter.swift +++ b/fearless/Modules/Banners/BannersRouter.swift @@ -12,4 +12,17 @@ final class BannersRouter: BannersRouterInput { let navigation = FearlessNavigationController(rootViewController: module.view.controller) view?.controller.present(navigation, animated: true) } + + func presentLiquidityPools(on view: ControllerBackedProtocol?, wallet: MetaAccountModel) { + guard + let tabBarController = view?.controller, + let viewController = LiquidityPoolsOverviewAssembly.configureModule(wallet: wallet)?.view.controller + else { + return + } + let navigationController = FearlessNavigationController(rootViewController: viewController) + + let presentingController = tabBarController.topModalViewController + presentingController.present(navigationController, animated: true, completion: nil) + } } diff --git a/fearless/Modules/Banners/BannersViewController.swift b/fearless/Modules/Banners/BannersViewController.swift index 1d381b026f..3330a4a738 100644 --- a/fearless/Modules/Banners/BannersViewController.swift +++ b/fearless/Modules/Banners/BannersViewController.swift @@ -3,8 +3,8 @@ import SoraFoundation protocol BannersViewOutput: AnyObject { func didLoad(view: BannersViewInput) - func didTapOnCell(at indexPath: IndexPath) - func didCloseCell(at indexPath: IndexPath) + func didTapOnBanner(_ banner: Banners) + func didCloseBanner(_ banner: Banners) } final class BannersViewController: UIViewController, ViewHolder { @@ -72,18 +72,12 @@ extension BannersViewController: Localizable { // MARK: - BannerCellectionCellDelegate extension BannersViewController: BannerCellectionCellDelegate { - func didActionButtonTapped(indexPath: IndexPath?) { - guard let indexPath = indexPath else { - return - } - output.didTapOnCell(at: indexPath) + func didActionButtonTapped(banner: Banners) { + output.didTapOnBanner(banner) } - func didCloseButtonTapped(indexPath: IndexPath?) { - guard let indexPath = indexPath else { - return - } - output.didCloseCell(at: indexPath) + func didCloseButtonTapped(banner: Banners) { + output.didCloseBanner(banner) } } diff --git a/fearless/Modules/Banners/BannersViewModelFactory.swift b/fearless/Modules/Banners/BannersViewModelFactory.swift index 5dff310d80..97d4c5a540 100644 --- a/fearless/Modules/Banners/BannersViewModelFactory.swift +++ b/fearless/Modules/Banners/BannersViewModelFactory.swift @@ -7,6 +7,7 @@ struct BannersViewModel { enum Banners: Int { case backup case buyXor + case liquidityPools } protocol BannersViewModelFactoryProtocol { @@ -14,17 +15,12 @@ protocol BannersViewModelFactoryProtocol { wallet: MetaAccountModel, locale: Locale ) -> BannersViewModel + + func createViewModel(banners: [Banners], locale: Locale) -> BannersViewModel } final class BannersViewModelFactory: BannersViewModelFactoryProtocol { - func createViewModel( - wallet: MetaAccountModel, - locale: Locale - ) -> BannersViewModel { - var banners: [Banners] = [] - if !wallet.hasBackup { - banners.insert(.backup, at: 0) - } + func createViewModel(banners: [Banners], locale: Locale) -> BannersViewModel { let bannersViewModel: [BannerCellViewModel] = banners.map { switch $0 { case .backup: @@ -39,7 +35,9 @@ final class BannersViewModelFactory: BannersViewModelFactoryProtocol { subtitle: subtitle, buttonTitle: buttonAction, image: R.image.fearlessBanner()!, - dismissable: true + dismissable: true, + fullsizeImage: false, + bannerType: .backup ) case .buyXor: let title = R.string.localizable @@ -53,11 +51,39 @@ final class BannersViewModelFactory: BannersViewModelFactoryProtocol { subtitle: subtitle, buttonTitle: buttonAction, image: R.image.xorBanner()!, - dismissable: true + dismissable: true, + fullsizeImage: false, + bannerType: .buyXor + ) + case .liquidityPools: + let title = "Liquidity pools" + let subtitle = "Invest your funds in Liquidity pools and receive rewards" + let buttonAction = "Show details" + + return BannerCellViewModel( + title: title, + subtitle: subtitle, + buttonTitle: buttonAction, + image: R.image.iconLpBanner()!, + dismissable: true, + fullsizeImage: true, + bannerType: .liquidityPools ) } } return BannersViewModel(banners: bannersViewModel) } + + func createViewModel( + wallet: MetaAccountModel, + locale: Locale + ) -> BannersViewModel { + var banners: [Banners] = [] + if !wallet.hasBackup { + banners.insert(.backup, at: 0) + } + + return createViewModel(banners: banners, locale: locale) + } } diff --git a/fearless/Modules/Crowdloan/CustomCrowdloan/Bifrost/BifrostBonusService.swift b/fearless/Modules/Crowdloan/CustomCrowdloan/Bifrost/BifrostBonusService.swift index 52f3f0611f..1bb4d886f0 100644 --- a/fearless/Modules/Crowdloan/CustomCrowdloan/Bifrost/BifrostBonusService.swift +++ b/fearless/Modules/Crowdloan/CustomCrowdloan/Bifrost/BifrostBonusService.swift @@ -2,6 +2,7 @@ import Foundation import RobinHood import SSFUtils import BigInt +import SSFModels final class BifrostBonusService { static let defaultReferralCode = "FRLS69" diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsInteractor.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsInteractor.swift index 8753836b16..6b678bfc96 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsInteractor.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsInteractor.swift @@ -123,11 +123,13 @@ extension LiquidityPoolDetailsInteractor: LiquidityPoolDetailsInteractorInput { func fetchApy(reservesId: String) { Task { + let address = try AddressFactory.address(for: Data(hex: reservesId), chain: chain) + let apyStream = try await liquidityPoolService.subscribePoolsAPY(poolIds: [address]) do { - let apy = try await liquidityPoolService.fetchPoolsAPY() - let address = try AddressFactory.address(for: Data(hex: reservesId), chain: chain) - await MainActor.run { - output?.didReceivePoolAPY(apy: apy.first(where: { $0.poolId == address })) + for try await apy in apyStream { + await MainActor.run { + output?.didReceivePoolAPY(apy: apy.first(where: { $0.value?.poolId == address })?.value) + } } } catch { await MainActor.run { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyInteractor.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyInteractor.swift index 3a8a366f25..e06b0c7eff 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyInteractor.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyInteractor.swift @@ -104,11 +104,13 @@ extension LiquidityPoolSupplyInteractor: LiquidityPoolSupplyInteractorInput { } Task { + let address = try AddressFactory.address(for: Data(hex: reservesId), chain: chain) + let apyStream = try await lpDataService.subscribePoolsAPY(poolIds: [address]) do { - let apy = try await lpDataService.fetchPoolsAPY() - let address = try AddressFactory.address(for: Data(hex: reservesId), chain: chain) - await MainActor.run { - output?.didReceivePoolAPY(apyInfo: apy.first(where: { $0.poolId == address })) + for try await apy in apyStream { + await MainActor.run { + output?.didReceivePoolAPY(apyInfo: apy.first(where: { $0.value?.poolId == address })?.value) + } } } catch { await MainActor.run { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmInteractor.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmInteractor.swift index 6b27bec5d1..b5184e8362 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmInteractor.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmInteractor.swift @@ -121,11 +121,13 @@ extension LiquidityPoolSupplyConfirmInteractor: LiquidityPoolSupplyConfirmIntera } Task { + let address = try AddressFactory.address(for: Data(hex: reservesId), chain: chain) + let apyStream = try await lpDataService.subscribePoolsAPY(poolIds: [address]) do { - let apy = try await lpDataService.fetchPoolsAPY() - let address = try AddressFactory.address(for: Data(hex: reservesId), chain: chain) - await MainActor.run { - output?.didReceivePoolAPY(apyInfo: apy.first(where: { $0.poolId == address })) + for try await apy in apyStream { + await MainActor.run { + output?.didReceivePoolAPY(apyInfo: apy.first(where: { $0.value?.poolId == address })?.value) + } } } catch { await MainActor.run { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListInteractor.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListInteractor.swift index 3026a781f4..cd19975056 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListInteractor.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListInteractor.swift @@ -22,6 +22,8 @@ final class AvailableLiquidityPoolsListInteractor { private let chain: ChainModel private var priceProvider: AnySingleValueProvider<[PriceData]>? + private var receivedPoolIds: [String] = [] + init( liquidityPoolService: PolkaswapLiquidityPoolService, priceLocalSubscriber: PriceLocalStorageSubscriber, @@ -61,7 +63,6 @@ extension AvailableLiquidityPoolsListInteractor: AvailableLiquidityPoolsListInte self.output = output fetchPools() - fetchApy() subscribeForPrices() } @@ -77,6 +78,7 @@ extension AvailableLiquidityPoolsListInteractor: AvailableLiquidityPoolsListInte if let pools = availablePools.value { fetchReserves(pools: pools) + fetchApy(pools: pools) } } } catch { @@ -87,13 +89,41 @@ extension AvailableLiquidityPoolsListInteractor: AvailableLiquidityPoolsListInte } } - func fetchApy() { + func fetchApy(pools: [LiquidityPair]) { + let poolIds: [String] = pools.compactMap { pool in + let baseAsset = chain.assets.first(where: { $0.currencyId == pool.baseAssetId }) + let targetAsset = chain.assets.first(where: { $0.currencyId == pool.targetAssetId }) + let rewardAsset = chain.assets.first(where: { $0.currencyId == pool.rewardAssetId }) + + guard baseAsset != nil, targetAsset != nil, rewardAsset != nil else { + return nil + } + + guard + let reservesId = pool.reservesId, + let address = try? AddressFactory.address(for: Data(hex: reservesId), chain: chain) + else { + return nil + } + + return address + } + + guard poolIds != receivedPoolIds else { + return + } + Task { + let apyStream = try await liquidityPoolService.subscribePoolsAPY(poolIds: poolIds) do { - let apy = try await liquidityPoolService.fetchPoolsAPY() + for try await apy in apyStream { + if apy.first?.type == .remote { + receivedPoolIds.append(contentsOf: apy.compactMap { $0.value?.poolId }) + } - await MainActor.run { - output?.didReceivePoolsAPY(apy: apy) + await MainActor.run { + output?.didReceivePoolsAPY(apy: apy.compactMap { $0.value }) + } } } catch { await MainActor.run { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListPresenter.swift index f22422e782..cac882f2b1 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListPresenter.swift @@ -9,7 +9,6 @@ protocol AvailableLiquidityPoolsListInteractorInput { func setup(with output: AvailableLiquidityPoolsListInteractorOutput) func fetchPools() - func fetchApy() } final class AvailableLiquidityPoolsListPresenter { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListInteractor.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListInteractor.swift index 5e424f4d1b..d226009ff9 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListInteractor.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListInteractor.swift @@ -25,6 +25,8 @@ final class UserLiquidityPoolsListInteractor { private let wallet: MetaAccountModel private var priceProvider: AnySingleValueProvider<[PriceData]>? + private var receivedPoolIds: [String] = [] + init( liquidityPoolService: PolkaswapLiquidityPoolService, priceLocalSubscriber: PriceLocalStorageSubscriber, @@ -66,7 +68,7 @@ extension UserLiquidityPoolsListInteractor: UserLiquidityPoolsListInteractorInpu self.output = output fetchPools() - fetchApy() + fetchUserPools() subscribeForPrices() } @@ -83,9 +85,6 @@ extension UserLiquidityPoolsListInteractor: UserLiquidityPoolsListInteractorInpu output?.didReceiveUserPools(accountPools: accountPools) } -// if let pools = accountPools { -// fetchReserves(pools: pools) -// } } catch { await MainActor.run { output?.didReceiveUserPoolsError(error: error) @@ -105,9 +104,14 @@ extension UserLiquidityPoolsListInteractor: UserLiquidityPoolsListInteractorInpu let userPoolsStream = try await liquidityPoolService.subscribeUserPools(accountId: accountId) for try await userPools in userPoolsStream { + print("received fetch pool response") await MainActor.run { output?.didReceiveLiquidityPairs(pools: userPools.value) } + + if let pools = userPools.value { + fetchApy(pools: pools) + } } } catch { output?.didReceiveLiquidityPairsError(error: error) @@ -115,16 +119,38 @@ extension UserLiquidityPoolsListInteractor: UserLiquidityPoolsListInteractorInpu } } - func fetchApy() { + func fetchApy(pools: [LiquidityPair]) { + let poolIds: [String] = pools.compactMap { + guard + let reservesId = $0.reservesId, + let address = try? AddressFactory.address(for: Data(hex: reservesId), chain: chain) + else { + return nil + } + + return address + } + + guard poolIds != receivedPoolIds else { + return + } + Task { + let apyStream = try await liquidityPoolService.subscribePoolsAPY(poolIds: poolIds) do { - let apy = try await liquidityPoolService.fetchPoolsAPY() + for try await apy in apyStream { + if apy.first?.type == .remote { + receivedPoolIds.append(contentsOf: apy.compactMap { $0.value?.poolId }) + } - await MainActor.run { - output?.didReceivePoolsAPY(apy: apy) + await MainActor.run { + output?.didReceivePoolsAPY(apy: apy.compactMap { $0.value }) + } } } catch { - output?.didReceivePoolsApyError(error: error) + await MainActor.run { + output?.didReceivePoolsApyError(error: error) + } } } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift index 47ad7eec84..6c23cfc10e 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift @@ -9,7 +9,6 @@ protocol UserLiquidityPoolsListInteractorInput { func setup(with output: UserLiquidityPoolsListInteractorOutput) func fetchPools() - func fetchApy() } final class UserLiquidityPoolsListPresenter { @@ -55,6 +54,7 @@ final class UserLiquidityPoolsListPresenter { private func provideViewModel() { let viewModel = viewModelFactory.buildViewModel( + accountPools: accountPools, pools: pools, reserves: reserves, apyInfos: apy, @@ -107,6 +107,7 @@ extension UserLiquidityPoolsListPresenter: LiquidityPoolsListViewOutput { extension UserLiquidityPoolsListPresenter: UserLiquidityPoolsListInteractorOutput { func didReceiveUserPools(accountPools: [AccountPool]?) { self.accountPools = accountPools + provideViewModel() } func didReceiveLiquidityPairs(pools: [LiquidityPair]?) { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListViewModelFactory.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListViewModelFactory.swift index 54e6290126..45ce5c357a 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListViewModelFactory.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListViewModelFactory.swift @@ -8,6 +8,7 @@ import SSFStorageQueryKit protocol UserLiquidityPoolsListViewModelFactory { func buildViewModel( + accountPools: [AccountPool]?, pools: [LiquidityPair]?, reserves: CachedStorageResponse<[PolkaswapPoolReservesInfo]>?, apyInfos: [PoolApyInfo]?, @@ -30,6 +31,7 @@ final class UserLiquidityPoolsListViewModelFactoryDefault: UserLiquidityPoolsLis } func buildViewModel( + accountPools: [AccountPool]?, pools: [LiquidityPair]?, reserves: CachedStorageResponse<[PolkaswapPoolReservesInfo]>?, apyInfos: [PoolApyInfo]?, @@ -41,6 +43,7 @@ final class UserLiquidityPoolsListViewModelFactoryDefault: UserLiquidityPoolsLis searchText: String? ) -> LiquidityPoolListViewModel { let poolViewModels: [LiquidityPoolListCellModel]? = pools?.sorted().compactMap { pair in + let accountPoolInfo = accountPools?.first(where: { $0.poolId == pair.identifier }) let baseAsset = chain.assets.first(where: { $0.currencyId == pair.baseAssetId }) let targetAsset = chain.assets.first(where: { $0.currencyId == pair.targetAssetId }) let rewardAsset = chain.assets.first(where: { $0.currencyId == pair.rewardAssetId }) @@ -77,12 +80,34 @@ final class UserLiquidityPoolsListViewModelFactoryDefault: UserLiquidityPoolsLis let reservesLabelText: String? = reservesString.flatMap { "\($0) TVL" } let reservesLabelValue: ShimmeredLabelState = .normal(reservesLabelText) + let baseAssetBalanceViewModelFactory = createBalanceViewModelFactory(for: ChainAsset(chain: chain, asset: baseAsset), wallet: wallet) + let targetAssetBalanceViewModelFactory = createBalanceViewModelFactory(for: ChainAsset(chain: chain, asset: targetAsset), wallet: wallet) + + let baseAssetViewModel = accountPoolInfo?.baseAssetPooled.flatMap { + baseAssetBalanceViewModelFactory.balanceFromPrice($0, priceData: baseAssetPrice, usageCase: .listCryptoWith(minimumFractionDigits: 1, maximumFractionDigits: 3)) + } + + let targetAssetViewModel = accountPoolInfo?.targetAssetPooled.flatMap { + targetAssetBalanceViewModelFactory.balanceFromPrice($0, priceData: targetAssetPrice, usageCase: .listCryptoWith(minimumFractionDigits: 1, maximumFractionDigits: 3)) + } + + let baseAssetPooledText = baseAssetViewModel?.value(for: locale).amount + let targetAssetPooledText = targetAssetViewModel?.value(for: locale).amount + + let stakingStatusLabelText: String? = baseAssetPooledText.flatMap { + guard let targetAssetPooledText = targetAssetPooledText else { + return nil + } + + return "\($0) - \(targetAssetPooledText)" + } + return LiquidityPoolListCellModel( tokenPairIconsVieWModel: iconsViewModel, tokenPairNameLabelText: tokenPairName, rewardTokenNameLabelText: rewardTokenNameLabelText, apyLabelText: apyLabelText, - stakingStatusLabelText: nil, + stakingStatusLabelText: stakingStatusLabelText, reservesLabelValue: reservesLabelValue, sortValue: reservesValue.or(.zero), liquidityPair: pair @@ -114,4 +139,11 @@ final class UserLiquidityPoolsListViewModelFactoryDefault: UserLiquidityPoolsLis let tokenFormatterValue = tokenFormatter.value(for: locale) return tokenFormatterValue } + + private func createBalanceViewModelFactory(for chainAsset: ChainAsset, wallet: MetaAccountModel) -> BalanceViewModelFactoryProtocol { + BalanceViewModelFactory( + targetAssetInfo: chainAsset.assetDisplayInfo, + selectedMetaAccount: wallet + ) + } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift index 30173bb3f4..2bc510b657 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift @@ -1,6 +1,7 @@ import UIKit import SoraFoundation import SnapKit +import SoraUI final class LiquidityPoolsListViewController: UIViewController, ViewHolder, HiddableBarWhenPushed { typealias RootViewType = LiquidityPoolsListViewLayout @@ -122,6 +123,8 @@ extension LiquidityPoolsListViewController: LiquidityPoolsListViewInput { rootView.bind(viewModel: viewModel) rootView.tableView.reloadData() + + reloadEmptyState(animated: false) } } @@ -132,3 +135,40 @@ extension LiquidityPoolsListViewController: Localizable { rootView.locale = selectedLocale } } + +// MARK: - EmptyStateViewOwnerProtocol + +extension LiquidityPoolsListViewController: EmptyStateViewOwnerProtocol { + var emptyStateDelegate: EmptyStateDelegate { self } + var emptyStateDataSource: EmptyStateDataSource { self } +} + +// MARK: - EmptyStateDataSource + +extension LiquidityPoolsListViewController: EmptyStateDataSource { + var viewForEmptyState: UIView? { + let emptyView = EmptyView() + emptyView.image = R.image.iconWarningGray() + emptyView.title = R.string.localizable + .emptyViewTitle(preferredLanguages: selectedLocale.rLanguages) + emptyView.text = R.string.localizable.selectAssetSearchEmptySubtitle(preferredLanguages: selectedLocale.rLanguages) + emptyView.iconMode = .smallFilled + return emptyView + } + + var contentViewForEmptyState: UIView { + rootView.contentView + } +} + +// MARK: - EmptyStateDelegate + +extension LiquidityPoolsListViewController: EmptyStateDelegate { + var shouldDisplayEmptyState: Bool { + guard let cellModels else { + return false + } + + return cellModels.isEmpty + } +} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift index afcf7fcfc8..f872c000cf 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift @@ -11,6 +11,8 @@ final class LiquidityPoolsListViewLayout: UIView { return view }() + let contentView = UIView() + let titleLabel: UILabel = { let label = UILabel() label.font = .h5Title @@ -114,8 +116,9 @@ final class LiquidityPoolsListViewLayout: UIView { vStackView.addArrangedSubview(topBar) vStackView.addArrangedSubview(searchTextField) vStackView.addArrangedSubview(separatorView) + addSubview(contentView) + contentView.addSubview(tableView) - addSubview(tableView) topBar.addSubview(titleLabel) topBar.addSubview(moreButton) topBar.addSubview(backButton) @@ -137,11 +140,15 @@ final class LiquidityPoolsListViewLayout: UIView { make.leading.trailing.equalToSuperview() } - tableView.snp.makeConstraints { make in + contentView.snp.makeConstraints { make in make.top.equalTo(vStackView.snp.bottom).offset(8) make.leading.trailing.bottom.equalToSuperview() } + tableView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + titleLabel.snp.makeConstraints { make in make.centerX.centerY.equalToSuperview() } diff --git a/fearless/Modules/NewWallet/ChainAssetList/ChainAssetListAssembly.swift b/fearless/Modules/NewWallet/ChainAssetList/ChainAssetListAssembly.swift index e7f55633ad..504e48a732 100644 --- a/fearless/Modules/NewWallet/ChainAssetList/ChainAssetListAssembly.swift +++ b/fearless/Modules/NewWallet/ChainAssetList/ChainAssetListAssembly.swift @@ -102,6 +102,6 @@ final class ChainAssetListAssembly { } private static func configureBannersModule(moduleOutput: BannersModuleOutput?) -> BannersModuleCreationResult? { - BannersAssembly.configureModule(output: moduleOutput) + BannersAssembly.configureModule(output: moduleOutput, type: .independent, wallet: nil) } } diff --git a/fearless/Modules/NewWallet/ChainAssetList/ChainAssetListPresenter.swift b/fearless/Modules/NewWallet/ChainAssetList/ChainAssetListPresenter.swift index 4a1e63ea05..5a5065c911 100644 --- a/fearless/Modules/NewWallet/ChainAssetList/ChainAssetListPresenter.swift +++ b/fearless/Modules/NewWallet/ChainAssetList/ChainAssetListPresenter.swift @@ -364,6 +364,8 @@ extension ChainAssetListPresenter: ChainAssetListModuleInput { // MARK: - BannersModuleOutput? extension ChainAssetListPresenter: BannersModuleOutput { + func didTapCloseBanners() {} + func reloadBannersView() { DispatchQueue.main.async { self.view?.reloadBanners() diff --git a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentAssembly.swift b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentAssembly.swift index d1e4e9066e..9f7a06d7af 100644 --- a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentAssembly.swift +++ b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentAssembly.swift @@ -102,12 +102,29 @@ final class PolkaswapAdjustmentAssembly { localizationManager: localizationManager ) + guard + let bannersModule = Self.configureBannersModule(output: presenter, wallet: wallet) + else { + return nil + } + let view = PolkaswapAdjustmentViewController( output: presenter, + bannersViewController: bannersModule.view.controller, localizationManager: localizationManager ) dataValidatingFactory.view = view + presenter.bannersModuleInput = bannersModule.input return (view, presenter) } + + // MARK: - Cofigure Modules + + private static func configureBannersModule( + output: BannersModuleOutput?, + wallet: MetaAccountModel + ) -> BannersModuleCreationResult? { + BannersAssembly.configureModule(output: output, type: .embed, wallet: wallet) + } } diff --git a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentPresenter.swift b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentPresenter.swift index 72d09784d9..38ff0076e5 100644 --- a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentPresenter.swift +++ b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentPresenter.swift @@ -21,6 +21,7 @@ final class PolkaswapAdjustmentPresenter { private weak var confirmationScreenModuleInput: PolkaswapSwapConfirmationModuleInput? private let router: PolkaswapAdjustmentRouterInput private let interactor: PolkaswapAdjustmentInteractorInput + weak var bannersModuleInput: BannersModuleInput? private let wallet: MetaAccountModel private let viewModelFactory: PolkaswapAdjustmentViewModelFactoryProtocol @@ -499,6 +500,8 @@ extension PolkaswapAdjustmentPresenter: PolkaswapAdjustmentViewOutput { func viewDidAppear() { interactor.fetchDisclaimerVisible() disclaimerWasShown = true + + bannersModuleInput?.update(banners: [.liquidityPools]) } func didLoad(view: PolkaswapAdjustmentViewInput) { @@ -949,3 +952,13 @@ extension PolkaswapAdjustmentPresenter: PolkaswapTransaktionSettingsModuleOutput } } } + +// MARK: - BannersModuleOutput + +extension PolkaswapAdjustmentPresenter: BannersModuleOutput { + func reloadBannersView() {} + + func didTapCloseBanners() { + view?.hideBanners() + } +} diff --git a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentProtocols.swift b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentProtocols.swift index e8122518eb..37200e0144 100644 --- a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentProtocols.swift +++ b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentProtocols.swift @@ -18,6 +18,7 @@ protocol PolkaswapAdjustmentViewInput: ControllerBackedProtocol { func didReceive(variant: SwapVariant) func didReceiveDetails(viewModel: PolkaswapAdjustmentDetailsViewModel?) func setButtonLoadingState(isLoading: Bool) + func hideBanners() } protocol PolkaswapAdjustmentViewOutput: AnyObject { diff --git a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewController.swift b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewController.swift index ed566e237a..933f4a2a16 100644 --- a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewController.swift +++ b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewController.swift @@ -13,7 +13,7 @@ final class PolkaswapAdjustmentViewController: UIViewController, ViewHolder, Hid // MARK: Private properties private let output: PolkaswapAdjustmentViewOutput - + private let bannersViewController: UIViewController private var amountFromInputViewModel: IAmountInputViewModel? private var amountToInputViewModel: IAmountInputViewModel? @@ -21,9 +21,11 @@ final class PolkaswapAdjustmentViewController: UIViewController, ViewHolder, Hid init( output: PolkaswapAdjustmentViewOutput, + bannersViewController: UIViewController, localizationManager: LocalizationManagerProtocol? ) { self.output = output + self.bannersViewController = bannersViewController super.init(nibName: nil, bundle: nil) self.localizationManager = localizationManager } @@ -45,6 +47,7 @@ final class PolkaswapAdjustmentViewController: UIViewController, ViewHolder, Hid setupActions() configure() addEndEditingTapGesture(for: rootView.contentView) + setupEmbededBannersView() } override func viewWillAppear(_ animated: Bool) { @@ -67,6 +70,17 @@ final class PolkaswapAdjustmentViewController: UIViewController, ViewHolder, Hid // MARK: - Private methods + private func setupEmbededBannersView() { + addChild(bannersViewController) + + guard let view = bannersViewController.view else { + return + } + + rootView.addBanners(view) + controller.didMove(toParent: self) + } + private func configure() { navigationController?.setNavigationBarHidden(true, animated: true) @@ -106,9 +120,6 @@ final class PolkaswapAdjustmentViewController: UIViewController, ViewHolder, Hid action: #selector(handleTapSwitchInputsButton), for: .touchUpInside ) - rootView.bannerButton.addAction { [weak self] in - self?.output.didTapLiquidityPools() - } let tapMinReceiveInfo = UITapGestureRecognizer( target: self, @@ -160,6 +171,10 @@ final class PolkaswapAdjustmentViewController: UIViewController, ViewHolder, Hid // MARK: - PolkaswapAdjustmentViewInput extension PolkaswapAdjustmentViewController: PolkaswapAdjustmentViewInput { + func hideBanners() { + rootView.removeBanners() + } + func setButtonLoadingState(isLoading: Bool) { rootView.previewButton.set(loading: isLoading) } diff --git a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewLayout.swift b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewLayout.swift index 70d29a9c37..fac6b35607 100644 --- a/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewLayout.swift +++ b/fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentViewLayout.swift @@ -60,11 +60,6 @@ final class PolkaswapAdjustmentViewLayout: UIView { let fromPerToPriceView = UIFactory.default.createMultiView() let toPerFromPriceView = UIFactory.default.createMultiView() let networkFeeView = UIFactory.default.createMultiView() - let bannerButton: UIButton = { - let button = UIButton() - button.setImage(R.image.iconLpBanner(), for: .normal) - return button - }() private lazy var multiViews = [ minMaxReceivedView, @@ -80,6 +75,8 @@ final class PolkaswapAdjustmentViewLayout: UIView { return button }() + let bannersViewContainer = UIView() + var locale: Locale = .current { didSet { applyLocalization() @@ -146,6 +143,22 @@ final class PolkaswapAdjustmentViewLayout: UIView { setInfoImage(for: minMaxReceivedView.titleLabel, text: text) } + func addBanners(_ view: UIView) { + bannersViewContainer.addSubview(view) + view.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + + func removeBanners() { + bannersViewContainer.isHidden = true + bannersViewContainer.snp.remakeConstraints { make in + make.leading.trailing.equalToSuperview().inset(16) + make.height.equalTo(0) + make.bottom.equalToSuperview() + } + } + // MARK: - Private methods private func setupLayout() { @@ -185,7 +198,7 @@ final class PolkaswapAdjustmentViewLayout: UIView { private func setupContentsLayout() { addSubview(contentView) - addSubview(bannerButton) + addSubview(bannersViewContainer) addSubview(previewButton) contentView.snp.makeConstraints { make in @@ -200,7 +213,7 @@ final class PolkaswapAdjustmentViewLayout: UIView { keyboardAdoptableConstraint = make.bottom.equalToSuperview().inset(UIConstants.bigOffset).constraint } - bannerButton.snp.makeConstraints { make in + bannersViewContainer.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(16) make.height.equalTo(139) make.bottom.equalToSuperview().inset(UIConstants.actionHeight + UIConstants.bigOffset * 2) diff --git a/fearless/Modules/RawData/RawDataAssembly.swift b/fearless/Modules/RawData/RawDataAssembly.swift index 65ef031664..150d87ac2a 100644 --- a/fearless/Modules/RawData/RawDataAssembly.swift +++ b/fearless/Modules/RawData/RawDataAssembly.swift @@ -1,6 +1,7 @@ import UIKit import SoraFoundation import SSFUtils +import SSFModels final class RawDataAssembly { static func configureModule( diff --git a/fearless/Modules/RawData/RawDataPresenter.swift b/fearless/Modules/RawData/RawDataPresenter.swift index 221901f08b..0a53e3df18 100644 --- a/fearless/Modules/RawData/RawDataPresenter.swift +++ b/fearless/Modules/RawData/RawDataPresenter.swift @@ -1,6 +1,7 @@ import Foundation import SoraFoundation import SSFUtils +import SSFModels protocol RawDataViewInput: ControllerBackedProtocol { func didReceive(text: String) From 58c63e571f052dc775f9ebe126a0d3c024691499 Mon Sep 17 00:00:00 2001 From: Alex Lebedko Date: Mon, 24 Jun 2024 17:44:42 +0700 Subject: [PATCH 049/115] package update --- .../xcshareddata/swiftpm/Package.resolved | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index 590d0594c1..1ff63b0a78 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -126,6 +126,15 @@ "version" : "0.1.7" } }, + { + "identity" : "shared-features-spm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/soramitsu/shared-features-spm.git", + "state" : { + "branch" : "liquidity-pools-fw", + "revision" : "d4d1739f097139599aeaa800d596e0a71a17e82a" + } + }, { "identity" : "swift-atomics", "kind" : "remoteSourceControl", @@ -216,15 +225,6 @@ "version" : "1.3.0" } }, - { - "identity" : "swiftformat", - "kind" : "remoteSourceControl", - "location" : "https://github.com/nicklockwood/SwiftFormat", - "state" : { - "revision" : "dd989a46d0c6f15c016484bab8afe5e7a67a4022", - "version" : "0.54.0" - } - }, { "identity" : "swiftimagereadwrite", "kind" : "remoteSourceControl", From d578ddb9ff8a51db7792c4c12b232a3900c4ded1 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Mon, 24 Jun 2024 17:16:57 +0500 Subject: [PATCH 050/115] [#FLW-4718] Reconnect socket after connection lost --- fearless.xcodeproj/project.pbxproj | 6 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../DataProvider/PriceProviderFactory.swift | 2 +- .../Extension/JSONRPCOperation+Result.swift | 6 +- fearless/Common/Extension/SafeArray.swift | 49 +++++++ .../Common/Extension/SafeDictionary.swift | 6 + .../Misc/SubstrateOperationFactory.swift | 8 +- .../ChainRegistry/ChainRegistry.swift | 53 +------ .../ChainRegistry/ChainRegistryFactory.swift | 4 +- .../ConnectionPool/ConnectionFactory.swift | 12 +- .../ConnectionPool/ConnectionPool.swift | 137 ++++++------------ .../EthereumConnectionPool.swift | 8 - .../RuntimeSyncService.swift | 2 +- 13 files changed, 134 insertions(+), 163 deletions(-) create mode 100644 fearless/Common/Extension/SafeArray.swift diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index 0bc8c2e8a0..339e0ed433 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -97,6 +97,7 @@ 074EB7AD290B9F64000A2A6A /* AddressCopiedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074EB7AC290B9F64000A2A6A /* AddressCopiedEvent.swift */; }; 074EB7AF290BA057000A2A6A /* ConnectionOfflineEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074EB7AE290BA056000A2A6A /* ConnectionOfflineEvent.swift */; }; 074EB7B1290BA142000A2A6A /* ConnectionOnlineEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074EB7B0290BA142000A2A6A /* ConnectionOnlineEvent.swift */; }; + 0754A3632C259CE200900E11 /* SafeArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0754A3622C259CE200900E11 /* SafeArray.swift */; }; 075A2F0A2BF722F300CE6F24 /* ExternalApiExplorerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 075A2F092BF722F300CE6F24 /* ExternalApiExplorerType.swift */; }; 075C647028098AFB00A55094 /* EthereumConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 075C646F28098AFB00A55094 /* EthereumConstants.swift */; }; 075FC63528D9AB1600E25263 /* CommonInputViewV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 075FC63428D9AB1600E25263 /* CommonInputViewV2.swift */; }; @@ -3152,6 +3153,7 @@ 074EB7AC290B9F64000A2A6A /* AddressCopiedEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressCopiedEvent.swift; sourceTree = ""; }; 074EB7AE290BA056000A2A6A /* ConnectionOfflineEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionOfflineEvent.swift; sourceTree = ""; }; 074EB7B0290BA142000A2A6A /* ConnectionOnlineEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionOnlineEvent.swift; sourceTree = ""; }; + 0754A3622C259CE200900E11 /* SafeArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeArray.swift; sourceTree = ""; }; 075A2F092BF722F300CE6F24 /* ExternalApiExplorerType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalApiExplorerType.swift; sourceTree = ""; }; 075C646F28098AFB00A55094 /* EthereumConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthereumConstants.swift; sourceTree = ""; }; 075FC63428D9AB1600E25263 /* CommonInputViewV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonInputViewV2.swift; sourceTree = ""; }; @@ -9176,6 +9178,7 @@ FA24FEFD2B95C32200CD9E04 /* Decimal+DoubleValue.swift */, FAC6CDA82BA814F20013A17E /* UIControl+Disable.swift */, 07F65DEA2C240A19004A1728 /* SafeDictionary.swift */, + 0754A3622C259CE200900E11 /* SafeArray.swift */, ); path = Extension; sourceTree = ""; @@ -17029,6 +17032,7 @@ FA3F5B69281BAF5C00BEF03B /* StakingAmountParachainStrategy.swift in Sources */, FA62623B2AC2E35A005D3D95 /* WalletConnectConfirmationProtocols.swift in Sources */, 8448221826B1624E007F4492 /* SelectValidatorsConfirmViewLayout.swift in Sources */, + 0754A3632C259CE200900E11 /* SafeArray.swift in Sources */, C6267B9528BDF6A5001E31BF /* ChainAssetListPresenter.swift in Sources */, FAD429362A8656B7001D6A16 /* UICollectionView.swift in Sources */, 8490145624A9404E008F705E /* AttributedStringDecorator.swift in Sources */, @@ -19300,7 +19304,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/soramitsu/shared-features-spm.git"; requirement = { - branch = "feature/xcm-routes"; + branch = "socket-connection-refactoring"; kind = branch; }; }; diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3ac93e9288..19758befa7 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -132,8 +132,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/soramitsu/shared-features-spm.git", "state" : { - "branch" : "feature/xcm-routes", - "revision" : "1781f7d9a8ea8eccccfcfd21d8c951f65cff480a" + "branch" : "socket-connection-refactoring", + "revision" : "e84295e599c4fc2fd097e0f13731159b008347bc" } }, { diff --git a/fearless/Common/DataProvider/PriceProviderFactory.swift b/fearless/Common/DataProvider/PriceProviderFactory.swift index 460fe220d8..086b3d43c9 100644 --- a/fearless/Common/DataProvider/PriceProviderFactory.swift +++ b/fearless/Common/DataProvider/PriceProviderFactory.swift @@ -26,7 +26,7 @@ class PriceProviderFactory { private func clearIfNeeded() { let filtred = providers.dictionary.filter { $0.value.target != nil } - providers = SafeDictionary(dict: filtred) + providers.replace(dict: filtred) } } diff --git a/fearless/Common/Extension/JSONRPCOperation+Result.swift b/fearless/Common/Extension/JSONRPCOperation+Result.swift index 1d77890e46..48e440f403 100644 --- a/fearless/Common/Extension/JSONRPCOperation+Result.swift +++ b/fearless/Common/Extension/JSONRPCOperation+Result.swift @@ -3,7 +3,11 @@ import SSFUtils extension JSONRPCOperation { static func failureOperation(_ error: Error) -> JSONRPCOperation { - let mockEngine = WebSocketEngine(connectionName: nil, url: ApplicationConfig.shared.wiki, autoconnect: false) + let mockEngine = try! WebSocketEngine( + connectionName: nil, + urls: [ApplicationConfig.shared.wiki], + autoconnect: false + ) let operation = JSONRPCOperation(engine: mockEngine, method: "") operation.result = .failure(error) return operation diff --git a/fearless/Common/Extension/SafeArray.swift b/fearless/Common/Extension/SafeArray.swift new file mode 100644 index 0000000000..018dd64dff --- /dev/null +++ b/fearless/Common/Extension/SafeArray.swift @@ -0,0 +1,49 @@ +import Foundation + +final class SafeArray: Collection { + private var array: [T] + private let concurrentQueue = DispatchQueue( + label: "jp.co.soramitsu.fearless.safe.array.queue.\(UUID().uuidString)", + attributes: .concurrent + ) + + var startIndex: Array.Index { + concurrentQueue.sync { + self.array.startIndex + } + } + + var endIndex: Array.Index { + concurrentQueue.sync { + self.array.endIndex + } + } + + init(array: [T] = [T]()) { + self.array = array + } + + func replace(array: [T]) { + concurrentQueue.async(flags: .barrier) { + self.array = array + } + } + + func append(_ newElement: T) { + concurrentQueue.async(flags: .barrier) { + self.array.append(newElement) + } + } + + func index(after i: Int) -> Int { + concurrentQueue.sync { + self.array.index(after: i) + } + } + + subscript(position: Int) -> T { + concurrentQueue.sync { + self.array[position] + } + } +} diff --git a/fearless/Common/Extension/SafeDictionary.swift b/fearless/Common/Extension/SafeDictionary.swift index 1f7bac4a57..95c5103c08 100644 --- a/fearless/Common/Extension/SafeDictionary.swift +++ b/fearless/Common/Extension/SafeDictionary.swift @@ -35,6 +35,12 @@ final class SafeDictionary: Collection { dictionary = dict } + func replace(dict: [V: T]) { + concurrentQueue.async(flags: .barrier) { [weak self] in + self?.dictionary = dict + } + } + func index(after i: Dictionary.Index) -> Dictionary.Index { concurrentQueue.sync { self.dictionary.index(after: i) diff --git a/fearless/Common/Network/Misc/SubstrateOperationFactory.swift b/fearless/Common/Network/Misc/SubstrateOperationFactory.swift index 56165d87f0..f47f66d014 100644 --- a/fearless/Common/Network/Misc/SubstrateOperationFactory.swift +++ b/fearless/Common/Network/Misc/SubstrateOperationFactory.swift @@ -14,13 +14,15 @@ final class SubstrateOperationFactory: SubstrateOperationFactoryProtocol { } func fetchChainOperation(_ url: URL) -> BaseOperation { - let engine = WebSocketEngine( + guard let engine = try? WebSocketEngine( connectionName: nil, - url: url, + urls: [url], reachabilityManager: nil, reconnectionStrategy: nil, logger: nil - ) + ) else { + return BaseOperation.createWithError(WebSocketEngineError.emptyUrls) + } return JSONRPCListOperation(engine: engine, method: RPCMethod.chain) } diff --git a/fearless/Common/Services/ChainRegistry/ChainRegistry.swift b/fearless/Common/Services/ChainRegistry/ChainRegistry.swift index b89ef5234b..6c7b8cfd8b 100644 --- a/fearless/Common/Services/ChainRegistry/ChainRegistry.swift +++ b/fearless/Common/Services/ChainRegistry/ChainRegistry.swift @@ -385,65 +385,28 @@ extension ChainRegistry: ChainRegistryProtocol { func retryConnection(for chainId: ChainModel.Id) { guard let chain = chains.first(where: { $0.chainId == chainId }), - let currentConnection = getConnection(for: chainId), - let currentURL = currentConnection.url + let currentConnection = getConnection(for: chainId) else { return } - connectionNeedsReconnect(for: chain, previusUrl: currentURL) + // TODO: - Remove retry logic if web socket engine refactoring helped } } // MARK: - ConnectionPoolDelegate extension ChainRegistry: ConnectionPoolDelegate { - func webSocketDidChangeState(url: URL, state: WebSocketEngine.State) { + func webSocketDidChangeState(chainId: SSFModels.ChainModel.Id, state: SSFUtils.WebSocketEngine.State) { guard let changedStateChain = chains.first(where: { chain in - chain.nodes.first { node in - node.url == url - } != nil + chain.chainId == chainId }) else { return } - - if case .connected = state { - let reconnectedEvent = ChainReconnectingEvent(chain: changedStateChain, state: state) - eventCenter.notify(with: reconnectedEvent) - } - - switch state { - case let .waitingReconnection(attempt: attempt): - if attempt > NetworkConstants.websocketReconnectAttemptsLimit { - connectionNeedsReconnect(for: changedStateChain, previusUrl: url) - } - case .notConnected: - connectionNeedsReconnect(for: changedStateChain, previusUrl: url) - default: - break - } - } - - func connectionNeedsReconnect(for chain: ChainModel, previusUrl: URL) { - guard chain.selectedNode == nil else { - return - } - - do { - if chain.isEthereum { - // TODO: Ethereum reconnect - } else { - _ = try substrateConnectionPool?.setupConnection(for: chain, ignoredUrl: previusUrl) - } - - let event = ChainsUpdatedEvent(updatedChains: [chain]) - eventCenter.notify(with: event) - } catch { - logger?.error("\(chain.name) error: \(error.localizedDescription)") - let reconnectedEvent = ChainReconnectingEvent(chain: chain, state: .notConnected) - eventCenter.notify(with: reconnectedEvent) - } + + let reconnectedEvent = ChainReconnectingEvent(chain: changedStateChain, state: state) + eventCenter.notify(with: reconnectedEvent) } -} + } // MARK: - EventVisitorProtocol diff --git a/fearless/Common/Services/ChainRegistry/ChainRegistryFactory.swift b/fearless/Common/Services/ChainRegistry/ChainRegistryFactory.swift index 227b385e33..908e49c759 100644 --- a/fearless/Common/Services/ChainRegistry/ChainRegistryFactory.swift +++ b/fearless/Common/Services/ChainRegistry/ChainRegistryFactory.swift @@ -111,10 +111,8 @@ final class ChainRegistryFactory { logger: Logger.shared ) - let queue = OperationQueue() let substrateConnectionPool = ConnectionPool( - connectionFactory: ConnectionFactory(logger: Logger.shared), - operationQueue: queue + connectionFactory: ConnectionFactory(logger: Logger.shared) ) let ethereumConnectionPool = EthereumConnectionPool() diff --git a/fearless/Common/Services/ChainRegistry/ConnectionPool/ConnectionFactory.swift b/fearless/Common/Services/ChainRegistry/ConnectionPool/ConnectionFactory.swift index 426c06b336..e0346735f0 100644 --- a/fearless/Common/Services/ChainRegistry/ConnectionPool/ConnectionFactory.swift +++ b/fearless/Common/Services/ChainRegistry/ConnectionPool/ConnectionFactory.swift @@ -6,9 +6,9 @@ typealias ChainConnection = JSONRPCEngine protocol ConnectionFactoryProtocol { func createConnection( connectionName: String?, - for url: URL, + for urls: [URL], delegate: WebSocketEngineDelegate - ) -> ChainConnection + ) throws -> ChainConnection } final class ConnectionFactory { @@ -25,12 +25,12 @@ final class ConnectionFactory { extension ConnectionFactory: ConnectionFactoryProtocol { func createConnection( connectionName: String?, - for url: URL, + for urls: [URL], delegate: WebSocketEngineDelegate - ) -> ChainConnection { - let engine = WebSocketEngine( + ) throws -> ChainConnection { + let engine = try WebSocketEngine( connectionName: connectionName, - url: url, + urls: urls, processingQueue: processingQueue, logger: nil ) diff --git a/fearless/Common/Services/ChainRegistry/ConnectionPool/ConnectionPool.swift b/fearless/Common/Services/ChainRegistry/ConnectionPool/ConnectionPool.swift index a8b4aec439..68d48dee7d 100644 --- a/fearless/Common/Services/ChainRegistry/ConnectionPool/ConnectionPool.swift +++ b/fearless/Common/Services/ChainRegistry/ConnectionPool/ConnectionPool.swift @@ -11,37 +11,35 @@ protocol ConnectionPoolProtocol { associatedtype T func setupConnection(for chain: ChainModel) throws -> T - func setupConnection(for chain: ChainModel, ignoredUrl: URL?) throws -> T func getConnection(for chainId: ChainModel.Id) -> T? func setDelegate(_ delegate: ConnectionPoolDelegate) func resetConnection(for chainId: ChainModel.Id) } protocol ConnectionPoolDelegate: AnyObject { - func webSocketDidChangeState(url: URL, state: WebSocketEngine.State) + func webSocketDidChangeState(chainId: ChainModel.Id, state: WebSocketEngine.State) } final class ConnectionPool { + struct ConnectionWrapper { + let chainId: String + let connection: WeakWrapper + } + private let connectionFactory: ConnectionFactoryProtocol private let applicationHandler = ApplicationHandler() + private lazy var injector = NodeApiKeyInjector() private weak var delegate: ConnectionPoolDelegate? - private let operationQueue: OperationQueue - - private lazy var lock = ReaderWriterLock() - private lazy var connectionLock = ReaderWriterLock() - private(set) var connectionsByChainIds: [ChainModel.Id: WeakWrapper] = [:] - private var failedUrls: [ChainModel.Id: Set] = [:] + private(set) var connections: SafeArray = .init() - private func clearUnusedConnections() { - connectionLock.exclusivelyWrite { - self.connectionsByChainIds = self.connectionsByChainIds.filter { $0.value.target != nil } - } + init(connectionFactory: ConnectionFactoryProtocol) { + self.connectionFactory = connectionFactory } - init(connectionFactory: ConnectionFactoryProtocol, operationQueue: OperationQueue) { - self.connectionFactory = connectionFactory - self.operationQueue = operationQueue + private func clearUnusedConnections() { + let filtred = connections.filter { $0.connection.target != nil } + connections.replace(array: filtred) } } @@ -50,85 +48,38 @@ final class ConnectionPool { extension ConnectionPool: ConnectionPoolProtocol { typealias T = ChainConnection - func setDelegate(_ delegate: ConnectionPoolDelegate) { - self.delegate = delegate - } - func setupConnection(for chain: ChainModel) throws -> ChainConnection { - try setupConnection(for: chain, ignoredUrl: nil) - } - - func setupConnection(for chain: ChainModel, ignoredUrl: URL?) throws -> ChainConnection { - if ignoredUrl == nil, - let connection = getConnection(for: chain.chainId) { + if let connection = getConnection(for: chain.chainId) { return connection } - - var chainFailedUrls = getFailedUrls(for: chain.chainId).or([]) - chainFailedUrls.insert(ignoredUrl) - -// If all available nodes are in blacklist => Remove all nodes except last one checked - if chainFailedUrls.count == chain.nodes.count { - lock.exclusivelyWrite { [weak self] in - self?.failedUrls[chain.chainId] = [ignoredUrl] - } - } - let node = chain.selectedNode ?? chain.nodes.first(where: { - ($0.url != ignoredUrl) && !chainFailedUrls.contains($0.url) - }) - - lock.exclusivelyWrite { [weak self] in - self?.failedUrls[chain.chainId] = chainFailedUrls + let nodesForPreparing: [ChainNodeModel] + if let selectedNode = chain.selectedNode { + nodesForPreparing = [selectedNode] + } else { + nodesForPreparing = Array(chain.nodes) } - guard let node = node else { - throw ConnectionPoolError.onlyOneNode - } - let url = DwillerApiKeyInjector().apiKeyInjectedURL(node: node) - - clearUnusedConnections() - - if let connection = getConnection(for: chain.chainId) { - if connection.url == url { - return connection - } else if ignoredUrl != nil { - connection.reconnect(url: url) - return connection - } - } - - let connection = connectionFactory.createConnection( - connectionName: chain.name, - for: url, + let preparedUrls = injector.injectKey(nodes: nodesForPreparing) + let connection = try connectionFactory.createConnection( + connectionName: chain.chainId, + for: preparedUrls, delegate: self ) - let wrapper = WeakWrapper(target: connection) - - connectionLock.exclusivelyWrite { [weak self] in - self?.connectionsByChainIds[chain.chainId] = wrapper - } + let wrapper = ConnectionWrapper(chainId: chain.chainId, connection: WeakWrapper(target: connection)) + connections.append(wrapper) return connection } - func getFailedUrls(for chainId: ChainModel.Id) -> Set? { - lock.concurrentlyRead { failedUrls[chainId] } - } - func getConnection(for chainId: ChainModel.Id) -> ChainConnection? { - connectionLock.concurrentlyRead { connectionsByChainIds[chainId]?.target as? ChainConnection } + connections.first(where: { $0.chainId == chainId })?.connection.target as? ChainConnection } - - func resetConnection(for chainId: ChainModel.Id) { - if let connection = getConnection(for: chainId) { - connection.disconnectIfNeeded() - } - - connectionLock.exclusivelyWrite { - self.connectionsByChainIds = self.connectionsByChainIds.filter { $0.key != chainId } - self.failedUrls[chainId] = nil - } + + func setDelegate(_ delegate: any ConnectionPoolDelegate) { + self.delegate = delegate } + + func resetConnection(for _: ChainModel.Id) {} } // MARK: - WebSocketEngineDelegate @@ -139,24 +90,26 @@ extension ConnectionPool: WebSocketEngineDelegate { from _: WebSocketEngine.State, to newState: WebSocketEngine.State ) { - guard let previousUrl = engine.url else { + guard let chainId = engine.connectionName else { return } - delegate?.webSocketDidChangeState(url: previousUrl, state: newState) + delegate?.webSocketDidChangeState(chainId: chainId, state: newState) } } -struct DwillerApiKeyInjector { - func apiKeyInjectedURL(node: ChainNodeModel) -> URL { - guard node.name.lowercased().contains("dwellir") else { - return node.url +struct NodeApiKeyInjector { + func injectKey(nodes: [ChainNodeModel]) -> [URL] { + nodes.map { + guard $0.name.lowercased().contains("dwellir") else { + return $0.url + } + #if DEBUG + return $0.url + #else + let apiKey = DwellirNodeApiKey.dwellirApiKey + return $0.url.appendingPathComponent("/\(apiKey)") + #endif } - #if DEBUG - return node.url - #else - let apiKey = DwellirNodeApiKey.dwellirApiKey - return node.url.appendingPathComponent("/\(apiKey)") - #endif } } diff --git a/fearless/Common/Services/ChainRegistry/ConnectionPool/EthereumConnectionPool.swift b/fearless/Common/Services/ChainRegistry/ConnectionPool/EthereumConnectionPool.swift index ac76855389..474610408c 100644 --- a/fearless/Common/Services/ChainRegistry/ConnectionPool/EthereumConnectionPool.swift +++ b/fearless/Common/Services/ChainRegistry/ConnectionPool/EthereumConnectionPool.swift @@ -27,14 +27,6 @@ final class EthereumConnectionPool: ConnectionPoolProtocol { return ws } - func setupConnection(for chain: SSFModels.ChainModel, ignoredUrl _: URL?) throws -> Web3.Eth { - // TODO: Ignored URL handling - let ws = try EthereumNodeFetching().getNode(for: chain) - connectionsByChainIds[chain.chainId] = ws - - return ws - } - func getConnection(for chainId: ChainModel.Id) -> Web3.Eth? { lock.lock() defer { diff --git a/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeSyncService.swift b/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeSyncService.swift index 1a81282b36..d065acb8f4 100644 --- a/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeSyncService.swift +++ b/fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeSyncService.swift @@ -314,7 +314,7 @@ extension RuntimeSyncService: RuntimeSyncServiceProtocol { return } - if knownConnection.url != connection.url { + if knownConnection.connectionName != connection.connectionName { knownChains[chain.chainId] = connection performSync(for: chain.chainId) From bdfc3a68ba8113083b7788799a56763debdffb85 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Wed, 26 Jun 2024 12:38:01 +0500 Subject: [PATCH 051/115] [#FLW-4718] Reconnect socket after connection lost. --- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../Common/Extension/JSONRPCOperation+Result.swift | 7 +------ .../Network/Misc/SubstrateOperationFactory.swift | 11 ++++++----- .../Services/ChainRegistry/ChainRegistry.swift | 4 ++-- .../ConnectionPool/ConnectionFactory.swift | 12 ++++++++---- .../ConnectionPool/ConnectionPool.swift | 3 ++- 6 files changed, 20 insertions(+), 19 deletions(-) diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index 19758befa7..8b33c52c8e 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -133,7 +133,7 @@ "location" : "https://github.com/soramitsu/shared-features-spm.git", "state" : { "branch" : "socket-connection-refactoring", - "revision" : "e84295e599c4fc2fd097e0f13731159b008347bc" + "revision" : "229b862ae178ca87833789f81b25ead56fa3af49" } }, { diff --git a/fearless/Common/Extension/JSONRPCOperation+Result.swift b/fearless/Common/Extension/JSONRPCOperation+Result.swift index 48e440f403..6f8c25afee 100644 --- a/fearless/Common/Extension/JSONRPCOperation+Result.swift +++ b/fearless/Common/Extension/JSONRPCOperation+Result.swift @@ -3,12 +3,7 @@ import SSFUtils extension JSONRPCOperation { static func failureOperation(_ error: Error) -> JSONRPCOperation { - let mockEngine = try! WebSocketEngine( - connectionName: nil, - urls: [ApplicationConfig.shared.wiki], - autoconnect: false - ) - let operation = JSONRPCOperation(engine: mockEngine, method: "") + let operation = JSONRPCOperation(engine: nil, method: "") operation.result = .failure(error) return operation } diff --git a/fearless/Common/Network/Misc/SubstrateOperationFactory.swift b/fearless/Common/Network/Misc/SubstrateOperationFactory.swift index f47f66d014..7e37d33f8f 100644 --- a/fearless/Common/Network/Misc/SubstrateOperationFactory.swift +++ b/fearless/Common/Network/Misc/SubstrateOperationFactory.swift @@ -14,15 +14,16 @@ final class SubstrateOperationFactory: SubstrateOperationFactoryProtocol { } func fetchChainOperation(_ url: URL) -> BaseOperation { - guard let engine = try? WebSocketEngine( - connectionName: nil, + guard let connectionStrategy = ConnectionStrategyImpl( urls: [url], - reachabilityManager: nil, - reconnectionStrategy: nil, - logger: nil + callbackQueue: .global() ) else { return BaseOperation.createWithError(WebSocketEngineError.emptyUrls) } + let engine = WebSocketEngine( + connectionName: nil, + connectionStrategy: connectionStrategy + ) return JSONRPCListOperation(engine: engine, method: RPCMethod.chain) } diff --git a/fearless/Common/Services/ChainRegistry/ChainRegistry.swift b/fearless/Common/Services/ChainRegistry/ChainRegistry.swift index 6c7b8cfd8b..a3b4a88192 100644 --- a/fearless/Common/Services/ChainRegistry/ChainRegistry.swift +++ b/fearless/Common/Services/ChainRegistry/ChainRegistry.swift @@ -402,11 +402,11 @@ extension ChainRegistry: ConnectionPoolDelegate { }) else { return } - + let reconnectedEvent = ChainReconnectingEvent(chain: changedStateChain, state: state) eventCenter.notify(with: reconnectedEvent) } - } +} // MARK: - EventVisitorProtocol diff --git a/fearless/Common/Services/ChainRegistry/ConnectionPool/ConnectionFactory.swift b/fearless/Common/Services/ChainRegistry/ConnectionPool/ConnectionFactory.swift index e0346735f0..5df814e33f 100644 --- a/fearless/Common/Services/ChainRegistry/ConnectionPool/ConnectionFactory.swift +++ b/fearless/Common/Services/ChainRegistry/ConnectionPool/ConnectionFactory.swift @@ -28,11 +28,15 @@ extension ConnectionFactory: ConnectionFactoryProtocol { for urls: [URL], delegate: WebSocketEngineDelegate ) throws -> ChainConnection { - let engine = try WebSocketEngine( - connectionName: connectionName, + guard let connectionStrategy = ConnectionStrategyImpl( urls: urls, - processingQueue: processingQueue, - logger: nil + callbackQueue: processingQueue + ) else { + throw ConnectionPoolError.noConnection + } + let engine = WebSocketEngine( + connectionName: connectionName, + connectionStrategy: connectionStrategy ) engine.delegate = delegate return engine diff --git a/fearless/Common/Services/ChainRegistry/ConnectionPool/ConnectionPool.swift b/fearless/Common/Services/ChainRegistry/ConnectionPool/ConnectionPool.swift index 68d48dee7d..6c18c88932 100644 --- a/fearless/Common/Services/ChainRegistry/ConnectionPool/ConnectionPool.swift +++ b/fearless/Common/Services/ChainRegistry/ConnectionPool/ConnectionPool.swift @@ -5,6 +5,7 @@ import SSFModels enum ConnectionPoolError: Error { case onlyOneNode + case noConnection } protocol ConnectionPoolProtocol { @@ -74,7 +75,7 @@ extension ConnectionPool: ConnectionPoolProtocol { func getConnection(for chainId: ChainModel.Id) -> ChainConnection? { connections.first(where: { $0.chainId == chainId })?.connection.target as? ChainConnection } - + func setDelegate(_ delegate: any ConnectionPoolDelegate) { self.delegate = delegate } From 5d0973e21a3bc552d63b72da5042e5a487ea812b Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Wed, 26 Jun 2024 13:51:02 +0500 Subject: [PATCH 052/115] [#FLW-4718] Reconnect socket after connection lost. --- fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved | 2 +- .../ChainRegistry/ConnectionPool/ConnectionFactory.swift | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8b33c52c8e..a168f8fdff 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -133,7 +133,7 @@ "location" : "https://github.com/soramitsu/shared-features-spm.git", "state" : { "branch" : "socket-connection-refactoring", - "revision" : "229b862ae178ca87833789f81b25ead56fa3af49" + "revision" : "306b42bebe0126c2c5bf5f3afef3ccca6abae901" } }, { diff --git a/fearless/Common/Services/ChainRegistry/ConnectionPool/ConnectionFactory.swift b/fearless/Common/Services/ChainRegistry/ConnectionPool/ConnectionFactory.swift index 5df814e33f..15432088ab 100644 --- a/fearless/Common/Services/ChainRegistry/ConnectionPool/ConnectionFactory.swift +++ b/fearless/Common/Services/ChainRegistry/ConnectionPool/ConnectionFactory.swift @@ -36,7 +36,8 @@ extension ConnectionFactory: ConnectionFactoryProtocol { } let engine = WebSocketEngine( connectionName: connectionName, - connectionStrategy: connectionStrategy + connectionStrategy: connectionStrategy, + processingQueue: processingQueue ) engine.delegate = delegate return engine From 8c2c71e1673b3fcb5a70272ea95c897c84732b87 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Wed, 26 Jun 2024 15:11:15 +0500 Subject: [PATCH 053/115] [#FLW-4725] Liberland network. There is not possible to past "Send to" address --- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../Modules/CrossChain/CrossChainPresenter.swift | 16 +++------------- .../CrossChain/CrossChainViewModelFactory.swift | 6 +++--- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index a168f8fdff..d664ab8e95 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -133,7 +133,7 @@ "location" : "https://github.com/soramitsu/shared-features-spm.git", "state" : { "branch" : "socket-connection-refactoring", - "revision" : "306b42bebe0126c2c5bf5f3afef3ccca6abae901" + "revision" : "a09aef88d1c2abdca3ced02694e123366a551d06" } }, { diff --git a/fearless/Modules/CrossChain/CrossChainPresenter.swift b/fearless/Modules/CrossChain/CrossChainPresenter.swift index 42d3050cd7..d385ffc8fb 100644 --- a/fearless/Modules/CrossChain/CrossChainPresenter.swift +++ b/fearless/Modules/CrossChain/CrossChainPresenter.swift @@ -246,18 +246,11 @@ final class CrossChainPresenter { } private func handle(newAddress: String) { - guard let destChain = selectedDestChainModel else { - return - } loadingCollector.addressExists = !newAddress.isEmpty checkLoadingState() interactor.fetchDestinationAccountInfo(address: newAddress) recipientAddress = newAddress - let isValid = interactor.validate(address: newAddress, for: destChain).isValid - let viewModel = viewModelFactory.buildRecipientViewModel( - address: newAddress, - isValid: isValid - ) + let viewModel = viewModelFactory.buildRecipientViewModel(address: newAddress) view?.didReceive(recipientViewModel: viewModel) } @@ -268,7 +261,7 @@ final class CrossChainPresenter { guard let chain = selectedDestChainModel else { return } - let isValid = interactor.validate(address: recipientAddress, for: chain).isValid + let isValid = interactor.validate(address: recipientAddress, for: chain).isValidOrSame if isValid, let recipientAddress = recipientAddress { handle(newAddress: recipientAddress) } else { @@ -819,10 +812,7 @@ extension CrossChainPresenter: WalletsManagmentModuleOutput { return } - let viewModel = viewModelFactory.buildRecipientViewModel( - address: address, - isValid: true - ) + let viewModel = viewModelFactory.buildRecipientViewModel(address: address) view?.didReceive(recipientViewModel: viewModel) destWallet = wallet recipientAddress = address diff --git a/fearless/Modules/CrossChain/CrossChainViewModelFactory.swift b/fearless/Modules/CrossChain/CrossChainViewModelFactory.swift index 1f6b75ec9e..0be88a9fa7 100644 --- a/fearless/Modules/CrossChain/CrossChainViewModelFactory.swift +++ b/fearless/Modules/CrossChain/CrossChainViewModelFactory.swift @@ -5,7 +5,7 @@ import SSFModels protocol CrossChainViewModelFactoryProtocol { func buildNetworkViewModel(chain: ChainModel) -> SelectNetworkViewModel - func buildRecipientViewModel(address: String, isValid: Bool) -> RecipientViewModel + func buildRecipientViewModel(address: String) -> RecipientViewModel } final class CrossChainViewModelFactory: CrossChainViewModelFactoryProtocol { @@ -23,11 +23,11 @@ final class CrossChainViewModelFactory: CrossChainViewModelFactoryProtocol { ) } - func buildRecipientViewModel(address: String, isValid: Bool) -> RecipientViewModel { + func buildRecipientViewModel(address: String) -> RecipientViewModel { RecipientViewModel( address: address, icon: try? iconGenerator.generateFromAddress(address), - isValid: isValid, + isValid: true, canEditing: true ) } From 1908218c9d89c2937e68b91a235c6fe8ac47206b Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Wed, 26 Jun 2024 16:29:14 +0500 Subject: [PATCH 054/115] =?UTF-8?q?[#FLW-4683]=20We=20can=E2=80=99t=20read?= =?UTF-8?q?=20ETH=20QR=20with=20amount=20in=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fearless.xcodeproj/project.pbxproj | 4 --- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../QRService/QRExtractionServiceError.swift | 8 ----- .../GetPreinstalledWalletPresenter.swift | 9 +++--- fearless/Modules/ScanQR/ScanQRPresenter.swift | 30 ++++++++++++------- .../Modules/ScanQR/ScanQRViewController.swift | 8 +++-- 6 files changed, 30 insertions(+), 31 deletions(-) delete mode 100644 fearless/ApplicationLayer/Services/QRService/QRExtractionServiceError.swift diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index 339e0ed433..24e7de5be5 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -1895,7 +1895,6 @@ FA00489B282CCFDC0032FF49 /* SelectValidatorsStartRelaychainViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA00489A282CCFDC0032FF49 /* SelectValidatorsStartRelaychainViewModelFactory.swift */; }; FA0066E92935D07D0068FC61 /* RecommendedValidatorListPoolStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA0066E82935D07D0068FC61 /* RecommendedValidatorListPoolStrategy.swift */; }; FA072C14277AE2FE00731718 /* QRCaptureService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA072C13277AE2FE00731718 /* QRCaptureService.swift */; }; - FA072C16277B023D00731718 /* QRExtractionServiceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA072C15277B023D00731718 /* QRExtractionServiceError.swift */; }; FA072C21277B19A900731718 /* ImageGalleryProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA072C20277B19A900731718 /* ImageGalleryProtocols.swift */; }; FA072C25277C0FA900731718 /* ApplicationSettingsPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA072C24277C0FA900731718 /* ApplicationSettingsPresentable.swift */; }; FA08B3652A31A09E00D9D126 /* BigUInt+StringInitializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA08B3642A31A09E00D9D126 /* BigUInt+StringInitializable.swift */; }; @@ -4977,7 +4976,6 @@ FA00489A282CCFDC0032FF49 /* SelectValidatorsStartRelaychainViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectValidatorsStartRelaychainViewModelFactory.swift; sourceTree = ""; }; FA0066E82935D07D0068FC61 /* RecommendedValidatorListPoolStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecommendedValidatorListPoolStrategy.swift; sourceTree = ""; }; FA072C13277AE2FE00731718 /* QRCaptureService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCaptureService.swift; sourceTree = ""; }; - FA072C15277B023D00731718 /* QRExtractionServiceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRExtractionServiceError.swift; sourceTree = ""; }; FA072C20277B19A900731718 /* ImageGalleryProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGalleryProtocols.swift; sourceTree = ""; }; FA072C24277C0FA900731718 /* ApplicationSettingsPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationSettingsPresentable.swift; sourceTree = ""; }; FA08B3642A31A09E00D9D126 /* BigUInt+StringInitializable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BigUInt+StringInitializable.swift"; sourceTree = ""; }; @@ -12292,7 +12290,6 @@ isa = PBXGroup; children = ( FA072C13277AE2FE00731718 /* QRCaptureService.swift */, - FA072C15277B023D00731718 /* QRExtractionServiceError.swift */, ); path = QRService; sourceTree = ""; @@ -18220,7 +18217,6 @@ 8BF525D6B5DFB7CF6C03B015 /* AnalyticsValidatorsViewController.swift in Sources */, FA6262662AC2E35A005D3D95 /* WalletConnectProposalAssembly.swift in Sources */, 237AD34CD1C2778834D7B330 /* AnalyticsValidatorsViewFactory.swift in Sources */, - FA072C16277B023D00731718 /* QRExtractionServiceError.swift in Sources */, FA37AE2A2859BA1D001DCA96 /* StakingBondMoreConfirmationParachainStrategy.swift in Sources */, 31E260D462BF33CFCDFEBA6C /* AnalyticsRewardDetailsProtocols.swift in Sources */, FA6262482AC2E35A005D3D95 /* WalletConnectActiveSessionsViewController.swift in Sources */, diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index d664ab8e95..adaf4c127a 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -133,7 +133,7 @@ "location" : "https://github.com/soramitsu/shared-features-spm.git", "state" : { "branch" : "socket-connection-refactoring", - "revision" : "a09aef88d1c2abdca3ced02694e123366a551d06" + "revision" : "1527989589adeff7821582ec80b8dcf7698f7bee" } }, { diff --git a/fearless/ApplicationLayer/Services/QRService/QRExtractionServiceError.swift b/fearless/ApplicationLayer/Services/QRService/QRExtractionServiceError.swift deleted file mode 100644 index 838b5ae218..0000000000 --- a/fearless/ApplicationLayer/Services/QRService/QRExtractionServiceError.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation - -enum QRExtractionServiceError: Error { - case invalidImage - case detectorUnavailable - case noFeatures - case plainAddress(address: String) -} diff --git a/fearless/Modules/GetPreinstalledWallet/GetPreinstalledWalletPresenter.swift b/fearless/Modules/GetPreinstalledWallet/GetPreinstalledWalletPresenter.swift index da2043db21..ce05e6117d 100644 --- a/fearless/Modules/GetPreinstalledWallet/GetPreinstalledWalletPresenter.swift +++ b/fearless/Modules/GetPreinstalledWallet/GetPreinstalledWalletPresenter.swift @@ -1,4 +1,5 @@ import Foundation +import SSFQRService import SoraFoundation import AVFoundation @@ -85,14 +86,12 @@ final class GetPreinstalledWalletPresenter: NSObject { } } - private func handleQRExtractionService(error: QRExtractionServiceError) { + private func handleQRExtractionService(error: QRExtractionError) { switch error { - case .noFeatures: + case .noFeatures, .invalidQrCode, .severalCoincidences: view?.present(message: L10n.InvoiceScan.Error.noInfo, animated: true) case .detectorUnavailable, .invalidImage: view?.present(message: L10n.InvoiceScan.Error.invalidImage, animated: true) - case .plainAddress: - break } } @@ -198,7 +197,7 @@ extension GetPreinstalledWalletPresenter: GetPreinstalledWalletInteractorOutput return } - if let extractionError = error as? QRExtractionServiceError { + if let extractionError = error as? QRExtractionError { handleQRExtractionService(error: extractionError) return } diff --git a/fearless/Modules/ScanQR/ScanQRPresenter.swift b/fearless/Modules/ScanQR/ScanQRPresenter.swift index 2833e3840e..7612ce6315 100644 --- a/fearless/Modules/ScanQR/ScanQRPresenter.swift +++ b/fearless/Modules/ScanQR/ScanQRPresenter.swift @@ -67,14 +67,24 @@ final class ScanQRPresenter: NSObject { } } - private func handleQRExtractionService(error: QRExtractionServiceError) { - switch error { - case .noFeatures: - view?.present(message: L10n.InvoiceScan.Error.noInfo, animated: true) - case .detectorUnavailable, .invalidImage: - view?.present(message: L10n.InvoiceScan.Error.invalidImage, animated: true) - case .plainAddress: - break + private func handleQRExtractionService() { + DispatchQueue.main.async { + self.view?.didStartLoading() + let viewModel = SheetAlertPresentableViewModel( + title: R.string.localizable.commonUndefinedErrorMessage( + preferredLanguages: self.selectedLocale.rLanguages + ), + message: nil, + actions: [], + closeAction: nil, + dismissCompletion: { [weak self] in + self?.scanState = .initializing(accessRequested: true) + DispatchQueue.global().async { + self?.interactor.startScanning() + } + } + ) + self.router.present(viewModel: viewModel, from: self.view) } } @@ -170,8 +180,8 @@ extension ScanQRPresenter: ScanQRInteractorOutput { return } - if let extractionError = error as? QRExtractionServiceError { - handleQRExtractionService(error: extractionError) + if let _ = error as? QRExtractionError { + handleQRExtractionService() return } diff --git a/fearless/Modules/ScanQR/ScanQRViewController.swift b/fearless/Modules/ScanQR/ScanQRViewController.swift index 97e5ce8912..d81af04b41 100644 --- a/fearless/Modules/ScanQR/ScanQRViewController.swift +++ b/fearless/Modules/ScanQR/ScanQRViewController.swift @@ -155,9 +155,11 @@ extension ScanQRViewController: ScanQRViewInput { } func present(message: String, animated _: Bool) { - rootView.messageLabel.text = message - rootView.messageLabel.alpha = 1.0 - scheduleMessageHide() + DispatchQueue.main.async { + self.rootView.messageLabel.text = message + self.rootView.messageLabel.alpha = 1.0 + self.scheduleMessageHide() + } } } From d9e8713186e2dd0b2c093ae8a42a1dc029523411 Mon Sep 17 00:00:00 2001 From: Alex Lebedko Date: Thu, 27 Jun 2024 16:48:28 +0700 Subject: [PATCH 055/115] bug fix and optimisations for slow internet connection --- .../xcshareddata/swiftpm/Package.resolved | 18 +- .../Model/SoraSubqueryPriceResponse.swift | 6 + .../Banners/BannersViewModelFactory.swift | 2 +- .../Common/LiquidityPools+ViewModel.swift | 26 + .../LiquidityPoolDetailsInteractor.swift | 12 +- .../LiquidityPoolDetailsPresenter.swift | 45 +- .../LiquidityPoolDetailsProtocols.swift | 4 +- .../LiquidityPoolDetailsRouter.swift | 4 +- .../LiquidityPoolDetailsViewController.swift | 12 + .../LiquidityPoolDetailsViewLayout.swift | 49 +- ...LiquidityPoolDetailsViewModelFactory.swift | 2 +- ...quidityPoolRemoveLiquidityInteractor.swift | 14 - ...iquidityPoolRemoveLiquidityPresenter.swift | 41 +- ...quidityPoolRemoveLiquidityViewLayout.swift | 6 +- ...PoolRemoveLiquidityConfirmViewLayout.swift | 2 +- .../LiquidityPoolSupplyAssembly.swift | 5 +- .../LiquidityPoolSupplyInteractor.swift | 34 +- .../LiquidityPoolSupplyPresenter.swift | 47 +- .../LiquidityPoolSupplyViewController.swift | 1 + .../LiquidityPoolSupplyViewLayout.swift | 6 +- .../LiquidityPoolSupplyConfirmAssembly.swift | 1 + ...LiquidityPoolSupplyConfirmInteractor.swift | 14 - .../LiquidityPoolSupplyConfirmPresenter.swift | 34 +- ...LiquidityPoolSupplyConfirmViewLayout.swift | 10 +- ...vailableLiquidityPoolsListInteractor.swift | 27 +- ...AvailableLiquidityPoolsListPresenter.swift | 26 +- ...leLiquidityPoolsListViewModelFactory.swift | 29 +- .../UserLiquidityPoolsListInteractor.swift | 58 +- .../UserLiquidityPoolsListPresenter.swift | 29 +- ...erLiquidityPoolsListViewModelFactory.swift | 26 +- .../LiquidityPoolsListProtocols.swift | 5 +- .../LiquidityPoolsListViewController.swift | 12 + .../LiquidityPoolsListViewLayout.swift | 8 +- .../View/LiquidityPoolListCell.swift | 15 +- .../LiquidityPoolListCellModel.swift | 8 +- .../LiquidityPoolsOverviewAssembly.swift | 3 + .../LiquidityPoolsOverviewPresenter.swift | 6 + .../LiquidityPoolsOverviewViewLayout.swift | 2 +- fearless/en.lproj/Localizable.strings | 175 +- fearless/id.lproj/Localizable.strings | 173 +- fearless/ja.lproj/Localizable.strings | 52 +- fearless/pt-PT.lproj/Localizable.strings | 1 + fearless/pt.lproj/Localizable.strings | 2206 +++++++++-------- fearless/ru.lproj/Localizable.strings | 173 +- fearless/tr.lproj/Localizable.strings | 54 +- fearless/vi.lproj/Localizable.strings | 177 +- fearless/zh-Hans.lproj/Localizable.strings | 182 +- 47 files changed, 1839 insertions(+), 2003 deletions(-) diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1ff63b0a78..590d0594c1 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -126,15 +126,6 @@ "version" : "0.1.7" } }, - { - "identity" : "shared-features-spm", - "kind" : "remoteSourceControl", - "location" : "https://github.com/soramitsu/shared-features-spm.git", - "state" : { - "branch" : "liquidity-pools-fw", - "revision" : "d4d1739f097139599aeaa800d596e0a71a17e82a" - } - }, { "identity" : "swift-atomics", "kind" : "remoteSourceControl", @@ -225,6 +216,15 @@ "version" : "1.3.0" } }, + { + "identity" : "swiftformat", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nicklockwood/SwiftFormat", + "state" : { + "revision" : "dd989a46d0c6f15c016484bab8afe5e7a67a4022", + "version" : "0.54.0" + } + }, { "identity" : "swiftimagereadwrite", "kind" : "remoteSourceControl", diff --git a/fearless/ApplicationLayer/Pricing/Model/SoraSubqueryPriceResponse.swift b/fearless/ApplicationLayer/Pricing/Model/SoraSubqueryPriceResponse.swift index 18478a95ae..a995224074 100644 --- a/fearless/ApplicationLayer/Pricing/Model/SoraSubqueryPriceResponse.swift +++ b/fearless/ApplicationLayer/Pricing/Model/SoraSubqueryPriceResponse.swift @@ -11,6 +11,12 @@ struct SoraSubqueryPricePage: Decodable { } struct SoraSubqueryPrice: Decodable { + enum CodingKeys: String, CodingKey { + case id + case priceUsd = "priceUSD" + case priceChangeDay + } + let id: String let priceUsd: String? let priceChangeDay: Decimal? diff --git a/fearless/Modules/Banners/BannersViewModelFactory.swift b/fearless/Modules/Banners/BannersViewModelFactory.swift index 97d4c5a540..72aa1a2042 100644 --- a/fearless/Modules/Banners/BannersViewModelFactory.swift +++ b/fearless/Modules/Banners/BannersViewModelFactory.swift @@ -57,7 +57,7 @@ final class BannersViewModelFactory: BannersViewModelFactoryProtocol { ) case .liquidityPools: let title = "Liquidity pools" - let subtitle = "Invest your funds in Liquidity pools and receive rewards" + let subtitle = "Invest your funds in Liquidity\npools and receive rewards" let buttonAction = "Show details" return BannerCellViewModel( diff --git a/fearless/Modules/LiquidityPools/Common/LiquidityPools+ViewModel.swift b/fearless/Modules/LiquidityPools/Common/LiquidityPools+ViewModel.swift index 9218c85f82..5bb93561a2 100644 --- a/fearless/Modules/LiquidityPools/Common/LiquidityPools+ViewModel.swift +++ b/fearless/Modules/LiquidityPools/Common/LiquidityPools+ViewModel.swift @@ -5,6 +5,7 @@ import SSFPools protocol LiquidityPoolsModelFactory { func buildReserves(pool: LiquidityPair, chain: ChainModel, reserves: PolkaswapPoolReservesInfo?, baseAssetPrice: PriceData?, targetAssetPrice: PriceData?) -> Decimal? + func buildReserves(accountPool: AccountPool, chain: ChainModel, reserves: PolkaswapPoolReservesInfo?, baseAssetPrice: PriceData?, targetAssetPrice: PriceData?) -> Decimal? } final class LiquidityPoolsModelFactoryDefault: LiquidityPoolsModelFactory { @@ -32,4 +33,29 @@ final class LiquidityPoolsModelFactoryDefault: LiquidityPoolsModelFactory { return poolReservesFiatValue } + + func buildReserves(accountPool: AccountPool, chain: ChainModel, reserves: PolkaswapPoolReservesInfo?, baseAssetPrice: PriceData?, targetAssetPrice: PriceData?) -> Decimal? { + let baseAsset = chain.assets.first(where: { $0.currencyId == accountPool.baseAssetId }) + let targetAsset = chain.assets.first(where: { $0.currencyId == accountPool.targetAssetId }) + + guard let baseAsset, let targetAsset else { + return nil + } + + let poolReservesValue = (reserves?.reserves.reserves).flatMap { Decimal.fromSubstrateAmount($0, precision: Int16(baseAsset.precision)) } + let baseAssetPriceValue = (baseAssetPrice?.price).flatMap { Decimal(string: $0) } + + let poolFeeValue = (reserves?.reserves.fee).flatMap { Decimal.fromSubstrateAmount($0, precision: Int16(targetAsset.precision)) } + let targetAssetPriceValue = (targetAssetPrice?.price).flatMap { Decimal(string: $0) } + + let poolReservesFiatValue: Decimal? = poolReservesValue.flatMap { poolReserves in + guard let baseAssetPriceValue, let poolFeeValue, let targetAssetPriceValue else { + return nil + } + + return (poolReserves * baseAssetPriceValue) + (poolFeeValue * targetAssetPriceValue) + } + + return poolReservesFiatValue + } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsInteractor.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsInteractor.swift index 6b678bfc96..14626a662c 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsInteractor.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsInteractor.swift @@ -91,9 +91,15 @@ extension LiquidityPoolDetailsInteractor: LiquidityPoolDetailsInteractorInput { Task { do { - let accountPool = try await liquidityPoolService.fetchUserPool(assetIdPair: assetIdPair, accountId: accountId) - await MainActor.run { - output?.didReceiveUserPool(pool: accountPool) + let accountPoolStream = try await liquidityPoolService.subscribeUserPools(accountId: accountId) + for try await accountPools in accountPoolStream { + guard let pool = accountPools.value?.first(where: { $0.poolId == assetIdPair.poolId }) else { + return + } + + await MainActor.run { + output?.didReceiveUserPool(pool: pool) + } } } catch { await MainActor.run { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift index 3c14d77cde..ecbdc1f860 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift @@ -7,16 +7,27 @@ import SSFStorageQueryKit enum LiquidityPoolDetailsInput { case initial - case userPool(liquidityPair: LiquidityPair?, reserves: PolkaswapPoolReservesInfo?, apyInfo: PoolApyInfo?, accountPool: AccountPool?) - case availablePool(liquidityPair: LiquidityPair?, reserves: PolkaswapPoolReservesInfo?, apyInfo: PoolApyInfo?) + case userPool(liquidityPair: LiquidityPair?, reserves: PolkaswapPoolReservesInfo?, apyInfo: PoolApyInfo?, accountPool: AccountPool?, availablePairs: [LiquidityPair]?) + case availablePool(liquidityPair: LiquidityPair?, reserves: PolkaswapPoolReservesInfo?, apyInfo: PoolApyInfo?, availablePairs: [LiquidityPair]?) + + var availablePairs: [LiquidityPair]? { + switch self { + case .initial: + return nil + case let .userPool(_, _, _, _, availablePairs): + return availablePairs + case let .availablePool(_, _, _, availablePairs): + return availablePairs + } + } var liquidityPair: LiquidityPair? { switch self { case .initial: return nil - case let .userPool(liquidityPair, _, _, _): + case let .userPool(liquidityPair, _, _, _, _): return liquidityPair - case let .availablePool(liquidityPair, _, _): + case let .availablePool(liquidityPair, _, _, _): return liquidityPair } } @@ -25,9 +36,9 @@ enum LiquidityPoolDetailsInput { switch self { case .initial: return nil - case let .userPool(_, reserves, _, _): + case let .userPool(_, reserves, _, _, _): return reserves - case let .availablePool(_, reserves, _): + case let .availablePool(_, reserves, _, _): return reserves } } @@ -36,9 +47,9 @@ enum LiquidityPoolDetailsInput { switch self { case .initial: return nil - case let .userPool(_, _, apyInfo, _): + case let .userPool(_, _, apyInfo, _, _): return apyInfo - case let .availablePool(_, _, apyInfo): + case let .availablePool(_, _, apyInfo, _): return apyInfo } } @@ -47,7 +58,7 @@ enum LiquidityPoolDetailsInput { switch self { case .initial: return nil - case let .userPool(_, _, _, accountPool): + case let .userPool(_, _, _, accountPool, _): return accountPool case .availablePool: return nil @@ -113,6 +124,8 @@ final class LiquidityPoolDetailsPresenter { self.wallet = wallet self.input = input + liquidityPair = input.liquidityPair + self.localizationManager = localizationManager } @@ -161,7 +174,7 @@ extension LiquidityPoolDetailsPresenter: LiquidityPoolDetailsViewOutput { return } - router.showSupplyFlow(liquidityPair: liquidityPair, chain: chain, wallet: wallet, from: view) + router.showSupplyFlow(liquidityPair: liquidityPair, chain: chain, wallet: wallet, availablePairs: input.availablePairs, from: view) } func removeButtonClicked() { @@ -171,6 +184,18 @@ extension LiquidityPoolDetailsPresenter: LiquidityPoolDetailsViewOutput { router.showRemoveFlow(liquidityPair: liquidityPair, chain: chain, wallet: wallet, from: view) } + + func didTapApyInfo() { + var infoText: String + var infoTitle: String + infoTitle = R.string.localizable.lpApyAlertTitle(preferredLanguages: selectedLocale.rLanguages) + infoText = R.string.localizable.lpApyAlertText(preferredLanguages: selectedLocale.rLanguages) + router.presentInfo( + message: infoText, + title: infoTitle, + from: view + ) + } } // MARK: - LiquidityPoolDetailsInteractorOutput diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsProtocols.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsProtocols.swift index ed37be2125..aacae2a6bb 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsProtocols.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsProtocols.swift @@ -3,8 +3,8 @@ import SSFModels typealias LiquidityPoolDetailsModuleCreationResult = (view: LiquidityPoolDetailsViewInput, input: LiquidityPoolDetailsModuleInput) -protocol LiquidityPoolDetailsRouterInput: AnyObject, AnyDismissable { - func showSupplyFlow(liquidityPair: LiquidityPair, chain: ChainModel, wallet: MetaAccountModel, from view: ControllerBackedProtocol?) +protocol LiquidityPoolDetailsRouterInput: AnyObject, AnyDismissable, SheetAlertPresentable { + func showSupplyFlow(liquidityPair: LiquidityPair, chain: ChainModel, wallet: MetaAccountModel, availablePairs: [LiquidityPair]?, from view: ControllerBackedProtocol?) func showRemoveFlow(liquidityPair: LiquidityPair, chain: ChainModel, wallet: MetaAccountModel, from view: ControllerBackedProtocol?) } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsRouter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsRouter.swift index f36f09cdc4..322e21b01f 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsRouter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsRouter.swift @@ -3,8 +3,8 @@ import SSFModels import SSFPools final class LiquidityPoolDetailsRouter: LiquidityPoolDetailsRouterInput { - func showSupplyFlow(liquidityPair: LiquidityPair, chain: ChainModel, wallet: MetaAccountModel, from view: ControllerBackedProtocol?) { - guard let controller = LiquidityPoolSupplyAssembly.configureModule(chain: chain, wallet: wallet, liquidityPair: liquidityPair)?.view.controller else { + func showSupplyFlow(liquidityPair: LiquidityPair, chain: ChainModel, wallet: MetaAccountModel, availablePairs: [LiquidityPair]?, from view: ControllerBackedProtocol?) { + guard let controller = LiquidityPoolSupplyAssembly.configureModule(chain: chain, wallet: wallet, liquidityPair: liquidityPair, availablePairs: availablePairs)?.view.controller else { return } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewController.swift index fd921e3048..6664f08b31 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewController.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewController.swift @@ -6,6 +6,7 @@ protocol LiquidityPoolDetailsViewOutput: AnyObject { func backButtonClicked() func supplyButtonClicked() func removeButtonClicked() + func didTapApyInfo() } final class LiquidityPoolDetailsViewController: UIViewController, ViewHolder, HiddableBarWhenPushed { @@ -50,9 +51,20 @@ final class LiquidityPoolDetailsViewController: UIViewController, ViewHolder, Hi rootView.removeButton.addAction { [weak self] in self?.output.removeButtonClicked() } + + let tapApyInfo = UITapGestureRecognizer( + target: self, + action: #selector(handleTapApyInfo) + ) + rootView.apyView + .addGestureRecognizer(tapApyInfo) } // MARK: - Private methods + + @objc private func handleTapApyInfo() { + output.didTapApyInfo() + } } // MARK: - LiquidityPoolDetailsViewInput diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift index 195a2bd6b5..9a42f2fdde 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsViewLayout.swift @@ -1,6 +1,12 @@ import UIKit final class LiquidityPoolDetailsViewLayout: UIView { + private enum Constants { + static let imageWidth: CGFloat = 12 + static let imageHeight: CGFloat = 12 + static let imageVerticalPosition: CGFloat = 2 + } + let navigationBar: BaseNavigationBar = { let bar = BaseNavigationBar() bar.set(.push) @@ -122,11 +128,11 @@ final class LiquidityPoolDetailsViewLayout: UIView { targetAssetPooledView.bindBalance(viewModel: viewModel?.targetAssetViewModel) if let baseAssetName = viewModel?.baseAssetName.uppercased() { - baseAssetPooledView.titleLabel.text = "Your \(baseAssetName) Pooled" + baseAssetPooledView.titleLabel.text = R.string.localizable.lpTokenPooledText(baseAssetName, preferredLanguages: locale.rLanguages) } if let targetAssetName = viewModel?.targetAssetName.uppercased() { - targetAssetPooledView.titleLabel.text = "Your \(targetAssetName) Pooled" + targetAssetPooledView.titleLabel.text = R.string.localizable.lpTokenPooledText(targetAssetName, preferredLanguages: locale.rLanguages) } viewModel?.rewardTokenIconViewModel?.loadImage( @@ -233,10 +239,39 @@ final class LiquidityPoolDetailsViewLayout: UIView { private func applyLocalization() { reservesView.titleLabel.text = "TVL" - apyView.titleLabel.text = "Strategic Bonus APY" - rewardTokenView.titleLabel.text = "Rewards Payout In" - supplyButton.imageWithTitleView?.title = "Supply Liquidity" - removeButton.imageWithTitleView?.title = "Remove Liquidity" - navigationBar.setTitle("Pool details") + rewardTokenView.titleLabel.text = R.string.localizable.lpRewardTokenTitle(preferredLanguages: locale.rLanguages) + supplyButton.imageWithTitleView?.title = R.string.localizable.lpSupplyButtonTitle(preferredLanguages: locale.rLanguages) + removeButton.imageWithTitleView?.title = R.string.localizable.lpRemoveButtonTitle(preferredLanguages: locale.rLanguages) + navigationBar.setTitle(R.string.localizable.lpPoolDetailsTitle(preferredLanguages: locale.rLanguages)) + + let texts = [ + R.string.localizable + .lpApyTitle(preferredLanguages: locale.rLanguages) + ] + + [ + apyView.titleLabel + ].enumerated().forEach { index, label in + setInfoImage(for: label, text: texts[index]) + } + } + + private func setInfoImage(for label: UILabel, text: String) { + let attributedString = NSMutableAttributedString(string: text) + + let imageAttachment = NSTextAttachment() + imageAttachment.image = R.image.iconInfoFilled() + imageAttachment.bounds = CGRect( + x: 0, + y: -Constants.imageVerticalPosition, + width: Constants.imageWidth, + height: Constants.imageHeight + ) + + let imageString = NSAttributedString(attachment: imageAttachment) + attributedString.append(NSAttributedString(string: " ")) + attributedString.append(imageString) + + label.attributedText = attributedString } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/ViewModel/LiquidityPoolDetailsViewModelFactory.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/ViewModel/LiquidityPoolDetailsViewModelFactory.swift index b7be5521d4..0bc245678a 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/ViewModel/LiquidityPoolDetailsViewModelFactory.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/ViewModel/LiquidityPoolDetailsViewModelFactory.swift @@ -86,7 +86,7 @@ final class LiquidityPoolDetailsViewModelFactoryDefault: LiquidityPoolDetailsVie let apyViewModel = apyLabelText.flatMap { TitleMultiValueViewModel(title: $0, subtitle: nil) } return LiquidityPoolDetailsViewModel( - pairTitleLabelText: "\(baseAsset.symbol.uppercased()) > \(targetAsset.symbol.uppercased())", + pairTitleLabelText: "\(baseAsset.symbol.uppercased()) › \(targetAsset.symbol.uppercased())", baseAssetName: baseAsset.symbol.uppercased(), targetAssetName: targetAsset.symbol.uppercased(), reservesViewModel: reservesViewModel, diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityInteractor.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityInteractor.swift index 0ea98ecb8a..fffc075e8b 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityInteractor.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityInteractor.swift @@ -7,8 +7,6 @@ import BigInt protocol LiquidityPoolRemoveLiquidityInteractorOutput: AnyObject { func didReceivePricesData(result: Result<[PriceData], Error>) func didReceiveAccountInfo(result: Result, for chainAsset: ChainAsset) - func didReceiveDexId(_ dexId: String) - func didReceiveDexIdError(_ error: Error) func didReceiveUserPool(pool: AccountPool?) func didReceiveUserPoolError(error: Error) func didReceiveFee(_ fee: BigUInt) @@ -73,17 +71,6 @@ final class LiquidityPoolRemoveLiquidityInteractor { ) } - private func fetchDexId() { - Task { - do { - let dexId = try await lpDataService.fetchDexId(baseAssetId: liquidityPair.baseAssetId) - output?.didReceiveDexId(dexId) - } catch { - output?.didReceiveDexIdError(error) - } - } - } - private func fetchReserves() { Task { do { @@ -147,7 +134,6 @@ final class LiquidityPoolRemoveLiquidityInteractor { extension LiquidityPoolRemoveLiquidityInteractor: LiquidityPoolRemoveLiquidityInteractorInput { func setup(with output: LiquidityPoolRemoveLiquidityInteractorOutput) { self.output = output - fetchDexId() fetchReserves() fetchUserPool() fetchTotalIssuance() diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityPresenter.swift index 954c577216..58280ca511 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityPresenter.swift @@ -6,20 +6,18 @@ import SSFModels import SSFPolkaswap struct RemoveLiquidityLoadingCollector { - var dexIdReady: Bool var totalIssuanceReady: Bool var reservesReady: Bool var feeReady: Bool init() { - dexIdReady = false totalIssuanceReady = false reservesReady = false feeReady = false } var isReady: Bool { - dexIdReady && totalIssuanceReady && reservesReady && feeReady + totalIssuanceReady && reservesReady && feeReady } } @@ -62,7 +60,7 @@ final class LiquidityPoolRemoveLiquidityPresenter { private let confirmViewModelFactory: LiquidityPoolSupplyConfirmViewModelFactory? private var removeInfo: RemoveLiquidityInfo? - private var reserves: PolkaswapPoolReservesInfo? + private var reserves: BigUInt? private var swapFromChainAsset: ChainAsset? private var swapToChainAsset: ChainAsset? private var prices: [PriceData]? @@ -124,6 +122,12 @@ final class LiquidityPoolRemoveLiquidityPresenter { self.dataValidatingFactory = dataValidatingFactory self.confirmViewModelFactory = confirmViewModelFactory self.removeInfo = removeInfo + dexId = liquidityPair.dexId + + if let removeInfo = removeInfo, let utilityAsset = chain.utilityAssets().first { + totalIssuance = removeInfo.totalIssuances.toSubstrateAmount(precision: Int16(utilityAsset.precision)) + reserves = removeInfo.baseAssetReserves.toSubstrateAmount(precision: Int16(utilityAsset.precision)) + } self.localizationManager = localizationManager } @@ -137,7 +141,7 @@ final class LiquidityPoolRemoveLiquidityPresenter { let targetAsset = chain.assets.first(where: { $0.currencyId == liquidityPair.targetAssetId }), let totalIssuance = totalIssuance, let reserves = reserves, - let baseAssetReserves = Decimal.fromSubstrateAmount(reserves.reserves.reserves, precision: Int16(baseAsset.precision)), + let baseAssetReserves = Decimal.fromSubstrateAmount(reserves, precision: Int16(baseAsset.precision)), let totalIssuanceDecimal = Decimal.fromSubstrateAmount(totalIssuance, precision: Int16(baseAsset.precision)) else { return nil @@ -378,8 +382,8 @@ extension LiquidityPoolRemoveLiquidityPresenter: LiquidityPoolRemoveLiquidityCon func didTapFeeInfo() { var infoText: String var infoTitle: String - infoTitle = "Network fee" - infoText = "Network fee is used to ensure SORA system’s growth and stable performance." + infoTitle = R.string.localizable.lpNetworkFeeAlertTitle(preferredLanguages: selectedLocale.rLanguages) + infoText = R.string.localizable.lpNetworkFeeAlertText(preferredLanguages: selectedLocale.rLanguages) let view = setupView ?? confirmView router.presentInfo( @@ -411,6 +415,8 @@ extension LiquidityPoolRemoveLiquidityPresenter: LiquidityPoolRemoveLiquidityVie func didLoad(view: LiquidityPoolRemoveLiquidityViewInput) { setupView = view interactor.setup(with: self) + + refreshFee() } func didTapBackButton() { @@ -424,8 +430,8 @@ extension LiquidityPoolRemoveLiquidityPresenter: LiquidityPoolRemoveLiquidityVie func didTapApyInfo() { var infoText: String var infoTitle: String - infoTitle = "Strategic Bonus APY" - infoText = "Farming reward for liquidity provision" + infoTitle = R.string.localizable.lpApyAlertTitle(preferredLanguages: selectedLocale.rLanguages) + infoText = R.string.localizable.lpApyAlertText(preferredLanguages: selectedLocale.rLanguages) router.presentInfo( message: infoText, title: infoTitle, @@ -590,17 +596,6 @@ extension LiquidityPoolRemoveLiquidityPresenter: LiquidityPoolRemoveLiquidityInt logger.customError(error) } - func didReceiveDexId(_ dexId: String) { - self.dexId = dexId - refreshFee() - loadingCollector.dexIdReady = true - checkLoadingState() - } - - func didReceiveDexIdError(_ error: Error) { - logger.customError(error) - } - func didReceiveUserPool(pool: AccountPool?) { accountPoolInfo = pool @@ -690,7 +685,11 @@ extension LiquidityPoolRemoveLiquidityPresenter: LiquidityPoolRemoveLiquidityInt } func didReceivePoolReserves(reserves: PolkaswapPoolReservesInfo?) { - self.reserves = reserves + guard let reserves else { + return + } + + self.reserves = reserves.reserves.reserves refreshFee() loadingCollector.reservesReady = true checkLoadingState() diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityViewLayout.swift index 200adfd303..68405b7b8d 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityViewLayout.swift @@ -245,9 +245,9 @@ final class LiquidityPoolRemoveLiquidityViewLayout: UIView { } private func applyLocalization() { - warningView.titleLabel.text = "NOTE" - warningView.textLabel.text = "Removing pool tokens converts your position back into underlying tokens at the current rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive." - titleLabel.text = "Remove Liquidity" + warningView.titleLabel.text = R.string.localizable.lpPoolRemoveWarningTitle(preferredLanguages: locale.rLanguages) + warningView.textLabel.text = R.string.localizable.lpPoolRemoveWarningText(preferredLanguages: locale.rLanguages) + titleLabel.text = R.string.localizable.lpRemoveLiquidityScreenTitle(preferredLanguages: locale.rLanguages) swapFromInputView.locale = locale swapToInputView.locale = locale diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmViewLayout.swift index 9263615326..3aab259394 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmViewLayout.swift @@ -105,7 +105,7 @@ final class LiquidityPoolRemoveLiquidityConfirmViewLayout: UIView { // MARK: - Private methods private func applyLocalization() { - titleLabel.text = "Remove Liquidity" + titleLabel.text = R.string.localizable.lpRemoveLiquidityScreenTitle(preferredLanguages: locale.rLanguages) confirmButton.imageWithTitleView?.title = R.string.localizable .commonConfirm(preferredLanguages: locale.rLanguages) } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyAssembly.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyAssembly.swift index a24fb4c07c..a2b2686fe0 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyAssembly.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyAssembly.swift @@ -6,7 +6,7 @@ import SSFModels import SoraKeystore final class LiquidityPoolSupplyAssembly { - static func configureModule(chain: ChainModel, wallet: MetaAccountModel, liquidityPair: LiquidityPair) -> LiquidityPoolSupplyModuleCreationResult? { + static func configureModule(chain: ChainModel, wallet: MetaAccountModel, liquidityPair: LiquidityPair, availablePairs: [LiquidityPair]?) -> LiquidityPoolSupplyModuleCreationResult? { guard let response = wallet.fetch(for: chain.accountRequest()) else { return nil } @@ -50,7 +50,8 @@ final class LiquidityPoolSupplyAssembly { logger: Logger.shared, wallet: wallet, dataValidatingFactory: dataValidatingFactory, - viewModelFactory: LiquidityPoolSupplyViewModelFactoryDefault() + viewModelFactory: LiquidityPoolSupplyViewModelFactoryDefault(), + availablePairs: availablePairs ) let view = LiquidityPoolSupplyViewController( diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyInteractor.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyInteractor.swift index e06b0c7eff..0dc5f98390 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyInteractor.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyInteractor.swift @@ -5,14 +5,14 @@ import SSFModels import BigInt protocol LiquidityPoolSupplyInteractorOutput: AnyObject { - func didReceiveDexId(_ dexId: String) - func didReceiveDexIdError(_ error: Error) func didReceiveFee(_ fee: BigUInt) func didReceiveFeeError(_ error: Error) func didReceivePricesData(result: Result<[PriceData], Error>) func didReceiveAccountInfo(result: Result, for chainAsset: ChainAsset) func didReceivePoolAPY(apyInfo: PoolApyInfo?) func didReceivePoolApyError(error: Error) + func didReceiveLiquidityPairs(pairs: [LiquidityPair]?) + func didReceiveLiquidityPairsError(error: Error) } final class LiquidityPoolSupplyInteractor { @@ -44,17 +44,6 @@ final class LiquidityPoolSupplyInteractor { self.accountInfoSubscriptionAdapter = accountInfoSubscriptionAdapter } - private func fetchDexId() { - Task { - do { - let dexId = try await lpDataService.fetchDexId(baseAssetId: liquidityPair.baseAssetId) - output?.didReceiveDexId(dexId) - } catch { - output?.didReceiveDexIdError(error) - } - } - } - private func subscribeToPrices() { let chainAssets = chain.chainAssets pricesProvider = priceLocalSubscriber.subscribeToPrices(for: chainAssets, listener: self) @@ -81,7 +70,6 @@ final class LiquidityPoolSupplyInteractor { extension LiquidityPoolSupplyInteractor: LiquidityPoolSupplyInteractorInput { func setup(with output: LiquidityPoolSupplyInteractorOutput) { self.output = output - fetchDexId() subscribeToPrices() subscribeToAccountInfo() fetchApy() @@ -98,6 +86,24 @@ extension LiquidityPoolSupplyInteractor: LiquidityPoolSupplyInteractorInput { } } + func fetchPools() { + Task { + do { + let availablePoolsStream = try await lpDataService.subscribeAvailablePools() + + for try await availablePools in availablePoolsStream { + await MainActor.run { + output?.didReceiveLiquidityPairs(pairs: availablePools.value) + } + } + } catch { + await MainActor.run { + output?.didReceiveLiquidityPairsError(error: error) + } + } + } + } + func fetchApy() { guard let reservesId = liquidityPair.reservesId else { return diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift index ac16ff3f0a..5274af5ec2 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift @@ -6,16 +6,14 @@ import BigInt import SSFPolkaswap struct SupplyLiquidityLoadingCollector { - var dexIdReady: Bool var feeReady: Bool init() { - dexIdReady = false feeReady = false } var isReady: Bool { - dexIdReady && feeReady + feeReady } } @@ -34,6 +32,7 @@ protocol LiquidityPoolSupplyViewInput: ControllerBackedProtocol { protocol LiquidityPoolSupplyInteractorInput: AnyObject { func setup(with output: LiquidityPoolSupplyInteractorOutput) func estimateFee(supplyLiquidityInfo: SupplyLiquidityInfo) + func fetchPools() } final class LiquidityPoolSupplyPresenter { @@ -61,6 +60,7 @@ final class LiquidityPoolSupplyPresenter { private var swapFromChainAsset: ChainAsset? private var swapToChainAsset: ChainAsset? private var prices: [PriceData]? + private var pairs: [LiquidityPair]? private var apyInfo: PoolApyInfo? private var slippadgeTolerance: Float = Constants.slippadgeTolerance @@ -109,7 +109,8 @@ final class LiquidityPoolSupplyPresenter { logger: LoggerProtocol, wallet: MetaAccountModel, dataValidatingFactory: SendDataValidatingFactory, - viewModelFactory: LiquidityPoolSupplyViewModelFactory + viewModelFactory: LiquidityPoolSupplyViewModelFactory, + availablePairs: [LiquidityPair]? ) { self.interactor = interactor self.router = router @@ -119,6 +120,8 @@ final class LiquidityPoolSupplyPresenter { self.wallet = wallet self.dataValidatingFactory = dataValidatingFactory self.viewModelFactory = viewModelFactory + pairs = availablePairs + dexId = liquidityPair.dexId self.localizationManager = localizationManager } @@ -156,7 +159,8 @@ final class LiquidityPoolSupplyPresenter { targetAsset: targetAssetInfo, baseAssetAmount: baseAssetAmount, targetAssetAmount: targetAssetAmount, - slippage: Decimal(floatLiteral: Double(slippadgeTolerance)) + slippage: Decimal(floatLiteral: Double(slippadgeTolerance)), + availablePairs: pairs ) interactor.estimateFee(supplyLiquidityInfo: supplyLiquidityInfo) @@ -288,6 +292,10 @@ final class LiquidityPoolSupplyPresenter { swapFromChainAsset = chain.chainAssets.first(where: { $0.asset.currencyId == liquidityPair.baseAssetId }) swapToChainAsset = chain.chainAssets.first(where: { $0.asset.currencyId == liquidityPair.targetAssetId }) + if pairs == nil { + interactor.fetchPools() + } + DispatchQueue.main.async { self.view?.didReceiveNetworkFee(fee: nil) self.provideViewModel() @@ -309,8 +317,8 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { func didTapApyInfo() { var infoText: String var infoTitle: String - infoTitle = "Strategic bonus APY" - infoText = "APY is a figure that represents the actual amount of interest earned on investments in Liquidity pool." + infoTitle = R.string.localizable.lpApyAlertTitle(preferredLanguages: selectedLocale.rLanguages) + infoText = R.string.localizable.lpApyAlertText(preferredLanguages: selectedLocale.rLanguages) router.presentInfo( message: infoText, title: infoTitle, @@ -321,8 +329,8 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { func didTapFeeInfo() { var infoText: String var infoTitle: String - infoTitle = "Network fee" - infoText = "Network fee is used to ensure SORA system’s growth and stable performance." + infoTitle = R.string.localizable.lpNetworkFeeAlertTitle(preferredLanguages: selectedLocale.rLanguages) + infoText = R.string.localizable.lpNetworkFeeAlertText(preferredLanguages: selectedLocale.rLanguages) router.presentInfo( message: infoText, title: infoTitle, @@ -364,7 +372,8 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { let inputData = LiquidityPoolSupplyConfirmInputData( baseAssetAmount: self.baseAssetResultAmount, targetAssetAmount: self.targetAssetResultAmount, - slippageTolerance: Decimal(floatLiteral: Double(self.slippadgeTolerance)) + slippageTolerance: Decimal(floatLiteral: Double(self.slippadgeTolerance)), + availablePools: pairs ) self.router.showConfirmation( @@ -469,24 +478,20 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { // MARK: - LiquidityPoolSupplyInteractorOutput extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyInteractorOutput { - func didReceivePoolAPY(apyInfo: SSFPolkaswap.PoolApyInfo?) { - self.apyInfo = apyInfo - provideViewModel() + func didReceiveLiquidityPairs(pairs: [LiquidityPair]?) { + self.pairs = pairs } - func didReceivePoolApyError(error: Error) { + func didReceiveLiquidityPairsError(error: Error) { logger.customError(error) } - func didReceiveDexId(_ dexId: String) { - self.dexId = dexId - refreshFee() - - loadingCollector.dexIdReady = true - checkLoadingState() + func didReceivePoolAPY(apyInfo: SSFPolkaswap.PoolApyInfo?) { + self.apyInfo = apyInfo + provideViewModel() } - func didReceiveDexIdError(_ error: Error) { + func didReceivePoolApyError(error: Error) { logger.customError(error) } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift index 2ffcee6435..634d71f07f 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewController.swift @@ -192,6 +192,7 @@ extension LiquidityPoolSupplyViewController: LiquidityPoolSupplyViewInput { func setButtonLoadingState(isLoading: Bool) { rootView.previewButton.set(loading: isLoading) + updatePreviewButton() } func didUpdating() { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift index fc00eafa46..05cbbbd5c3 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyViewLayout.swift @@ -266,7 +266,7 @@ final class LiquidityPoolSupplyViewLayout: UIView { } private func applyLocalization() { - titleLabel.text = "Supply Liquidity" + titleLabel.text = R.string.localizable.lpSupplyLiquidityScreenTitle(preferredLanguages: locale.rLanguages) swapFromInputView.locale = locale swapToInputView.locale = locale @@ -289,8 +289,8 @@ final class LiquidityPoolSupplyViewLayout: UIView { .commonPreview(preferredLanguages: locale.rLanguages) slippageView.titleLabel.text = R.string.localizable.polkaswapSettingsSlippageTitle(preferredLanguages: locale.rLanguages) - apyView.titleLabel.text = "Strategic Bonus APY" - rewardTokenView.titleLabel.text = "Rewards Payout In" + apyView.titleLabel.text = R.string.localizable.lpApyTitle(preferredLanguages: locale.rLanguages) + rewardTokenView.titleLabel.text = R.string.localizable.lpRewardTokenTitle(preferredLanguages: locale.rLanguages) } private func setInfoImage(for label: UILabel, text: String) { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmAssembly.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmAssembly.swift index 937f66a9d6..c6bc18b9af 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmAssembly.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmAssembly.swift @@ -9,6 +9,7 @@ struct LiquidityPoolSupplyConfirmInputData { let baseAssetAmount: Decimal let targetAssetAmount: Decimal let slippageTolerance: Decimal + let availablePools: [LiquidityPair]? } enum LiquidityPoolSupplyConfirmAssembly { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmInteractor.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmInteractor.swift index b5184e8362..beff029912 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmInteractor.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmInteractor.swift @@ -5,8 +5,6 @@ import SSFPools import BigInt protocol LiquidityPoolSupplyConfirmInteractorOutput: AnyObject { - func didReceiveDexId(_ dexId: String) - func didReceiveDexIdError(_ error: Error) func didReceiveFee(_ fee: BigUInt) func didReceiveFeeError(_ error: Error) func didReceivePricesData(result: Result<[PriceData], Error>) @@ -46,17 +44,6 @@ final class LiquidityPoolSupplyConfirmInteractor { self.accountInfoSubscriptionAdapter = accountInfoSubscriptionAdapter } - private func fetchDexId() { - Task { - do { - let dexId = try await lpDataService.fetchDexId(baseAssetId: liquidityPair.baseAssetId) - output?.didReceiveDexId(dexId) - } catch { - output?.didReceiveDexIdError(error) - } - } - } - private func subscribeToPrices() { let chainAssets = chain.chainAssets pricesProvider = priceLocalSubscriber.subscribeToPrices(for: chainAssets, listener: self) @@ -83,7 +70,6 @@ final class LiquidityPoolSupplyConfirmInteractor { extension LiquidityPoolSupplyConfirmInteractor: LiquidityPoolSupplyConfirmInteractorInput { func setup(with output: LiquidityPoolSupplyConfirmInteractorOutput) { self.output = output - fetchDexId() fetchApy() subscribeToPrices() subscribeToAccountInfo() diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmPresenter.swift index 74cffd38e2..a1310746af 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmPresenter.swift @@ -6,16 +6,14 @@ import SSFModels import BigInt struct SupplyLiquidityConfirmLoadingCollector { - var dexIdReady: Bool var feeReady: Bool init() { - dexIdReady = false feeReady = false } var isReady: Bool { - dexIdReady && feeReady + feeReady } } @@ -57,6 +55,8 @@ final class LiquidityPoolSupplyConfirmPresenter { (xorBalance ?? 0) - (networkFee ?? 0) } + private var availablePairs: [LiquidityPair]? + private var swapFromBalance: Decimal? private var swapToBalance: Decimal? @@ -87,6 +87,8 @@ final class LiquidityPoolSupplyConfirmPresenter { self.inputData = inputData self.wallet = wallet self.viewModelFactory = viewModelFactory + dexId = liquidityPair.dexId + availablePairs = inputData.availablePools self.localizationManager = localizationManager } @@ -111,7 +113,8 @@ final class LiquidityPoolSupplyConfirmPresenter { targetAsset: targetAssetInfo, baseAssetAmount: inputData.baseAssetAmount, targetAssetAmount: inputData.targetAssetAmount, - slippage: inputData.slippageTolerance + slippage: inputData.slippageTolerance, + availablePairs: availablePairs ) interactor.estimateFee(supplyLiquidityInfo: supplyLiquidityInfo) @@ -235,8 +238,8 @@ extension LiquidityPoolSupplyConfirmPresenter: LiquidityPoolSupplyConfirmViewOut func didTapApyInfo() { var infoText: String var infoTitle: String - infoTitle = "Strategic bonus APY" - infoText = "APY is a figure that represents the actual amount of interest earned on investments in Liquidity pool." + infoTitle = R.string.localizable.lpApyAlertTitle(preferredLanguages: selectedLocale.rLanguages) + infoText = R.string.localizable.lpApyAlertText(preferredLanguages: selectedLocale.rLanguages) router.presentInfo( message: infoText, title: infoTitle, @@ -247,8 +250,8 @@ extension LiquidityPoolSupplyConfirmPresenter: LiquidityPoolSupplyConfirmViewOut func didTapFeeInfo() { var infoText: String var infoTitle: String - infoTitle = "Network fee" - infoText = "Network fee is used to ensure SORA system’s growth and stable performance." + infoTitle = R.string.localizable.lpNetworkFeeAlertTitle(preferredLanguages: selectedLocale.rLanguages) + infoText = R.string.localizable.lpNetworkFeeAlertText(preferredLanguages: selectedLocale.rLanguages) router.presentInfo( message: infoText, title: infoTitle, @@ -274,7 +277,8 @@ extension LiquidityPoolSupplyConfirmPresenter: LiquidityPoolSupplyConfirmViewOut targetAsset: targetAssetInfo, baseAssetAmount: inputData.baseAssetAmount, targetAssetAmount: inputData.targetAssetAmount, - slippage: inputData.slippageTolerance + slippage: inputData.slippageTolerance, + availablePairs: availablePairs ) interactor.submit(supplyLiquidityInfo: supplyLiquidityInfo) @@ -312,18 +316,6 @@ extension LiquidityPoolSupplyConfirmPresenter: LiquidityPoolSupplyConfirmInterac logger.customError(error) } - func didReceiveDexId(_ dexId: String) { - self.dexId = dexId - refreshFee() - - loadingCollector.dexIdReady = true - checkLoadingState() - } - - func didReceiveDexIdError(_ error: Error) { - logger.customError(error) - } - func didReceiveFee(_ fee: BigUInt) { guard let utilityAsset = chain.utilityAssets().first else { return diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewLayout.swift index 36b793e40e..24b4d3f127 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmViewLayout.swift @@ -147,12 +147,12 @@ final class LiquidityPoolSupplyConfirmViewLayout: UIView { // MARK: - Private methods private func applyLocalization() { - titleLabel.text = "Confirm Liquidity" + titleLabel.text = R.string.localizable.lpConfirmLiquidityScreenTitle(preferredLanguages: locale.rLanguages) confirmButton.imageWithTitleView?.title = R.string.localizable .commonConfirm(preferredLanguages: locale.rLanguages) - swapStubTitle.text = "Output is estimated. If the price changes more than 0.5% your transaction will revert." - slippageView.titleLabel.text = "Slippage" - rewardTokenView.titleLabel.text = "Rewards Payout In" + swapStubTitle.text = R.string.localizable.lpConfirmLiquidityWarningText(preferredLanguages: locale.rLanguages) + slippageView.titleLabel.text = R.string.localizable.lpSlippageTitle(preferredLanguages: locale.rLanguages) + rewardTokenView.titleLabel.text = R.string.localizable.lpRewardTokenTitle(preferredLanguages: locale.rLanguages) } private func setupLayout() { @@ -234,7 +234,7 @@ final class LiquidityPoolSupplyConfirmViewLayout: UIView { let texts = [ R.string.localizable .polkaswapNetworkFee(preferredLanguages: locale.rLanguages), - "Strategic Bonus APY" + R.string.localizable.lpApyTitle(preferredLanguages: locale.rLanguages) ] [ diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListInteractor.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListInteractor.swift index cd19975056..048536a0a3 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListInteractor.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListInteractor.swift @@ -4,7 +4,7 @@ import SSFPolkaswap import SSFModels import SSFStorageQueryKit -protocol AvailableLiquidityPoolsListInteractorOutput { +protocol AvailableLiquidityPoolsListInteractorOutput: AnyObject { func didReceiveLiquidityPairs(pairs: [LiquidityPair]?) func didReceivePoolsReserves(reserves: CachedStorageResponse<[PolkaswapPoolReservesInfo]>) func didReceivePoolsAPY(apy: [PoolApyInfo]?) @@ -17,13 +17,17 @@ protocol AvailableLiquidityPoolsListInteractorOutput { final class AvailableLiquidityPoolsListInteractor { private let liquidityPoolService: PolkaswapLiquidityPoolService - private var output: AvailableLiquidityPoolsListInteractorOutput? + private weak var output: AvailableLiquidityPoolsListInteractorOutput? private let priceLocalSubscriber: PriceLocalStorageSubscriber private let chain: ChainModel private var priceProvider: AnySingleValueProvider<[PriceData]>? private var receivedPoolIds: [String] = [] + private var reservesTask: Task? + private var poolsTask: Task? + private var apyTask: Task? + init( liquidityPoolService: PolkaswapLiquidityPoolService, priceLocalSubscriber: PriceLocalStorageSubscriber, @@ -35,7 +39,7 @@ final class AvailableLiquidityPoolsListInteractor { } private func fetchReserves(pools: [LiquidityPair]) { - Task { + reservesTask = Task { do { let reservesStream = try await liquidityPoolService.subscribePoolsReserves(pools: pools) @@ -66,14 +70,21 @@ extension AvailableLiquidityPoolsListInteractor: AvailableLiquidityPoolsListInte subscribeForPrices() } + func cancelTasks() { + [poolsTask, reservesTask, apyTask].compactMap { $0 }.forEach { $0.cancel() } + } + func fetchPools() { - Task { + poolsTask = Task { [weak self] in + guard let self else { + return + } do { let availablePoolsStream = try await liquidityPoolService.subscribeAvailablePools() for try await availablePools in availablePoolsStream { await MainActor.run { - output?.didReceiveLiquidityPairs(pairs: availablePools.value) + self.output?.didReceiveLiquidityPairs(pairs: availablePools.value) } if let pools = availablePools.value { @@ -83,7 +94,7 @@ extension AvailableLiquidityPoolsListInteractor: AvailableLiquidityPoolsListInte } } catch { await MainActor.run { - output?.didReceiveLiquidityPairsError(error: error) + self.output?.didReceiveLiquidityPairsError(error: error) } } } @@ -113,9 +124,9 @@ extension AvailableLiquidityPoolsListInteractor: AvailableLiquidityPoolsListInte return } - Task { - let apyStream = try await liquidityPoolService.subscribePoolsAPY(poolIds: poolIds) + apyTask = Task { do { + let apyStream = try await liquidityPoolService.subscribePoolsAPY(poolIds: poolIds) for try await apy in apyStream { if apy.first?.type == .remote { receivedPoolIds.append(contentsOf: apy.compactMap { $0.value?.poolId }) diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListPresenter.swift index cac882f2b1..764e181c62 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListPresenter.swift @@ -9,6 +9,7 @@ protocol AvailableLiquidityPoolsListInteractorInput { func setup(with output: AvailableLiquidityPoolsListInteractorOutput) func fetchPools() + func cancelTasks() } final class AvailableLiquidityPoolsListPresenter { @@ -52,6 +53,10 @@ final class AvailableLiquidityPoolsListPresenter { } private func provideViewModel() { + guard let pairs, pairs.isNotEmpty else { + return + } + let viewModel = viewModelFactory.buildViewModel( pairs: pairs, reserves: reserves, @@ -76,13 +81,16 @@ extension AvailableLiquidityPoolsListPresenter: LiquidityPoolsListViewOutput { } func didTapOn(viewModel: LiquidityPoolListCellModel) { - let liquidityPair = viewModel.liquidityPair + guard let liquidityPair = viewModel.liquidityPair else { + return + } + let reserves = reserves?.value?.first(where: { $0.poolId == liquidityPair.pairId }) let reservesAddress = liquidityPair.reservesId.map { try? AddressFactory.address(for: Data(hex: $0), chain: chain) } let apyInfo = apy?.first(where: { $0.poolId == reservesAddress }) - let assetIdPair = AssetIdPair(baseAssetIdCode: viewModel.liquidityPair.baseAssetId, targetAssetIdCode: viewModel.liquidityPair.targetAssetId) - let input = LiquidityPoolDetailsInput.availablePool(liquidityPair: liquidityPair, reserves: reserves, apyInfo: apyInfo) + let assetIdPair = AssetIdPair(baseAssetIdCode: liquidityPair.baseAssetId, targetAssetIdCode: liquidityPair.targetAssetId) + let input = LiquidityPoolDetailsInput.availablePool(liquidityPair: liquidityPair, reserves: reserves, apyInfo: apyInfo, availablePairs: pairs) router.showPoolDetails(assetIdPair: assetIdPair, chain: chain, wallet: wallet, input: input, from: view) } @@ -92,6 +100,7 @@ extension AvailableLiquidityPoolsListPresenter: LiquidityPoolsListViewOutput { } func didTapBackButton() { + interactor.cancelTasks() router.dismiss(view: view) } @@ -99,6 +108,11 @@ extension AvailableLiquidityPoolsListPresenter: LiquidityPoolsListViewOutput { searchText = text provideViewModel() } + + func didAppearView() { + let viewModel = viewModelFactory.buildLoadingViewModel(type: type) + view?.didReceive(viewModel: viewModel) + } } extension AvailableLiquidityPoolsListPresenter: AvailableLiquidityPoolsListInteractorOutput { @@ -140,7 +154,11 @@ extension AvailableLiquidityPoolsListPresenter: AvailableLiquidityPoolsListInter } } -extension AvailableLiquidityPoolsListPresenter: LiquidityPoolsListModuleInput {} +extension AvailableLiquidityPoolsListPresenter: LiquidityPoolsListModuleInput { + func resetTasks() { + interactor.cancelTasks() + } +} extension AvailableLiquidityPoolsListPresenter: Localizable { func applyLocalization() {} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListViewModelFactory.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListViewModelFactory.swift index e4b760a7b6..73af29cb1f 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListViewModelFactory.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListViewModelFactory.swift @@ -18,6 +18,8 @@ protocol AvailableLiquidityPoolsListViewModelFactory { type: LiquidityPoolListType, searchText: String? ) -> LiquidityPoolListViewModel + + func buildLoadingViewModel(type: LiquidityPoolListType) -> LiquidityPoolListViewModel } final class AvailableLiquidityPoolsListViewModelFactoryDefault: AvailableLiquidityPoolsListViewModelFactory { @@ -29,6 +31,26 @@ final class AvailableLiquidityPoolsListViewModelFactoryDefault: AvailableLiquidi self.assetBalanceFormatterFactory = assetBalanceFormatterFactory } + private func buildLoadingCellViewModel() -> LiquidityPoolListCellModel { + LiquidityPoolListCellModel(tokenPairIconsVieWModel: TokenPairsIconViewModel(firstTokenIconViewModel: nil, secondTokenIconViewModel: nil), tokenPairNameLabelText: nil, rewardTokenNameLabelText: nil, apyLabelText: nil, stakingStatusLabelText: nil, reservesLabelText: nil, sortValue: 0, liquidityPair: nil) + } + + func buildLoadingViewModel(type: LiquidityPoolListType) -> LiquidityPoolListViewModel { + var poolViewModels: [LiquidityPoolListCellModel] = [] + for _ in 0 ... 19 { + poolViewModels.append(buildLoadingCellViewModel()) + } + + return LiquidityPoolListViewModel( + poolViewModels: poolViewModels, + titleLabelText: "Available pools", + moreButtonVisible: type == .embed, + backgroundVisible: type == .full, + refreshAvailable: type == .full, + isEmbed: type == .embed + ) + } + func buildViewModel( pairs: [LiquidityPair]?, reserves: CachedStorageResponse<[PolkaswapPoolReservesInfo]>?, @@ -58,7 +80,7 @@ final class AvailableLiquidityPoolsListViewModelFactoryDefault: AvailableLiquidi let tokenPairName = "\(baseAsset.symbol.uppercased())-\(targetAsset.symbol.uppercased())" let reservesAddress = pair.reservesId.map { try? AddressFactory.address(for: Data(hex: $0), chain: chain) } - let rewardTokenNameLabelText = "Earn \(rewardAsset.symbol.uppercased())" + let rewardTokenNameLabelText = R.string.localizable.lpRewardTokenText(rewardAsset.symbol.uppercased(), preferredLanguages: locale.rLanguages) let apyValue = apyInfos?.first(where: { $0.poolId == reservesAddress })?.apy let apyLabelText = apyValue.flatMap { NumberFormatter.percentAPY.stringFromDecimal($0) } @@ -75,14 +97,13 @@ final class AvailableLiquidityPoolsListViewModelFactoryDefault: AvailableLiquidi let reservesString = reservesValue.flatMap { fiatFormatter.stringFromDecimal($0) } let reservesLabelText: String? = reservesString.flatMap { "\($0) TVL" } - let reservesLabelValue: ShimmeredLabelState = .normal(reservesLabelText) return LiquidityPoolListCellModel( tokenPairIconsVieWModel: iconsViewModel, tokenPairNameLabelText: tokenPairName, rewardTokenNameLabelText: rewardTokenNameLabelText, apyLabelText: apyLabelText, stakingStatusLabelText: nil, - reservesLabelValue: reservesLabelValue, + reservesLabelText: reservesLabelText, sortValue: reservesValue.or(.zero), liquidityPair: pair ) @@ -91,7 +112,7 @@ final class AvailableLiquidityPoolsListViewModelFactoryDefault: AvailableLiquidi return true } - return $0.tokenPairNameLabelText.lowercased().contains(searchText.lowercased()) + return $0.tokenPairNameLabelText?.lowercased().contains(searchText.lowercased()) == true } return LiquidityPoolListViewModel( diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListInteractor.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListInteractor.swift index d226009ff9..2412c916f5 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListInteractor.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListInteractor.swift @@ -4,7 +4,7 @@ import SSFPolkaswap import SSFModels import SSFStorageQueryKit -protocol UserLiquidityPoolsListInteractorOutput { +protocol UserLiquidityPoolsListInteractorOutput: AnyObject { func didReceiveLiquidityPairs(pools: [LiquidityPair]?) func didReceivePoolsReserves(reserves: CachedStorageResponse<[PolkaswapPoolReservesInfo]>?) func didReceivePoolsAPY(apy: [PoolApyInfo]) @@ -19,7 +19,7 @@ protocol UserLiquidityPoolsListInteractorOutput { final class UserLiquidityPoolsListInteractor { private let liquidityPoolService: PolkaswapLiquidityPoolService - private var output: UserLiquidityPoolsListInteractorOutput? + private weak var output: UserLiquidityPoolsListInteractorOutput? private let priceLocalSubscriber: PriceLocalStorageSubscriber private let chain: ChainModel private let wallet: MetaAccountModel @@ -27,6 +27,9 @@ final class UserLiquidityPoolsListInteractor { private var receivedPoolIds: [String] = [] + private var poolsTask: Task? + private var apyTask: Task? + init( liquidityPoolService: PolkaswapLiquidityPoolService, priceLocalSubscriber: PriceLocalStorageSubscriber, @@ -39,24 +42,6 @@ final class UserLiquidityPoolsListInteractor { self.wallet = wallet } -// private func fetchReserves(pools: [AccountPool]) { -// Task { -// do { -// let reservesStream = try await liquidityPoolService.subscribePoolsReserves(pools: pools) -// -// for try await reserves in reservesStream { -// await MainActor.run { -// output?.didReceivePoolsReserves(reserves: reserves) -// } -// } -// } catch { -// await MainActor.run { -// output?.didReceivePoolsReservesError(error: error) -// } -// } -// } -// } - private func subscribeForPrices() { let chainAssets = chain.chainAssets priceProvider = priceLocalSubscriber.subscribeToPrices(for: chainAssets, listener: self) @@ -68,29 +53,11 @@ extension UserLiquidityPoolsListInteractor: UserLiquidityPoolsListInteractorInpu self.output = output fetchPools() - fetchUserPools() subscribeForPrices() } - func fetchUserPools() { - guard let accountId = wallet.fetch(for: chain.accountRequest())?.accountId else { - output?.didReceiveLiquidityPairsError(error: ChainAccountFetchingError.accountNotExists) - return - } - - Task { - do { - let accountPools = try await liquidityPoolService.fetchUserPools(accountId: accountId) - await MainActor.run { - output?.didReceiveUserPools(accountPools: accountPools) - } - - } catch { - await MainActor.run { - output?.didReceiveUserPoolsError(error: error) - } - } - } + func cancelTasks() { + [poolsTask, apyTask].compactMap { $0 }.forEach { $0.cancel() } } func fetchPools() { @@ -99,14 +66,13 @@ extension UserLiquidityPoolsListInteractor: UserLiquidityPoolsListInteractorInpu return } - Task { + poolsTask = Task { do { let userPoolsStream = try await liquidityPoolService.subscribeUserPools(accountId: accountId) for try await userPools in userPoolsStream { - print("received fetch pool response") await MainActor.run { - output?.didReceiveLiquidityPairs(pools: userPools.value) + output?.didReceiveUserPools(accountPools: userPools.value) } if let pools = userPools.value { @@ -119,7 +85,7 @@ extension UserLiquidityPoolsListInteractor: UserLiquidityPoolsListInteractorInpu } } - func fetchApy(pools: [LiquidityPair]) { + func fetchApy(pools: [AccountPool]) { let poolIds: [String] = pools.compactMap { guard let reservesId = $0.reservesId, @@ -135,9 +101,9 @@ extension UserLiquidityPoolsListInteractor: UserLiquidityPoolsListInteractorInpu return } - Task { - let apyStream = try await liquidityPoolService.subscribePoolsAPY(poolIds: poolIds) + apyTask = Task { do { + let apyStream = try await liquidityPoolService.subscribePoolsAPY(poolIds: poolIds) for try await apy in apyStream { if apy.first?.type == .remote { receivedPoolIds.append(contentsOf: apy.compactMap { $0.value?.poolId }) diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift index 6c23cfc10e..17d46f17ef 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift @@ -9,6 +9,7 @@ protocol UserLiquidityPoolsListInteractorInput { func setup(with output: UserLiquidityPoolsListInteractorOutput) func fetchPools() + func cancelTasks() } final class UserLiquidityPoolsListPresenter { @@ -55,7 +56,6 @@ final class UserLiquidityPoolsListPresenter { private func provideViewModel() { let viewModel = viewModelFactory.buildViewModel( accountPools: accountPools, - pools: pools, reserves: reserves, apyInfos: apy, chain: chain, @@ -79,14 +79,25 @@ extension UserLiquidityPoolsListPresenter: LiquidityPoolsListViewOutput { interactor.setup(with: self) } + func didAppearView() {} + func didTapOn(viewModel: LiquidityPoolListCellModel) { - let liquidityPair = viewModel.liquidityPair + guard let liquidityPair = viewModel.liquidityPair else { + return + } + let reserves = reserves?.value?.first(where: { $0.poolId == liquidityPair.pairId }) let reservesAddress = liquidityPair.reservesId.map { try? AddressFactory.address(for: Data(hex: $0), chain: chain) } let apyInfo = apy?.first(where: { $0.poolId == reservesAddress }) let accountPool = accountPools?.first(where: { $0.poolId == liquidityPair.pairId }) - let assetIdPair = AssetIdPair(baseAssetIdCode: viewModel.liquidityPair.baseAssetId, targetAssetIdCode: viewModel.liquidityPair.targetAssetId) - let input = LiquidityPoolDetailsInput.userPool(liquidityPair: liquidityPair, reserves: reserves, apyInfo: apyInfo, accountPool: accountPool) + let assetIdPair = AssetIdPair(baseAssetIdCode: liquidityPair.baseAssetId, targetAssetIdCode: liquidityPair.targetAssetId) + let input = LiquidityPoolDetailsInput.userPool( + liquidityPair: liquidityPair, + reserves: reserves, + apyInfo: apyInfo, + accountPool: accountPool, + availablePairs: accountPools?.compactMap { $0.liquidityPair } + ) router.showPoolDetails(assetIdPair: assetIdPair, chain: chain, wallet: wallet, input: input, from: view) } @@ -95,6 +106,7 @@ extension UserLiquidityPoolsListPresenter: LiquidityPoolsListViewOutput { } func didTapBackButton() { + interactor.cancelTasks() router.dismiss(view: view) } @@ -107,12 +119,13 @@ extension UserLiquidityPoolsListPresenter: LiquidityPoolsListViewOutput { extension UserLiquidityPoolsListPresenter: UserLiquidityPoolsListInteractorOutput { func didReceiveUserPools(accountPools: [AccountPool]?) { self.accountPools = accountPools + moduleOutput?.shouldShowUserPools(accountPools?.isNotEmpty == true) + provideViewModel() } func didReceiveLiquidityPairs(pools: [LiquidityPair]?) { self.pools = pools - moduleOutput?.shouldShowUserPools(pools?.isNotEmpty == true) provideViewModel() } @@ -153,7 +166,11 @@ extension UserLiquidityPoolsListPresenter: UserLiquidityPoolsListInteractorOutpu } } -extension UserLiquidityPoolsListPresenter: LiquidityPoolsListModuleInput {} +extension UserLiquidityPoolsListPresenter: LiquidityPoolsListModuleInput { + func resetTasks() { + interactor.cancelTasks() + } +} extension UserLiquidityPoolsListPresenter: Localizable { func applyLocalization() {} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListViewModelFactory.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListViewModelFactory.swift index 45ce5c357a..d323f10b98 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListViewModelFactory.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListViewModelFactory.swift @@ -9,7 +9,6 @@ import SSFStorageQueryKit protocol UserLiquidityPoolsListViewModelFactory { func buildViewModel( accountPools: [AccountPool]?, - pools: [LiquidityPair]?, reserves: CachedStorageResponse<[PolkaswapPoolReservesInfo]>?, apyInfos: [PoolApyInfo]?, chain: ChainModel, @@ -32,7 +31,6 @@ final class UserLiquidityPoolsListViewModelFactoryDefault: UserLiquidityPoolsLis func buildViewModel( accountPools: [AccountPool]?, - pools: [LiquidityPair]?, reserves: CachedStorageResponse<[PolkaswapPoolReservesInfo]>?, apyInfos: [PoolApyInfo]?, chain: ChainModel, @@ -42,8 +40,7 @@ final class UserLiquidityPoolsListViewModelFactoryDefault: UserLiquidityPoolsLis type: LiquidityPoolListType, searchText: String? ) -> LiquidityPoolListViewModel { - let poolViewModels: [LiquidityPoolListCellModel]? = pools?.sorted().compactMap { pair in - let accountPoolInfo = accountPools?.first(where: { $0.poolId == pair.identifier }) + let poolViewModels: [LiquidityPoolListCellModel]? = accountPools?.compactMap { pair in let baseAsset = chain.assets.first(where: { $0.currencyId == pair.baseAssetId }) let targetAsset = chain.assets.first(where: { $0.currencyId == pair.targetAssetId }) let rewardAsset = chain.assets.first(where: { $0.currencyId == pair.rewardAssetId }) @@ -61,16 +58,16 @@ final class UserLiquidityPoolsListViewModelFactoryDefault: UserLiquidityPoolsLis let tokenPairName = "\(baseAsset.symbol.uppercased())-\(targetAsset.symbol.uppercased())" let reservesAddress = pair.reservesId.map { try? AddressFactory.address(for: Data(hex: $0), chain: chain) } - let rewardTokenNameLabelText = "Earn \(rewardAsset.symbol.uppercased())" + let rewardTokenNameLabelText = R.string.localizable.lpRewardTokenText(rewardAsset.symbol.uppercased(), preferredLanguages: locale.rLanguages) let apyInfo = apyInfos?.first(where: { $0.poolId == reservesAddress }) let apyValue = apyInfo?.apy let apyLabelText = apyValue.flatMap { NumberFormatter.percentAPY.stringFromDecimal($0) } let baseAssetPrice = prices?.first(where: { $0.priceId == baseAsset.priceId }) let targetAssetPrice = prices?.first(where: { $0.priceId == targetAsset.priceId }) - let poolReservesInfo = reserves?.value?.first(where: { $0.poolId == pair.pairId }) + let poolReservesInfo = reserves?.value?.first(where: { $0.poolId == pair.poolId }) let reservesValue = modelFactory.buildReserves( - pool: pair, + accountPool: pair, chain: chain, reserves: poolReservesInfo, baseAssetPrice: baseAssetPrice, @@ -78,16 +75,15 @@ final class UserLiquidityPoolsListViewModelFactoryDefault: UserLiquidityPoolsLis ) let reservesString = reservesValue.flatMap { fiatFormatter.stringFromDecimal($0) } let reservesLabelText: String? = reservesString.flatMap { "\($0) TVL" } - let reservesLabelValue: ShimmeredLabelState = .normal(reservesLabelText) let baseAssetBalanceViewModelFactory = createBalanceViewModelFactory(for: ChainAsset(chain: chain, asset: baseAsset), wallet: wallet) let targetAssetBalanceViewModelFactory = createBalanceViewModelFactory(for: ChainAsset(chain: chain, asset: targetAsset), wallet: wallet) - let baseAssetViewModel = accountPoolInfo?.baseAssetPooled.flatMap { + let baseAssetViewModel = pair.baseAssetPooled.flatMap { baseAssetBalanceViewModelFactory.balanceFromPrice($0, priceData: baseAssetPrice, usageCase: .listCryptoWith(minimumFractionDigits: 1, maximumFractionDigits: 3)) } - let targetAssetViewModel = accountPoolInfo?.targetAssetPooled.flatMap { + let targetAssetViewModel = pair.targetAssetPooled.flatMap { targetAssetBalanceViewModelFactory.balanceFromPrice($0, priceData: targetAssetPrice, usageCase: .listCryptoWith(minimumFractionDigits: 1, maximumFractionDigits: 3)) } @@ -108,22 +104,22 @@ final class UserLiquidityPoolsListViewModelFactoryDefault: UserLiquidityPoolsLis rewardTokenNameLabelText: rewardTokenNameLabelText, apyLabelText: apyLabelText, stakingStatusLabelText: stakingStatusLabelText, - reservesLabelValue: reservesLabelValue, + reservesLabelText: reservesLabelText, sortValue: reservesValue.or(.zero), - liquidityPair: pair + liquidityPair: pair.liquidityPair ) }.sorted(by: { $0.sortValue > $1.sortValue }).filter { guard let searchText, searchText.isNotEmpty else { return true } - return $0.tokenPairNameLabelText.lowercased().contains(searchText.lowercased()) + return $0.tokenPairNameLabelText?.lowercased().contains(searchText.lowercased()) == true } return LiquidityPoolListViewModel( poolViewModels: poolViewModels, - titleLabelText: "User pools", - moreButtonVisible: type == .embed && (poolViewModels?.count ?? 0 < pools?.count ?? 0), + titleLabelText: R.string.localizable.lpUserPoolsTitle(preferredLanguages: locale.rLanguages), + moreButtonVisible: type == .embed && (poolViewModels?.count ?? 0 < accountPools?.count ?? 0), backgroundVisible: type == .full, refreshAvailable: type == .full, isEmbed: type == .embed diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListProtocols.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListProtocols.swift index 3a3255ea0b..17076eac54 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListProtocols.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListProtocols.swift @@ -14,6 +14,7 @@ protocol LiquidityPoolsListViewOutput: AnyObject { func didTapMoreButton() func didTapBackButton() func searchTextDidChanged(_ text: String?) + func didAppearView() } protocol LiquidityPoolsListInteractorInput: AnyObject { @@ -31,7 +32,9 @@ protocol LiquidityPoolsListRouterInput: AnyObject, AnyDismissable { ) } -protocol LiquidityPoolsListModuleInput: AnyObject {} +protocol LiquidityPoolsListModuleInput: AnyObject { + func resetTasks() +} protocol LiquidityPoolsListModuleOutput: AnyObject { func didTapMoreUserPools() diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift index 2bc510b657..d21b34b133 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift @@ -5,12 +5,15 @@ import SoraUI final class LiquidityPoolsListViewController: UIViewController, ViewHolder, HiddableBarWhenPushed { typealias RootViewType = LiquidityPoolsListViewLayout + var keyboardHandler: FearlessKeyboardHandler? // MARK: Private properties private var cellModels: [LiquidityPoolListCellModel]? private let output: LiquidityPoolsListViewOutput + private var viewLoadingFinished: Bool = false + // MARK: - Constructor init( @@ -59,6 +62,14 @@ final class LiquidityPoolsListViewController: UIViewController, ViewHolder, Hidd if keyboardHandler == nil { setupKeyboardHandler() } + + guard !viewLoadingFinished else { + return + } + + viewLoadingFinished = true + + output.didAppearView() } override func viewDidDisappear(_ animated: Bool) { @@ -119,6 +130,7 @@ extension LiquidityPoolsListViewController: UITableViewDataSource, UITableViewDe extension LiquidityPoolsListViewController: LiquidityPoolsListViewInput { func didReceive(viewModel: LiquidityPoolListViewModel) { + print("didreceive viewmodels: ", viewModel.poolViewModels) cellModels = viewModel.poolViewModels rootView.bind(viewModel: viewModel) diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift index f872c000cf..f089a1176d 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift @@ -27,6 +27,7 @@ final class LiquidityPoolsListViewLayout: UIView { button.setImage(R.image.iconChevronRight(), for: .normal) button.semanticContentAttribute = .forceRightToLeft button.backgroundColor = R.color.colorWhite8() + button.isHidden = true return button }() @@ -54,6 +55,7 @@ final class LiquidityPoolsListViewLayout: UIView { let searchTextField: SearchTextField = { let searchTextField = UIFactory.default.createSearchTextField() searchTextField.triangularedView?.strokeWidth = 0 + searchTextField.isHidden = true return searchTextField }() @@ -126,7 +128,7 @@ final class LiquidityPoolsListViewLayout: UIView { private func setupConstraints() { vStackView.snp.makeConstraints { make in - keyboardAdoptableConstraint = make.bottom.lessThanOrEqualToSuperview().constraint + make.bottom.lessThanOrEqualToSuperview() make.leading.trailing.top.equalToSuperview() } @@ -137,12 +139,12 @@ final class LiquidityPoolsListViewLayout: UIView { topBar.snp.makeConstraints { make in make.height.equalTo(42) - make.leading.trailing.equalToSuperview() } contentView.snp.makeConstraints { make in make.top.equalTo(vStackView.snp.bottom).offset(8) - make.leading.trailing.bottom.equalToSuperview() + make.leading.trailing.equalToSuperview() + keyboardAdoptableConstraint = make.bottom.equalToSuperview().constraint } tableView.snp.makeConstraints { make in diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/View/LiquidityPoolListCell.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/View/LiquidityPoolListCell.swift index 9108e6fd4a..6e0010d533 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/View/LiquidityPoolListCell.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/View/LiquidityPoolListCell.swift @@ -2,15 +2,15 @@ import UIKit class LiquidityPoolListCell: UITableViewCell { let tokenPairIconsView = TokenPairIconsView() - let tokenPairNameLabel: UILabel = { - let label = UILabel() + let tokenPairNameLabel: SkeletonLabel = { + let label = SkeletonLabel(skeletonSize: CGSize(width: 50, height: 12)) label.font = .capsTitle label.textColor = .white return label }() - let rewardTokenNameLabel: UILabel = { - let label = UILabel() + let rewardTokenNameLabel: SkeletonLabel = { + let label = SkeletonLabel(skeletonSize: CGSize(width: 50, height: 12)) label.font = .p2Paragraph label.textColor = R.color.colorWhite50() return label @@ -60,14 +60,15 @@ class LiquidityPoolListCell: UITableViewCell { func bind(viewModel: LiquidityPoolListCellModel) { tokenPairIconsView.bind(viewModel: viewModel.tokenPairIconsVieWModel) - tokenPairNameLabel.text = viewModel.tokenPairNameLabelText - rewardTokenNameLabel.text = viewModel.rewardTokenNameLabelText stakingStatusLabel.text = viewModel.stakingStatusLabelText + tokenPairNameLabel.updateTextWithLoading(viewModel.tokenPairNameLabelText) + rewardTokenNameLabel.updateTextWithLoading(viewModel.rewardTokenNameLabelText) apyLabel.updateTextWithLoading(viewModel.apyLabelText) - reservesLabel.apply(state: viewModel.reservesLabelValue) + reservesLabel.updateTextWithLoading(viewModel.reservesLabelText) stakingStatusLabel.isHidden = viewModel.stakingStatusLabelText == nil + reservesLabel.isHidden = viewModel.stakingStatusLabelText != nil } private func drawSubviews() { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListCellModel.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListCellModel.swift index b3f5d71adc..479e001ed7 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListCellModel.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/ViewModel/LiquidityPoolListCellModel.swift @@ -4,11 +4,11 @@ import SSFPolkaswap struct LiquidityPoolListCellModel { let tokenPairIconsVieWModel: TokenPairsIconViewModel - let tokenPairNameLabelText: String - let rewardTokenNameLabelText: String + let tokenPairNameLabelText: String? + let rewardTokenNameLabelText: String? let apyLabelText: String? let stakingStatusLabelText: String? - let reservesLabelValue: ShimmeredLabelState? + let reservesLabelText: String? let sortValue: Decimal - let liquidityPair: LiquidityPair + let liquidityPair: LiquidityPair? } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewAssembly.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewAssembly.swift index e90500b8f6..25ce71a399 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewAssembly.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewAssembly.swift @@ -29,6 +29,9 @@ final class LiquidityPoolsOverviewAssembly { return nil } + presenter.userPoolsInput = userPoolsModule.input + presenter.availablePoolsInput = availablePoolsModule.input + let view = LiquidityPoolsOverviewViewController( output: presenter, localizationManager: localizationManager, diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewPresenter.swift index 8c8e604050..520b86ef84 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewPresenter.swift @@ -11,6 +11,9 @@ final class LiquidityPoolsOverviewPresenter { private let chain: ChainModel private let wallet: MetaAccountModel + var availablePoolsInput: LiquidityPoolsListModuleInput? + var userPoolsInput: LiquidityPoolsListModuleInput? + // MARK: - Constructors init( @@ -41,6 +44,9 @@ extension LiquidityPoolsOverviewPresenter: LiquidityPoolsOverviewViewOutput { func backButtonClicked() { router.dismiss(view: view) + + availablePoolsInput?.resetTasks() + userPoolsInput?.resetTasks() } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewLayout.swift index e8c1b11717..5a3c0f0959 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewLayout.swift @@ -33,6 +33,7 @@ final class LiquidityPoolsOverviewViewLayout: UIView { let containerStackView: UIStackView = { let stackView = UIFactory.default.createVerticalStackView(spacing: 16) stackView.distribution = .fill + stackView.alignment = .center return stackView }() @@ -85,7 +86,6 @@ final class LiquidityPoolsOverviewViewLayout: UIView { userPoolsContainerView.snp.updateConstraints { make in make.height.equalTo(userSectionHeight) - make.leading.trailing.equalToSuperview().inset(16) } availablePoolsContainerView.snp.updateConstraints { make in diff --git a/fearless/en.lproj/Localizable.strings b/fearless/en.lproj/Localizable.strings index 5284e48d99..22c218e6d0 100644 --- a/fearless/en.lproj/Localizable.strings +++ b/fearless/en.lproj/Localizable.strings @@ -91,10 +91,14 @@ "application.status.view.copied.description" = "Wallet address copied successfully"; "application.status.view.copied.title" = "Address copied"; "application.status.view.hash.copied.description" = "Hash copied successfully"; +"application.status.view.hash.copied.description" = "Hash copied successfully"; +"application.status.view.hash.copied.title" = "Hash copied"; "application.status.view.hash.copied.title" = "Hash copied"; "application.status.view.offline.description" = "Please check your connection and try again"; "application.status.view.offline.title" = "It seems that you’re offline"; "application.status.view.reconnected.description" = "Сonnection successfully restored"; +"application.status.view.reconnected.description" = "Сonnection successfully restored"; +"application.status.view.reconnected.title" = "Reconnected"; "application.status.view.reconnected.title" = "Reconnected"; "applied.fearless.wallet.bonus" = "Fearless wallet bonus applied"; "apply.fearless.wallet.bonus" = "Tap to apply Fearless wallet bonus"; @@ -152,6 +156,7 @@ "backup.wallet.replace.several.alert" = "You currently have several chain accounts that has been added by replacing the main key pair, and it possesses a specific one. However, please note that our current wallet backup (export) flow does not support the storage of multiple key pairs. As a result, you can only save your main key pair. \n \n To ensure the safety of your replaced chain account, we recommend that you first back it up separately before proceeding with the current flow. Once you have successfully backed up your replaced chain account, you can proceed to the current flow without any concerns."; "backup.wallet.seed" = "Show Raw Seed"; "backup.wallet.title" = "Backup wallet"; +"balance.locks.blocked.row.title" = "Blocked"; "balance.locks.governance.row.title" = "Governance"; "balance.locks.liquidity.pools.row.title" = "Liquidity Pools"; "balance.locks.nomination.pools.row.title" = "Nomination Pools"; @@ -176,6 +181,7 @@ Euro cash"; "common.action.receive" = "Receive"; "common.action.send" = "Send"; "common.action.teleport" = "Teleport"; +"common.activation.required" = "Activation Required"; "common.add" = "Add"; "common.address" = "Address"; "common.advanced" = "Advanced"; @@ -280,6 +286,7 @@ Euro cash"; "common.search.start.title" = "Search results will appear here"; "common.secret.derivation.path" = "Secret derivation path"; "common.select" = "Select"; +"common.select" = "Select"; "common.select.all" = "Select all"; "common.select.asset" = "Select asset"; "common.select.network" = "Select Network"; @@ -395,6 +402,7 @@ Euro cash"; "error.invalid.address" = "Invalid address for selected chain"; "error.message.enter.the.name" = "Enter the name…"; "error.message.enter.the.url.address" = "Enter the URL address…"; +"error.scan.qr.disabled.asset" = "The asset you want to transfer is either unsupported or turned off. Go to Asset Management, activate the asset, and then scan the QR code again."; "error.unsupported.asset" = "You're trying to make a transfer of the asset which isn't currently supported on the App. Please choose another asset or request another QR code."; "ethereum.crypto.type" = "Ethereum keypair crypto type"; "ethereum.secret.derivation.path" = "Ethereum secret derivation path"; @@ -460,6 +468,26 @@ Seed: %@"; "label.testnet" = "Testnet"; "language.title" = "Language"; "learn.more.about.crowdloans" = "Learn more about Crowdloans"; +"lp.apy.alert.text" = "Farming reward for liquidity provision"; +"lp.apy.alert.title" = "Strategic Bonus APY"; +"lp.apy.title" = "Strategic Bonus APY"; +"lp.available.pools.title" = "Available pools"; +"lp.confirm.liquidity.screen.title" = "Confirm Liquidity"; +"lp.confirm.liquidity.warning.text" = "Output is estimated. If the price changes more than 0.5% your transaction will revert."; +"lp.network.fee.alert.text" = "Network fee is used to ensure SORA system’s growth and stable performance."; +"lp.network.fee.alert.title" = "Network fee"; +"lp.pool.details.title" = "Pool details"; +"lp.pool.remove.warning.text" = "Removing pool tokens converts your position back into underlying tokens at the current rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive."; +"lp.pool.remove.warning.title" = "NOTE"; +"lp.remove.button.title" = "Remove Liquidity"; +"lp.remove.liquidity.screen.title" = "Remove Liquidity"; +"lp.reward.token.text" = "Earn %@"; +"lp.reward.token.title" = "Rewards Payout In"; +"lp.slippage.title" = "Slippage"; +"lp.supply.button.title" = "Supply Liquidity"; +"lp.supply.liquidity.screen.title" = "Supply Liquidity"; +"lp.token.pooled.text" = "Your %@ Pooled"; +"lp.user.pools.title" = "User pools"; "manage.assets.account.missing.text" = "Add an account..."; "manage.assets.search.hint" = "Search by asset"; "members.common" = "Members"; @@ -486,6 +514,7 @@ This is your transaction hash:"; "network.info.address" = "Node address"; "network.info.name" = "Node name"; "network.info.title" = "Node Info"; +"network.issue.main" = "Connection Error: Unable to connect to the network. Please try again."; "network.issue.network.unavailible" = "Network is unavailable"; "network.issue.node.unavailable" = "Node is unavailable"; "network.issue.notofication" = "Notification"; @@ -507,6 +536,7 @@ This is your transaction hash:"; "nft.list.empty.message" = "There aren't any NFTs yet. Buy or mint NFTs to see them here."; "nft.owner.title" = "Owned"; "nft.share.address" = "My public address to receive: %s"; +"nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; "nft.stub.text" = "NFTs are coming soon"; "nft.stub.title" = "Sumimasen!"; "nft.tokenid.title" = "Token ID"; @@ -591,17 +621,20 @@ Terms and Conditions and Privacy Policy"; "polkaswap.disclaimer.title" = "Disclaimer"; "polkaswap.liqudity.fee.info" = "A portion of each trade (0.3%) goes to liquidity providers as a protocol incentive."; "polkaswap.liquidity.provider.fee" = "Liquidity Provider Fee"; +"polkaswap.liquidity.provider.fee" = "Liquidity Provider Fee "; "polkaswap.market.alert.choose.action" = "Choose asset"; "polkaswap.market.alert.message" = "You haven't completed the swap adjustments. One or both assets haven't been chosen, so the market selection isn't available."; "polkaswap.market.alert.title" = "Market selection isn't available"; "polkaswap.market.algorithm.title" = "Market algorithm"; "polkaswap.market.smart.description" = "SMART liquidity routing ensures the best price for any transaction by combining only the best price options from all available markets. When available, Token Bonding Curve (TBC) will be used for liquidity as long as the asset price is more affordable than from other sources, upon which the XYK pool is utilized."; "polkaswap.market.stub" = "Market:"; +"polkaswap.market.stub" = "Market"; "polkaswap.market.tbc.description" = "TBC — buying only from the Token Bonding Curve (Primary Market). There is a possibility that the price can become unfavorable compared to the XYK pool (Secondary Market), but the value received from the vested rewards might turn out to be much more favorable over time."; "polkaswap.market.xyk.description" = "XYK — buying only from the XYK pool (Secondary Market). Traditional XYK pool swap where anyone can buy or sell assets by shifting the market maker’s position on the x*y=k curve."; "polkaswap.max.received" = "Maximums sold "; "polkaswap.maximum.sold.info" = "Your transaction will revert if there is a large, unfavorable price movement before it is confirmed."; "polkaswap.min.received" = "Min received"; +"polkaswap.min.received" = "Min received "; "polkaswap.minimum.received.info" = "Your transaction will revert if there is a large, unfavorable price movement before it is confirmed."; "polkaswap.network.fee" = "Network fee "; "polkaswap.network.fee.info" = "Network fee is used to ensure SORA system's growth and stable performance."; @@ -685,7 +718,7 @@ Terms and Conditions and Privacy Policy"; "scam.description.donation.stub" = "This address has been flagged as suspicious. We strongly recommend that you don't send %s to this account."; "scam.description.exchange.stub" = "This address is marked as an exchange, be careful as the deposit and withdrawal addresses may different."; "scam.description.sanctions.stub" = "This address has been flagged due to an entity related to a country under sanctions. We strongly recommend that you don't send %s to this account."; -"scam.description.scam.stub" = "This address has been flagged due to evidence of a scam. We strongly recommend that you don't send %s to this account."; +"scam.description.scam.stub" = "This address has been flagged due to evidence of a scam. We strongly recommend that you don't send {asset} to this account."; "scam.name.stub" = "Name:"; "scam.reason.stub" = "Reason:"; "scam.warning.alert.subtitle" = "We strongly recommend that you don't send %s to this account."; @@ -836,8 +869,11 @@ to your account to avoid remaining stake."; "staking.pool.create.root" = "Root"; "staking.pool.create.stateToggler" = "State toggler"; "staking.pool.create.title" = "Create a pool"; +"staking.pool.create.title" = "Create a pool"; "staking.pool.info.title" = "Pool Info"; "staking.pool.management.select.validators.subtitle" = "To continue you should\nselect validators"; +"staking.pool.management.select.validators.subtitle" = "To continue you should\nselect validators"; +"staking.pool.management.select.validators.title" = "Select pool validators"; "staking.pool.management.select.validators.title" = "Select pool validators"; "staking.pool.rewards.delay.text" = "Stake at any time. You will start earning interest after %s"; "staking.pool.select.validators.manual" = "Select manually"; @@ -896,6 +932,7 @@ to your account to avoid remaining stake."; "staking.round.title" = "round #%@"; "staking.select.suggested" = "Select suggested"; "staking.select.validators.confirm.title" = "Selecting validators"; +"staking.select.validators.confirm.title" = "Selecting validators"; "staking.select.validators.custom.button.title" = "Select by yourself"; "staking.select.validators.custom.desc" = "You should trust your nominations to act competently and honestly, basing your decision purely on their current profitability could lead to reduced profits or even a loss of funds."; "staking.select.validators.custom.title" = "Stake with your validators"; @@ -1027,9 +1064,9 @@ Remember to make a backup of your key and keep it in a safe and private place (e "transaction.details.from" = "From"; "transaction.details.hash.title" = "Extrinsic Hash"; "transaction.details.view.etherscan" = "View in Etherscan"; +"transaction.details.view.oklink" = "View in OKX explorer"; "transaction.details.view.polkascan" = "View in Polkascan"; "transaction.details.view.reefscan" = "View in Reefscan"; -"transaction.details.view.oklink" = "View in OKX explorer"; "transaction.details.view.subscan" = "View in Subscan"; "transaction.list.header" = "All transaction"; "transaction.status.completed" = "Completed"; @@ -1045,6 +1082,8 @@ Remember to make a backup of your key and keep it in a safe and private place (e "username.setup.title" = "Create an account"; "username.setup.title.2.0" = "Create a new wallet"; "validator.info.comission.title" = "Comission"; +"validator.info.min.stake.alert.text" = "Minimum stake among active nominators is %s. To get rewards you have to stake more."; +"validator.info.min.stake.among.active.nominators.text" = "Minimum stake among active nominators"; "validators.list.empty.message" = "No validators found"; "verify.phone.number.title" = "Verify your phone number"; "vesting.claim.disclaimer.text" = "Due to the unique vesting schedules of each parachain, our app is unable to display the quantity of locked tokens eligible for claiming. Please be advised that initiating a claim may be impractical if the prospective amount is marginal and comparable to the transaction fee involved. For comprehensive insights into your locked balances, consult the Subscan blockexplorer. We urge you to assess the information carefully and proceed at your own discretion."; @@ -1054,6 +1093,7 @@ Remember to make a backup of your key and keep it in a safe and private place (e "view.wallet" = "View wallet"; "wallet.account.locks.democracy" = "Democracy"; "wallet.account.locks.vesting" = "Vesting"; +"wallet.all.assets.hidden" = "You have hidden all assets"; "wallet.asset.buy" = "Buy"; "wallet.asset.buy.with" = "Buy %s with"; "wallet.asset.receive" = "Receive"; @@ -1115,6 +1155,7 @@ belongs to the right network"; "wallet.send.balance.total.after.transfer" = "Total after transfer"; "wallet.send.confirm.title" = "Confirm transfer"; "wallet.send.dead.recipient.message" = "Your transfer will fail since the final amount on the destination account will be less than the minimal balance. Please try to increase the amount."; +"wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%1$s) on the destination account will be less than the minimal balance (%2$s). Please increase the amount by %3$s"; "wallet.send.dead.recipient.title" = "Amount is too low"; "wallet.send.eth.dead.recipient.message" = "Insufficient Ethereum balance in the recipient's account prevents the completion of ERC20 token transfer. Please ensure the receiver has enough Ethereum to proceed with the transfer."; "wallet.send.existential.warning" = "Your transfer will remove the account from blockstore since it will make the total balance lower than the minimal balance."; @@ -1135,9 +1176,11 @@ belongs to the right network"; "what.accounts.for.export" = "What accounts in the wallet do you want to export?"; "xcm.cross.chain.button.title" = "Cross Chain"; "xcm.cross.chain.invalid.address.message" = "According to the address provided you're trying to make a transfer on the wrong network."; +"xcm.cross.chain.invalid.address.message" = "According to the address provided you're trying to make a transfer on the wrong network."; "xcm.cross.chain.invalid.address.title" = "Is not network address"; "xcm.destination.network.fee.title" = "Destination Network Fee"; "xcm.destination.network.title" = "Destination network"; +"xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; "xcm.mywallets.button.title" = "My wallets"; "xcm.origin.network.fee.title" = "Origin Network Fee"; "xcm.origin.network.title" = "Origin network"; @@ -1146,130 +1189,4 @@ belongs to the right network"; "your.validators.stop.nominating.title" = "Stop nominating"; "your.validators.validator.total.stake" = "Total staked: %@"; "сurrencies.stub.text" = "Currencies"; -"cant.fetch.refresh" = "Can't fetch data, tap to refresh."; -"card.attention.text" = "Attention"; -"card.hub.manage.card" = "Manage SORA Card"; -"card.hub.manage.card.alert.message" = "To manage your SORA Card, please install the official SORA Card App. Tap OK to be directed to the App Store."; -"card.hub.manage.card.alert.title" = "Download SORA Card App"; -"card.hub.settings.logout.button" = "Log out"; -"card.hub.settings.logout.description" = "You are about to log out of SORA Card. You will still have access to the SORA Card standalone app, but the balance will no longer be available to you in the SORA Wallet."; -"card.hub.settings.logout.title" = "Log out of SORA Card"; -"card.hub.settings.title" = "Card settings"; -"card.hub.title" = "Card details"; -"card.hub.update.button" = "Update now"; -"card.hub.update.description" = "You are using an outdated version of the app, some part might not be working as intended."; -"card.hub.update.title" = "Update app to latest version"; -"card.issuance.screen.free.card.description" = "To issue a card for FREE: \ -Hold, stake or provide liquidity for at least €%@ worth of XOR in your SORA account"; -"card.issuance.screen.free.card.get.xor" = "Get %@ XOR"; -"card.issuance.screen.paid.card.description" = "One time fee to issue a card"; -"card.issuance.screen.paid.card.note" = "Note: paid card issuance will be available at a later stage."; -"card.issuance.screen.paid.card.pay.euro" = "Pay €%@ issuance fee"; -"card.issuance.screen.paid.card.title" = "€%@ Issuance fee"; -"card.issuance.screen.title" = "Card issuance"; -"card.or" = "or"; -"card.update.button" = "Update app"; -"card.update.title" = "Update app to latest version to access SORA Card"; -"cardhub.coming.soon" = "Card management is coming soon"; -"cardhub.exchange" = "Exchange"; -"cardhub.freeze" = "Freeze"; -"cardhub.iban.title" = "IBAN account details"; -"cardhub.top.up" = "Top up"; -"cardhub.transfer" = "Transfer"; -"change.email.title" = "Change your email"; -"common.back" = "Back"; -"common.change.email" = "Use another email"; -"common.no.spam" = "No spam! Only to secure your account"; -"common.resend.code" = "Resend code"; -"common.resend.link" = "Resend link"; -"common.resend.timer" = "Resend in %02d:%02d"; -"common.send.code" = "Send SMS code"; -"common.send.link" = "Send link"; -"common.support" = "Support"; -"common.try.again" = "Try again"; -"common.user.not.found" = "No mobile number found, please recheck it and try again."; -"details.already.have.card" = "I already have a card"; -"details.already.used.free.try" = "You already used your free try"; -"details.annual.service.fee" = "€0 annual service fee"; -"details.description" = "Get a Euro IBAN account and debit card accessible within your SORA Wallet"; -"details.enough.xor.desription" = "You have enough XOR"; -"details.free.card.issuance" = "Free card issuance"; -"details.free.card.issuance.conditions.euro" = "or €%@ application fee"; -"details.free.card.issuance.conditions.xor" = "If you hold, stake or provide liquidity for at least €%@ worth of XOR in your SORA account"; -"details.get.more.xor" = "Get more XOR"; -"details.issue.card" = "Issue card for free"; -"details.need.xor.desription" = "You need %@ more XOR (€%@)"; -"details.title" = "Top up SORA Card with fiat or crypto and pay online, in-store or withdraw from an ATM"; -"enter.email.description" = "Magic link will be sent\nto your email"; -"enter.email.input.field.label" = "Email"; -"enter.email.title" = "Enter your email"; -"enter.phone.number.description" = "You will receive\na verification code via SMS"; -"enter.phone.number.phone.input.field.label" = "Phone Number"; -"entry.card.info" = "Card info"; -"get.more.xor.dialog.buy.option" = "Buy XOR with card"; -"get.more.xor.dialog.deposit.option" = "Receive XOR"; -"get.more.xor.dialog.description" = "You can swap for XOR or buy with Euro"; -"get.more.xor.dialog.swap.option" = "Swap crypto for XOR"; -"get.prepared.alert" = "You have only %@ free attempts to pass the KYC process. Every other attempt after that will cost €%@. Paid attempts will be available after the next update of the app."; -"get.prepared.need" = "To complete the KYC you will need to:"; -"get.prepared.ok.title" = "OK, I’m ready"; -"get.prepared.personal.info.description" = "Fill in the form with your name and address"; -"get.prepared.personal.info.title" = "Submit your personal info"; -"get.prepared.proof.address.description" = "An official document (utility bill, bank statement, government statement or correspondence) that contains your full name and address, and that is no older then 3 months."; -"get.prepared.proof.address.note" = "Note: Statements from neobanks like Revolut and N26 are currently not seen as valid proof of address."; -"get.prepared.proof.address.title" = "Submit a photo of proof of address"; -"get.prepared.submit.id.photo.description" = "Passport, Driver License, ID card or Residence Permit"; -"get.prepared.submit.id.photo.title" = "Submit a photo of your ID document"; -"get.prepared.take.selfie.description" = "The guidance will be provided during the process"; -"get.prepared.take.selfie.title" = "Take a selfie"; -"get.prepared.title" = "Get prepared"; -"iban.frozen.description" = "Your IBAN has been frozen.\ -To find out more, contact %@"; -"iban.pending.description" = "Your IBAN issuance is pending.\ -In case the wait longer than 72h contact us via %@"; -"iban.suspended.description" = "Your IBAN has been suspended.\ -To find out more, contact %@"; -"kyc.result.verification.in.progress" = "Verification in progress"; -"kyc.result.verification.in.progress.description" = "You have successfully completed your KYC application. The review is pending and you can expect a decision shortly.\n\nUsually the decision is made the same day and in some cases can take up to 3 days."; -"log.out" = "Log out"; -"login.title" = "Log in or Sign up"; -"no.free.kyc.attempts.description" = "You have used your free attempt to pass the KYC process.\n\nWe kindly ask you to wait until the next upgrade of the application to proceed with paid attempts."; -"no.free.kyc.attempts.title" = "No more free attempts"; -"otp.error.message.wrong.code" = "Code incorrect. Please, try again."; -"paid.attempts.available.later" = "Paid attempts will be available at a later stage"; -"payment.widget.unavailable.confirm" = "Yes, I understand"; -"payment.widget.unavailable.description" = "Apologies for the inconvenience.\nWe’re working diligently to resolve this.\nPlease, try again later."; -"payment.widget.unavailable.message" = "The payment widget is currently unavailable"; -"payment.widget.unavailable.title" = "Widget unavailable"; -"select.country.title" = "Select your country"; -"status.not.started" = "Get SORA Card"; -"terms.and.conditions.confirm.description" = "By continuing you confirm that you have read, understood and accepted these policies"; -"terms.and.conditions.sora.community.alert" = "The SORA community does not collect any personal data, to get the SORA Card and IBAN account you will need to complete a KYC process directly with the card issuer."; -"unsupported.countries.disclaimer" = "Residents from certain countries can not apply for SORA Card at this moment"; -"unsupported.countries.link" = "See the list"; -"user.registration.first.name.input.filed.label" = "First name"; -"user.registration.last.name.input.filed.label" = "Last name"; -"user.registration.title" = "Introduce yourself"; -"verification.failed.description" = "The KYC was terminated or it failed otherwise."; -"verification.failed.title" = "Verification failed"; -"verification.rejected.description" = "Your application has been rejected."; -"verification.rejected.screen.attempts.price.disclaimer" = "Every other attempt will cost you €%@"; -"verification.rejected.screen.attempts.used" = "You have used your free KYC attempts."; -"verification.rejected.screen.try.again.for.euros" = "Try again for €%@"; -"verification.rejected.screen.try.again.for.free" = "Try again for free"; -"verification.rejected.support" = "Support"; -"verification.rejected.title" = "Verification rejected"; -"verification.successful.description" = "Your KYC verification is successful and we are already preparing to send you the SORA card!"; -"verification.successful.title" = "Application approved"; -"verify.email.description" = "Follow the magic link that has been sent\nto %@ \nThe email might take several minutes to arrive. Please verify your spam folder if you haven't received the email after 5 minutes."; -"verify.email.resend" = "RE-SEND IN %@"; -"verify.email.title" = "Verify your email"; -"verify.phone.number.code.input.field.label" = "SMS code"; -"verify.phone.number.description" = "Enter SMS code that has been sent to\n%@"; -"verify.phone.number.send.code" = "SEND CODE"; -"balance.locks.blocked.row.title" = "Blocked"; -"wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%s) on the destination account will be less than the minimal balance (%s). Please increase the amount by %s"; -"common.more" = "more"; -"nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; -"wallet.all.assets.hidden" = "You have hidden all assets"; -"xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; +"common.more" = "More"; diff --git a/fearless/id.lproj/Localizable.strings b/fearless/id.lproj/Localizable.strings index e40d3699aa..11cfde5ab5 100644 --- a/fearless/id.lproj/Localizable.strings +++ b/fearless/id.lproj/Localizable.strings @@ -91,10 +91,14 @@ "application.status.view.copied.description" = "Alamat dompet berhasil disalin"; "application.status.view.copied.title" = "Alamat tersalin"; "application.status.view.hash.copied.description" = "Hash sukses disalin"; +"application.status.view.hash.copied.description" = "Hash berhasil tersalin"; "application.status.view.hash.copied.title" = "Hash disalin"; +"application.status.view.hash.copied.title" = "Hash tersalin"; "application.status.view.offline.description" = "Silakan periksa koneksi Anda dan coba lagi"; "application.status.view.offline.title" = "Tampaknya Anda sedang offline"; "application.status.view.reconnected.description" = "Koneksi Berhasil pulih"; +"application.status.view.reconnected.description" = "Koneksi berhasil dipulihkan"; +"application.status.view.reconnected.title" = "Menghubungkan kembali"; "application.status.view.reconnected.title" = "Menghubungkan kembali"; "applied.fearless.wallet.bonus" = "Bonus fearless wallet diterapkan"; "apply.fearless.wallet.bonus" = "Ketuk untuk menerapkan bonus Fearless wallet"; @@ -152,6 +156,7 @@ "backup.wallet.replace.several.alert" = "Saat ini Anda memiliki beberapa akun berantai yang telah ditambahkan dengan mengganti pasangan kunci utama, dan akun tersebut memiliki akun yang spesifik. Namun, perlu diketahui bahwa alur pencadangan (ekspor) dompet kami saat ini tidak mendukung penyimpanan beberapa pasangan kunci. Akibatnya, Anda hanya dapat menyimpan pasangan kunci utama Anda. \n \n Untuk memastikan keamanan akun rantai pengganti Anda, kami menyarankan Anda mencadangkannya terlebih dahulu secara terpisah sebelum melanjutkan dengan alur saat ini. Setelah Anda berhasil mencadangkan akun rantai yang diganti, Anda dapat melanjutkan ke alur saat ini tanpa khawatir."; "backup.wallet.seed" = "Tunjukkan Frase Seed"; "backup.wallet.title" = "Cadangkan dompet"; +"balance.locks.blocked.row.title" = "Blocked"; "balance.locks.governance.row.title" = "Pemerintahan"; "balance.locks.liquidity.pools.row.title" = "Kumpulan Likuiditas"; "balance.locks.nomination.pools.row.title" = "Kumpulan Nominasi"; @@ -175,6 +180,7 @@ "common.action.receive" = "Terima"; "common.action.send" = "Kirim"; "common.action.teleport" = "Teleportasi"; +"common.activation.required" = "Activation Required"; "common.add" = "Menambahkan"; "common.address" = "Alamat"; "common.advanced" = "Lanjutan"; @@ -279,6 +285,7 @@ "common.search.start.title" = "Hasil pencarian akan muncul di sini"; "common.secret.derivation.path" = "Secret derivation path"; "common.select" = "Pilih"; +"common.select" = "Pilih"; "common.select.all" = "Pilih semuanya"; "common.select.asset" = "Pilih aset"; "common.select.network" = "Pilih jaringan"; @@ -394,6 +401,7 @@ "error.invalid.address" = "Alamat tidak valid untuk jaringan yang dipilih"; "error.message.enter.the.name" = "Masukan nama..."; "error.message.enter.the.url.address" = "Masukan alamat URL..."; +"error.scan.qr.disabled.asset" = "The asset you want to transfer is either unsupported or turned off. Go to Asset Management, activate the asset, and then scan the QR code again."; "error.unsupported.asset" = "Anda mencoba melakukan transfer aset yang saat ini tidak didukung di Aplikasi. Silakan pilih aset lain atau minta kode QR lain."; "ethereum.crypto.type" = "Jenis kripto pasangan kunci Ethereum"; "ethereum.secret.derivation.path" = "Jalur derivasi rahasia Ethereum"; @@ -459,6 +467,26 @@ Seed: %@"; "label.testnet" = "Testnet"; "language.title" = "Bahasa"; "learn.more.about.crowdloans" = "Pelajari lebih lanjut tentang Crowdloan"; +"lp.apy.alert.text" = "Farming reward for liquidity provision"; +"lp.apy.alert.title" = "Strategic Bonus APY"; +"lp.apy.title" = "Strategic Bonus APY"; +"lp.available.pools.title" = "Available pools"; +"lp.confirm.liquidity.screen.title" = "Confirm Liquidity"; +"lp.confirm.liquidity.warning.text" = "Output is estimated. If the price changes more than 0.5% your transaction will revert."; +"lp.network.fee.alert.text" = "Network fee is used to ensure SORA system’s growth and stable performance."; +"lp.network.fee.alert.title" = "Network fee"; +"lp.pool.details.title" = "Pool details"; +"lp.pool.remove.warning.text" = "Removing pool tokens converts your position back into underlying tokens at the current rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive."; +"lp.pool.remove.warning.title" = "NOTE"; +"lp.remove.button.title" = "Remove Liquidity"; +"lp.remove.liquidity.screen.title" = "Remove Liquidity"; +"lp.reward.token.text" = "Earn %@"; +"lp.reward.token.title" = "Rewards Payout In"; +"lp.slippage.title" = "Slippage"; +"lp.supply.button.title" = "Supply Liquidity"; +"lp.supply.liquidity.screen.title" = "Supply Liquidity"; +"lp.token.pooled.text" = "Your %@ Pooled"; +"lp.user.pools.title" = "User pools"; "manage.assets.account.missing.text" = "Tambahkan akun..."; "manage.assets.search.hint" = "Cari berdasarkan aset"; "members.common" = "Anggota"; @@ -479,6 +507,7 @@ Seed: %@"; "network.info.address" = "Alamat node"; "network.info.name" = "Nama node"; "network.info.title" = "Node Info"; +"network.issue.main" = "Connection Error: Unable to connect to the network. Please try again."; "network.issue.network.unavailible" = "Jaringan tidak tersedia"; "network.issue.node.unavailable" = "Node tidak tersedia"; "network.issue.notofication" = "Pemberitahuan"; @@ -500,6 +529,7 @@ Seed: %@"; "nft.list.empty.message" = "Belum ada NFT. Beli atau cetak NFT untuk melihatnya di sini."; "nft.owner.title" = "Dimiliki"; "nft.share.address" = "Alamat publik saya untuk menerima:%s"; +"nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; "nft.stub.text" = "Nft segera hadir"; "nft.stub.title" = "Sumimasen!"; "nft.tokenid.title" = "ID token"; @@ -584,17 +614,20 @@ Syarat dan Ketentuan serta Kebijakan Privasi"; "polkaswap.disclaimer.title" = "Penafian"; "polkaswap.liqudity.fee.info" = "Sebagian dari setiap perdagangan (0,3%) diberikan kepada penyedia likuiditas sebagai insentif protokol."; "polkaswap.liquidity.provider.fee" = "Biaya penyedia likuiditas"; +"polkaswap.liquidity.provider.fee" = "Biaya penyedia likuiditas"; "polkaswap.market.alert.choose.action" = "Pilih aset"; "polkaswap.market.alert.message" = "Anda belum menyelesaikan penyesuaian swap. Salah satu atau kedua aset belum dipilih, sehingga pemilihan pasar tidak tersedia."; "polkaswap.market.alert.title" = "Pilihan pasar tidak tersedia"; "polkaswap.market.algorithm.title" = "Algoritma pasar"; "polkaswap.market.smart.description" = "Perutean likuiditas SMART memastikan harga terbaik untuk setiap transaksi dengan hanya menggabungkan opsi harga terbaik dari semua pasar yang tersedia. Jika tersedia, Token Bonding Curve (TBC) akan digunakan untuk likuiditas selama harga aset lebih terjangkau dibandingkan dari sumber lain, yang mana kumpulan XYK digunakan."; "polkaswap.market.stub" = "Pasar:"; +"polkaswap.market.stub" = "Pasar"; "polkaswap.market.tbc.description" = "TBC — membeli hanya dari Token Bonding Curve (Pasar Utama). Ada kemungkinan bahwa harga bisa menjadi tidak menguntungkan dibandingkan dengan kumpulan XYK (Pasar Sekunder), namun nilai yang diterima dari imbalan pribadi mungkin menjadi jauh lebih menguntungkan seiring berjalannya waktu."; "polkaswap.market.xyk.description" = "XYK — membeli hanya dari kumpulan XYK (Pasar Sekunder). Pertukaran kumpulan XYK tradisional di mana siapa pun dapat membeli atau menjual aset dengan menggeser posisi pembuat pasar pada kurva x*y=k."; "polkaswap.max.received" = "Maksimum terjual"; "polkaswap.maximum.sold.info" = "Transaksi Anda akan kembali terjadi jika terjadi pergerakan harga yang besar dan tidak menguntungkan sebelum dikonfirmasi."; "polkaswap.min.received" = "Minimal diterima"; +"polkaswap.min.received" = "Minimal diterima"; "polkaswap.minimum.received.info" = "Transaksi Anda akan kembali terjadi jika terjadi pergerakan harga yang besar dan tidak menguntungkan sebelum dikonfirmasi."; "polkaswap.network.fee" = "Biaya jaringan"; "polkaswap.network.fee.info" = "Biaya jaringan digunakan untuk memastikan pertumbuhan sistem SORA dan kinerja yang stabil."; @@ -827,9 +860,12 @@ Syarat dan Ketentuan serta Kebijakan Privasi"; "staking.pool.create.root" = "Root"; "staking.pool.create.stateToggler" = "Pengalih status"; "staking.pool.create.title" = "Buat kumpulan"; +"staking.pool.create.title" = "Buat sebuah kumpulan"; "staking.pool.info.title" = "Info kumpulan"; "staking.pool.management.select.validators.subtitle" = "Untuk melanjutkan, Anda harus\nmemilih validator"; +"staking.pool.management.select.validators.subtitle" = "Untuk melanjutkan, Anda harus\nmemilih validator"; "staking.pool.management.select.validators.title" = "Select pool validators"; +"staking.pool.management.select.validators.title" = "Pilih kumpulan validator"; "staking.pool.rewards.delay.text" = "Stake kapan saja. Anda akan mulai mendapatkan bunga setelah %s"; "staking.pool.select.validators.manual" = "Pilih manual"; "staking.pool.select.validators.suggested" = "Pilih disarankan"; @@ -887,6 +923,7 @@ Syarat dan Ketentuan serta Kebijakan Privasi"; "staking.round.title" = "ronde #%@"; "staking.select.suggested" = "Pilih disarankan"; "staking.select.validators.confirm.title" = "Memilih validator"; +"staking.select.validators.confirm.title" = "Memilih validator"; "staking.select.validators.custom.button.title" = "Pilih sendiri"; "staking.select.validators.custom.desc" = "Anda harus memercayai pencalonan Anda untuk bertindak secara kompeten dan jujur, mendasarkan keputusan Anda semata-mata pada profitabilitas mereka saat ini dapat menyebabkan berkurangnya keuntungan atau bahkan kehilangan dana."; "staking.select.validators.custom.title" = "Stake dengan validator Anda"; @@ -1015,9 +1052,9 @@ Syarat dan Ketentuan serta Kebijakan Privasi"; "transaction.details.from" = "Dari"; "transaction.details.hash.title" = "Extrinsic Hash"; "transaction.details.view.etherscan" = "Lihat di Etherscan"; +"transaction.details.view.oklink" = "View in OKX explorer"; "transaction.details.view.polkascan" = "Lihat di Polkascan"; "transaction.details.view.reefscan" = "Lihat di Reefscan"; -"transaction.details.view.oklink" = "View in OKX explorer"; "transaction.details.view.subscan" = "Lihat di Subscan"; "transaction.list.header" = "Semua transaksi"; "transaction.status.completed" = "Selesai"; @@ -1033,6 +1070,8 @@ Syarat dan Ketentuan serta Kebijakan Privasi"; "username.setup.title" = "Buat Akun"; "username.setup.title.2.0" = "Buat dompet baru"; "validator.info.comission.title" = "Komisi"; +"validator.info.min.stake.alert.text" = "Minimum stake among active nominators is %s. To get rewards you have to stake more."; +"validator.info.min.stake.among.active.nominators.text" = "Minimum stake among active nominators"; "validators.list.empty.message" = "Validator tidak ditemukan"; "verify.phone.number.title" = "Verifikasi nomor ponselmu"; "vesting.claim.disclaimer.text" = "Karena jadwal vesting unik dari setiap parachain, aplikasi kami tidak dapat menampilkan jumlah token terkunci yang memenuhi syarat untuk diklaim. Harap diperhatikan bahwa mengajukan klaim mungkin tidak praktis jika jumlah yang diharapkan kecil dan sebanding dengan biaya transaksi. Untuk wawasan komprehensif tentang saldo terkunci Anda, konsultasikan dengan penjelajah blok Subscan. Kami mendesak Anda untuk menilai informasi dengan hati-hati dan melanjutkan sesuai kebijaksanaan Anda sendiri."; @@ -1042,6 +1081,7 @@ Syarat dan Ketentuan serta Kebijakan Privasi"; "view.wallet" = "Lihat dompet"; "wallet.account.locks.democracy" = "Demokrasi"; "wallet.account.locks.vesting" = "Pemberian"; +"wallet.all.assets.hidden" = "Kamu sudah menyembunyikan semua aset"; "wallet.asset.buy" = "Beli"; "wallet.asset.buy.with" = "Beli dengan"; "wallet.asset.receive" = "Menerima"; @@ -1099,6 +1139,7 @@ akan muncul di sini"; "wallet.send.balance.total.after.transfer" = "Total setelah transfer"; "wallet.send.confirm.title" = "Konfirmasi transfer"; "wallet.send.dead.recipient.message" = "Transfer Anda akan gagal karena jumlah akhir di akun tujuan kurang dari setoran yang ada. Tolong, coba tambah jumlahnya."; +"wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%1$s) on the destination account will be less than the minimal balance (%2$s). Please increase the amount by %3$s"; "wallet.send.dead.recipient.title" = "Jumlah terlalu sedikit"; "wallet.send.eth.dead.recipient.message" = "Saldo Ethereum yang tidak mencukupi di akun penerima menghalangi penyelesaian transfer token ERC20. Harap pastikan penerima memiliki cukup Ethereum untuk melanjutkan transfer."; "wallet.send.existential.warning" = "Transfer Anda akan menghapus akun dari blockstore karena akan membuat saldo total lebih rendah dari existential deposit"; @@ -1119,9 +1160,11 @@ akan muncul di sini"; "what.accounts.for.export" = "Akun apa di dompet yang ingin Anda ekspor?"; "xcm.cross.chain.button.title" = "Lintas rantai"; "xcm.cross.chain.invalid.address.message" = "Menurut alamat yang diberikan Anda mencoba melakukan transfer di jaringan yang salah."; +"xcm.cross.chain.invalid.address.message" = "Menurut alamat yang diberikan Anda mencoba melakukan transfer di jaringan yang salah."; "xcm.cross.chain.invalid.address.title" = "Itu bukan alamat jaringan"; "xcm.destination.network.fee.title" = "Biaya destinasi jaringan"; "xcm.destination.network.title" = "Destinasi jaringan"; +"xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; "xcm.mywallets.button.title" = "Dompetku"; "xcm.origin.network.fee.title" = "Biaya jaringan origin"; "xcm.origin.network.title" = "Jaringan origin"; @@ -1130,130 +1173,4 @@ akan muncul di sini"; "your.validators.stop.nominating.title" = "berhenti mencalonkan diri"; "your.validators.validator.total.stake" = "Total ditaruhkan: %@"; "сurrencies.stub.text" = "Mata uang"; -"cant.fetch.refresh" = "Can't fetch data, tap to refresh."; -"card.attention.text" = "Perhatian"; -"card.hub.manage.card" = "Manage SORA Card"; -"card.hub.manage.card.alert.message" = "To manage your SORA Card, please install the official SORA Card App. Tap OK to be directed to the App Store."; -"card.hub.manage.card.alert.title" = "Download SORA Card App"; -"card.hub.settings.logout.button" = "Log out"; -"card.hub.settings.logout.description" = "You are about to log out of SORA Card. You will still have access to the SORA Card standalone app, but the balance will no longer be available to you in the SORA Wallet."; -"card.hub.settings.logout.title" = "Keluar dari Kartu SORA"; -"card.hub.settings.title" = "Card settings"; -"card.hub.title" = "Card details"; -"card.hub.update.button" = "Update now"; -"card.hub.update.description" = "You are using an outdated version of the app, some part might not be working as intended."; -"card.hub.update.title" = "Update app to latest version"; -"card.issuance.screen.free.card.description" = "To issue a card for FREE: \ -Hold, stake or provide liquidity for at least €%@ worth of XOR in your SORA account"; -"card.issuance.screen.free.card.get.xor" = "Get %@ XOR"; -"card.issuance.screen.paid.card.description" = "One time fee to issue a card"; -"card.issuance.screen.paid.card.note" = "Catatan: Penerbitan kartu berbayar akan tersedia pada tahap selanjutnya."; -"card.issuance.screen.paid.card.pay.euro" = "Pay €%@ issuance fee"; -"card.issuance.screen.paid.card.title" = "€ %@ biaya penerbitan"; -"card.issuance.screen.title" = "Card issuance"; -"card.or" = "atau"; -"card.update.button" = "Update app"; -"card.update.title" = "Update app to latest version to access SORA Card"; -"cardhub.coming.soon" = "Pengelolaan kartu akan segera hadir"; -"cardhub.exchange" = "Menukarkan"; -"cardhub.freeze" = "Membekukan"; -"cardhub.iban.title" = "Detail akun IBAN"; -"cardhub.top.up" = "Isi ulang"; -"cardhub.transfer" = "Transfer"; -"change.email.title" = "Change your email"; -"common.back" = "Back"; -"common.change.email" = "Use another email"; -"common.no.spam" = "Tidak ada email spam! Hanya untuk mengamankan akun Anda"; -"common.resend.code" = "Resend code"; -"common.resend.link" = "Resend link"; -"common.resend.timer" = "Kirim ulang dalam %02d:% 02d"; -"common.send.code" = "Kirim kode SMS"; -"common.send.link" = "Kirim tautan"; -"common.support" = "Support"; -"common.try.again" = "Try again"; -"common.user.not.found" = "Nomor ponsel tidak ditemukan, harap periksa kembali dan coba lagi."; -"details.already.have.card" = "I already have a card"; -"details.already.used.free.try" = "You already used your free try"; -"details.annual.service.fee" = "€0 biaya layanan tahunan"; -"details.description" = "Dapatkan akun Euro IBAN dan kartu debit yang dapat diakses di dalam SORA Wallet Anda"; -"details.enough.xor.desription" = "You have enough XOR"; -"details.free.card.issuance" = "Penerbitan kartu gratis"; -"details.free.card.issuance.conditions.euro" = "or €%@ application fee"; -"details.free.card.issuance.conditions.xor" = "If you hold, stake or provide liquidity for at least €%@ worth of XOR in your SORA account"; -"details.get.more.xor" = "Get more XOR"; -"details.issue.card" = "Issue card for free"; -"details.need.xor.desription" = "Anda membutuhkan %@ lebih banyak XOR (€%@)"; -"details.title" = "Top up SORA Card with fiat or crypto and pay online, in-store or withdraw from an ATM"; -"enter.email.description" = "Magic link will be sent\nto your email"; -"enter.email.input.field.label" = "Surel"; -"enter.email.title" = "Enter your email"; -"enter.phone.number.description" = "Anda akan menerima \n kode verifikasi melalui SMS"; -"enter.phone.number.phone.input.field.label" = "Nomor telepon"; -"entry.card.info" = "Card info"; -"get.more.xor.dialog.buy.option" = "Buy XOR with card"; -"get.more.xor.dialog.deposit.option" = "Receive XOR"; -"get.more.xor.dialog.description" = "You can swap for XOR or buy with Euro"; -"get.more.xor.dialog.swap.option" = "Swap crypto for XOR"; -"get.prepared.alert" = "Anda hanya memiliki %@ percobaan gratis untuk melewati proses KYC. Setiap percobaan lainnya setelah itu akan dikenakan biaya €%@. Percobaan berbayar akan tersedia setelah pembaruan aplikasi berikutnya."; -"get.prepared.need" = "To complete the KYC you will need to:"; -"get.prepared.ok.title" = "Baik saya siap"; -"get.prepared.personal.info.description" = "Isi formulir dengan nama dan alamat Anda"; -"get.prepared.personal.info.title" = "Kirimkan informasi pribadi Anda"; -"get.prepared.proof.address.description" = "Dokumen resmi (tagihan utilitas, laporan bank, laporan pemerintah atau korespondensi) yang berisi nama lengkap dan alamat Anda, dan berumur tidak lebih dari 3 bulan."; -"get.prepared.proof.address.note" = "Catatan: Pernyataan dari neobank seperti Revolut dan N26 saat ini tidak dianggap sebagai bukti alamat yang sah."; -"get.prepared.proof.address.title" = "Kirimkan foto bukti alamat"; -"get.prepared.submit.id.photo.description" = "Paspor, Surat Izin Mengemudi, KTP atau Izin Tinggal"; -"get.prepared.submit.id.photo.title" = "Kirimkan foto dokumen identitas Anda"; -"get.prepared.take.selfie.description" = "Bimbingan akan diberikan selama proses tersebut"; -"get.prepared.take.selfie.title" = "Ambil selfie"; -"get.prepared.title" = "Bersiap"; -"iban.frozen.description" = "Your IBAN has been frozen.\ -To find out more, contact %@"; -"iban.pending.description" = "Penerbitan IBAN Anda tertunda.\ - Jika menunggu lebih dari 72 jam, hubungi kami melalui %@"; -"iban.suspended.description" = "Your IBAN has been suspended.\ -To find out more, contact %@"; -"kyc.result.verification.in.progress" = "Verifikasi sedang berlangsung"; -"kyc.result.verification.in.progress.description" = "You have successfully completed your KYC application. The review is pending and you can expect a decision shortly.\n\nUsually the decision is made the same day and in some cases can take up to 3 days."; -"log.out" = "Log out"; -"login.title" = "Masuk atau mendaftar"; -"no.free.kyc.attempts.description" = "You have used your free attempt to pass the KYC process.\n\nWe kindly ask you to wait until the next upgrade of the application to proceed with paid attempts."; -"no.free.kyc.attempts.title" = "Tidak ada lagi percobaan gratis"; -"otp.error.message.wrong.code" = "Kode salah. Silakan, coba lagi."; -"paid.attempts.available.later" = "Paid attempts will be available at a later stage"; -"payment.widget.unavailable.confirm" = "Yes, I understand"; -"payment.widget.unavailable.description" = "Apologies for the inconvenience.\nWe’re working diligently to resolve this.\nPlease, try again later."; -"payment.widget.unavailable.message" = "The payment widget is currently unavailable"; -"payment.widget.unavailable.title" = "Widget unavailable"; -"select.country.title" = "Pilih negaramu"; -"status.not.started" = "Get SORA Card"; -"terms.and.conditions.confirm.description" = "Dengan melanjutkan, Anda mengonfirmasi bahwa Anda telah membaca, memahami, dan menerima kebijakan ini"; -"terms.and.conditions.sora.community.alert" = "Untuk mendapatkan akun IBAN yang diperlukan untuk SORA Card, pengguna diharuskan menjalani proses KYC dengan penerbit kartu. Ini diperlukan kepatuhan. Komunitas SORA tidak dan tidak akan mengumpulkan data pribadi Anda."; -"unsupported.countries.disclaimer" = "Penduduk dari negara tertentu tidak dapat mengajukan SORA Card untuk saat ini"; -"unsupported.countries.link" = "Lihat daftarnya"; -"user.registration.first.name.input.filed.label" = "Nama depan"; -"user.registration.last.name.input.filed.label" = "nama keluarga"; -"user.registration.title" = "Introduce yourself"; -"verification.failed.description" = "The KYC was terminated or it failed otherwise."; -"verification.failed.title" = "Verification failed"; -"verification.rejected.description" = "Your application has been rejected."; -"verification.rejected.screen.attempts.price.disclaimer" = "Setiap upaya lainnya akan dikenakan biaya € %@"; -"verification.rejected.screen.attempts.used" = "You have used your free KYC attempts."; -"verification.rejected.screen.try.again.for.euros" = "Try again for €%@"; -"verification.rejected.screen.try.again.for.free" = "Try again for free"; -"verification.rejected.support" = "Support"; -"verification.rejected.title" = "Lamaran ditolak"; -"verification.successful.description" = "Verifikasi KYC Anda berhasil dan kami sedang mempersiapkan untuk mengirimkan kartu SORA kepada Anda!"; -"verification.successful.title" = "Permohonan disetujui"; -"verify.email.description" = "Follow the magic link that has been sent\nto %@ \nThe email might take several minutes to arrive. Please verify your spam folder if you haven't received the email after 5 minutes."; -"verify.email.resend" = "RE-SEND IN %@"; -"verify.email.title" = "Verify your email"; -"verify.phone.number.code.input.field.label" = "SMS code"; -"verify.phone.number.description" = "Enter SMS code that has been sent to\n%@"; -"verify.phone.number.send.code" = "SEND CODE"; -"balance.locks.blocked.row.title" = "Blocked"; -"wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%s) on the destination account will be less than the minimal balance (%s). Please increase the amount by %s"; -"common.more" = "more"; -"nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; -"wallet.all.assets.hidden" = "You have hidden all assets"; -"xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; +"common.more" = "More"; diff --git a/fearless/ja.lproj/Localizable.strings b/fearless/ja.lproj/Localizable.strings index d8cc99bd10..e32172c1be 100644 --- a/fearless/ja.lproj/Localizable.strings +++ b/fearless/ja.lproj/Localizable.strings @@ -91,10 +91,14 @@ "application.status.view.copied.description" = "ウォレットのアドレスが正常にコピーされました"; "application.status.view.copied.title" = "アドレスをコピーしました"; "application.status.view.hash.copied.description" = "ハッシュが正常にコピーされました"; +"application.status.view.hash.copied.description" = "ハッシュが正常にコピーされました"; +"application.status.view.hash.copied.title" = "ハッシュをコピーしました"; "application.status.view.hash.copied.title" = "ハッシュをコピーしました"; "application.status.view.offline.description" = "インターネット接続を確認し、もう一度お試しください"; "application.status.view.offline.title" = "インターネットに繋がってないようです"; "application.status.view.reconnected.description" = "接続が正常に復元されました"; +"application.status.view.reconnected.description" = "接続が正常に復元されました"; +"application.status.view.reconnected.title" = "再接続されました"; "application.status.view.reconnected.title" = "再接続されました"; "applied.fearless.wallet.bonus" = "フィアレス・ウォレットのボーナスが適用されました"; "apply.fearless.wallet.bonus" = "タップしてフィアレス・ウォレットのボーナスを適用"; @@ -152,6 +156,7 @@ "backup.wallet.replace.several.alert" = "あなたは現在、メインのキー・ペアを置き換えて追加した複数のチェーンアカウントを持っており、それぞれに特定のキー・ペアを所有しています。しかし、現在のウォレットのバックアップ(エクスポート)機能は、複数のキー・ペアの保存をサポートしていないのでご注意ください。現状、保存できるのはメインのキー・ペアのみとなります。 \n \n チェーンアカウントの安全性を確保するため、現在のフローを進める前に、まず別個にバックアップすることをお勧めします。置き換えたチェーンアカウントのバックアップに成功したら、何の心配もなくフローを進むことができます。"; "backup.wallet.seed" = "シードを表示"; "backup.wallet.title" = "ウォレットのバックアップ"; +"balance.locks.blocked.row.title" = "Blocked"; "balance.locks.governance.row.title" = "Governance"; "balance.locks.liquidity.pools.row.title" = "Liquidity Pools"; "balance.locks.nomination.pools.row.title" = "Nomination Pools"; @@ -175,6 +180,7 @@ "common.action.receive" = "受け取る"; "common.action.send" = "送る"; "common.action.teleport" = "テレポート"; +"common.activation.required" = "Activation Required"; "common.add" = "追加"; "common.address" = "アドレス"; "common.advanced" = "高度な"; @@ -279,6 +285,7 @@ "common.search.start.title" = "検索結果はここに表示されます"; "common.secret.derivation.path" = "秘密の派生パス(Derivation Path)"; "common.select" = "選択"; +"common.select" = "選択"; "common.select.all" = "全て選択"; "common.select.asset" = "アセットの選択"; "common.select.network" = "ネットワークの選択"; @@ -394,6 +401,7 @@ "error.invalid.address" = "選択したチェーンのアドレスが無効です"; "error.message.enter.the.name" = "名前を入力してください…"; "error.message.enter.the.url.address" = "URLアドレスを入力してください…"; +"error.scan.qr.disabled.asset" = "The asset you want to transfer is either unsupported or turned off. Go to Asset Management, activate the asset, and then scan the QR code again."; "error.unsupported.asset" = "現在アプリでサポートされていないアセットを送信しようとしています。別のアセットを選択するか、別のQRコードをリクエストしてください"; "ethereum.crypto.type" = "イーサリアム(Ethereum)キー・ペアの暗号方式"; "ethereum.secret.derivation.path" = "イーサリアム(Ethereum)の秘密の派生パス(Derivation Path)"; @@ -459,6 +467,26 @@ "label.testnet" = "試験ネットワーク"; "language.title" = "言語"; "learn.more.about.crowdloans" = "クラウドローン(Crowdloan)についてさらに学ぶ"; +"lp.apy.alert.text" = "Farming reward for liquidity provision"; +"lp.apy.alert.title" = "Strategic Bonus APY"; +"lp.apy.title" = "Strategic Bonus APY"; +"lp.available.pools.title" = "Available pools"; +"lp.confirm.liquidity.screen.title" = "Confirm Liquidity"; +"lp.confirm.liquidity.warning.text" = "Output is estimated. If the price changes more than 0.5% your transaction will revert."; +"lp.network.fee.alert.text" = "Network fee is used to ensure SORA system’s growth and stable performance."; +"lp.network.fee.alert.title" = "Network fee"; +"lp.pool.details.title" = "Pool details"; +"lp.pool.remove.warning.text" = "Removing pool tokens converts your position back into underlying tokens at the current rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive."; +"lp.pool.remove.warning.title" = "NOTE"; +"lp.remove.button.title" = "Remove Liquidity"; +"lp.remove.liquidity.screen.title" = "Remove Liquidity"; +"lp.reward.token.text" = "Earn %@"; +"lp.reward.token.title" = "Rewards Payout In"; +"lp.slippage.title" = "Slippage"; +"lp.supply.button.title" = "Supply Liquidity"; +"lp.supply.liquidity.screen.title" = "Supply Liquidity"; +"lp.token.pooled.text" = "Your %@ Pooled"; +"lp.user.pools.title" = "User pools"; "manage.assets.account.missing.text" = "アカウントを追加..."; "manage.assets.search.hint" = "トークンまたはネットワークで検索"; "members.common" = "会員"; @@ -485,6 +513,7 @@ "network.info.address" = "ノードアドレス"; "network.info.name" = "ノード名"; "network.info.title" = "ノード情報"; +"network.issue.main" = "Connection Error: Unable to connect to the network. Please try again."; "network.issue.network.unavailible" = "ネットワークは利用できません"; "network.issue.node.unavailable" = "ノードは利用できません"; "network.issue.notofication" = "お知らせ"; @@ -506,6 +535,7 @@ "nft.list.empty.message" = "There aren't any NFTs yet. Buy or mint NFTs to see them here."; "nft.owner.title" = "所有"; "nft.share.address" = "受信するパブリック・アドレス: %s"; +"nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; "nft.stub.text" = "NFTがもうすぐ登場します"; "nft.stub.title" = "失礼しました"; "nft.tokenid.title" = "トークンID"; @@ -589,17 +619,20 @@ "polkaswap.disclaimer.title" = "免責事項"; "polkaswap.liqudity.fee.info" = "各取引の一部(0.3%)は、プロトコル・インセンティブとして流動性プロバイダー(LP)に送られます"; "polkaswap.liquidity.provider.fee" = "流動性プロバイダー手数料"; +"polkaswap.liquidity.provider.fee" = "流動性プロバイダー手数料"; "polkaswap.market.alert.choose.action" = "アセットの選択"; "polkaswap.market.alert.message" = "スワップ調整が完了していません。一方または両方のアセットが選択されていないため、マーケット選択は利用できません"; "polkaswap.market.alert.title" = "マーケット選択は利用できません"; "polkaswap.market.algorithm.title" = "マーケット・アルゴリズム"; "polkaswap.market.smart.description" = "SMART流動性ルーティングは、利用可能な全てのマーケットから最良の価格オプションだけ組み合わせることで、あらゆるトランザクションの最良の価格を保証します。XYKプールや他のプールが利用される場合より資産価格が手頃であれば、可能な限りトークン・ボンディング・カーブ(TBC)が流動性に使用されます"; "polkaswap.market.stub" = "マーケット:"; +"polkaswap.market.stub" = "マーケット"; "polkaswap.market.tbc.description" = "TBC ー トークン・ボンディング・カーブ(プライマリー・マーケット)からのみの購入。XYKプール(セカンダリー・マーケット)と比較して不利な価格になる可能性がありますが、既得権報酬から得られる価値は、時間の経過とともにはるかに有利になる可能性があります"; "polkaswap.market.xyk.description" = "XYK — XYKプール(セカンダリー・マーケット)からのみ購入します。x*y=k 曲線上のマーケット・メーカーの位置をシフトすることで、誰でもアセットを売買できる従来のXYKプールスワップ"; "polkaswap.max.received" = "最大販売数"; "polkaswap.maximum.sold.info" = "確認される前に大きくて不利な価格変動があった場合、トランザクションは元に戻ります"; "polkaswap.min.received" = "受信した最少"; +"polkaswap.min.received" = "最少受信"; "polkaswap.minimum.received.info" = "確認される前に大きくて不利な価格変動があった場合、トランザクションは元に戻ります"; "polkaswap.network.fee" = "ネットワーク手数料"; "polkaswap.network.fee.info" = "ネットワーク手数料は、SORAシステムの発展と安定したパフォーマンスを確保するために使用されます"; @@ -832,8 +865,11 @@ "staking.pool.create.root" = "ルート"; "staking.pool.create.stateToggler" = "状態トグラー"; "staking.pool.create.title" = "プールを作成"; +"staking.pool.create.title" = "プールを作成"; "staking.pool.info.title" = "プール情報"; "staking.pool.management.select.validators.subtitle" = "続行するには、\nバリデーターを選択してください"; +"staking.pool.management.select.validators.subtitle" = "続行するには、\nバリデーターを選択してください"; +"staking.pool.management.select.validators.title" = "プールバリデーターの選択"; "staking.pool.management.select.validators.title" = "プールバリデーターの選択"; "staking.pool.rewards.delay.text" = "いつでもステークできます。%s以降に利息が発生します"; "staking.pool.select.validators.manual" = "手動で選択"; @@ -892,6 +928,7 @@ "staking.round.title" = "ラウンド #%@"; "staking.select.suggested" = "提案されたものを選択"; "staking.select.validators.confirm.title" = "バリデーターの選択"; +"staking.select.validators.confirm.title" = "バリデーターの選択"; "staking.select.validators.custom.button.title" = "自分で選択してください"; "staking.select.validators.custom.desc" = "ノミネーターが有能かつ誠実に行動することを信頼すべきです。現在の収益性だけで判断すると、利益の減少や資金喪失につながる可能性があります"; "staking.select.validators.custom.title" = "バリデーターとステークする"; @@ -1022,9 +1059,9 @@ "transaction.details.from" = "から"; "transaction.details.hash.title" = "外形的ハッシュ"; "transaction.details.view.etherscan" = "イーサスキャン(Etherscan)で見る"; +"transaction.details.view.oklink" = "View in OKX explorer"; "transaction.details.view.polkascan" = "ポルカスキャン(Polkascan)で見る"; "transaction.details.view.reefscan" = "View in Reefscan"; -"transaction.details.view.oklink" = "View in OKX explorer"; "transaction.details.view.subscan" = "サブスキャン(Subscan)で見る"; "transaction.list.header" = "全てのトランザクション"; "transaction.status.completed" = "完了"; @@ -1040,6 +1077,8 @@ "username.setup.title" = "アカウントを作成する"; "username.setup.title.2.0" = "新しいウォレットを作成"; "validator.info.comission.title" = "コミッション"; +"validator.info.min.stake.alert.text" = "Minimum stake among active nominators is %s. To get rewards you have to stake more."; +"validator.info.min.stake.among.active.nominators.text" = "Minimum stake among active nominators"; "validators.list.empty.message" = "バリデーターが見つかりません"; "verify.phone.number.title" = "Verify your phone number"; "vesting.claim.disclaimer.text" = "Due to the unique vesting schedules of each parachain, our app is unable to display the quantity of locked tokens eligible for claiming. Please be advised that initiating a claim may be impractical if the prospective amount is marginal and comparable to the transaction fee involved. For comprehensive insights into your locked balances, consult the Subscan blockexplorer. We urge you to assess the information carefully and proceed at your own discretion."; @@ -1049,6 +1088,7 @@ "view.wallet" = "ウォレットを見る"; "wallet.account.locks.democracy" = "民主主義"; "wallet.account.locks.vesting" = "権利確定"; +"wallet.all.assets.hidden" = "全アセットを非表示にしました"; "wallet.asset.buy" = "購入"; "wallet.asset.buy.with" = "で購入"; "wallet.asset.receive" = "受け取る"; @@ -1105,6 +1145,7 @@ "wallet.send.balance.total.after.transfer" = "送信後の合計"; "wallet.send.confirm.title" = "送信を確認"; "wallet.send.dead.recipient.message" = "宛先アカウントの最終金額が既存の預金より少ないため、送信は失敗します。量を増やしてみてください"; +"wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%1$s) on the destination account will be less than the minimal balance (%2$s). Please increase the amount by %3$s"; "wallet.send.dead.recipient.title" = "金額が少なすぎる"; "wallet.send.eth.dead.recipient.message" = "受信者のアカウントのイーサリアム(Ethereum)残高が不十分な場合、ERC20トークン転送が完了しません。受信者が転送を続行するのに十分なイーサリアムを持っていることを確認してください"; "wallet.send.existential.warning" = "送信すると、残高が最少預金よりも少なくなるため、ブロックストアからアカウントが削除されます"; @@ -1125,9 +1166,11 @@ "what.accounts.for.export" = "ウォレット内のどのアカウントをエクスポートしますか?"; "xcm.cross.chain.button.title" = "クロス・チェーン"; "xcm.cross.chain.invalid.address.message" = "提供されたアドレスによると、間違ったネットワークで送信しようとしています"; +"xcm.cross.chain.invalid.address.message" = "提供されたアドレスによると、間違ったネットワークで送信しようとしています"; "xcm.cross.chain.invalid.address.title" = "ネットワーク・アドレスではありません"; "xcm.destination.network.fee.title" = "宛先ネットワーク手数料"; "xcm.destination.network.title" = "宛先ネットワーク"; +"xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; "xcm.mywallets.button.title" = "私のウォレット"; "xcm.origin.network.fee.title" = "元ネットワーク手数料"; "xcm.origin.network.title" = "元ネットワーク"; @@ -1136,9 +1179,4 @@ "your.validators.stop.nominating.title" = "ノミネートを中止"; "your.validators.validator.total.stake" = "総ステーク:%@"; "сurrencies.stub.text" = "通貨"; -"balance.locks.blocked.row.title" = "Blocked"; -"wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%s) on the destination account will be less than the minimal balance (%s). Please increase the amount by %s"; -"common.more" = "more"; -"nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; -"wallet.all.assets.hidden" = "You have hidden all assets"; -"xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; +"common.more" = "More"; diff --git a/fearless/pt-PT.lproj/Localizable.strings b/fearless/pt-PT.lproj/Localizable.strings index 645b157e59..6fddcf6a59 100644 --- a/fearless/pt-PT.lproj/Localizable.strings +++ b/fearless/pt-PT.lproj/Localizable.strings @@ -1278,3 +1278,4 @@ To find out more, contact %@"; "nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; "wallet.all.assets.hidden" = "You have hidden all assets"; "xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; +"common.more" = "More"; diff --git a/fearless/pt.lproj/Localizable.strings b/fearless/pt.lproj/Localizable.strings index 3f21dc96f0..9dab7ed33b 100644 --- a/fearless/pt.lproj/Localizable.strings +++ b/fearless/pt.lproj/Localizable.strings @@ -1,1166 +1,1196 @@ -"common.close" = "Fechar"; -"common.next" = "Próximo"; -"common.cancel" = "Cancelar"; -"common.skip" = "Saltar"; -"common.use" = "Usar"; -"common.error.general.title" = "Erro"; -"common.copy.address" = "Copiar endereço"; -"operation.error.title" = "A operação falhou"; -"operation.error.message" = "Por favor, tente novamente mais tarde."; -"connection.error.title" = "Falha na conexão"; -"connection.error.message" = "Por favor, verifique a ligação à Internet e tente novamente mais tarde."; -"onboarding.restore.account" = "Importar conta"; -"onboarding.terms.and.conditions.1" = "Li e concordei com\ -os Termos, Condições e a Política de Privacidade"; -"common.terms.and.conditions" = "Termos e Condições"; -"common.privacy.policy" = "Política de Privacidade"; -"pincode.enter.pin.code" = "Insira o código PIN"; -"pincode.confirm.your.pin.code" = "Confirme o seu código PIN"; -"pincode.set.your.pin.code" = "Defina o seu código PIN"; -"ask.touchid.title" = "ID Touch"; -"ask.touchid.message" = "Gostaria de usar a identificação digital para autenticação?"; -"ask.faceid.title" = "ID Facial"; -"ask.faceid.message" = "Gostaria de usar a Identificação Facial para a autenticação?"; -"ask.biometry.reason" = "Autenticar para aceder à conta"; -"access.restore.words.error.message" = "A mnemónica deve conter 24 palavras no máximo"; -"access.restore.phrase.error.message" = "A sua mnemónica é inválida. Por favor, tente outra."; -"common.wallet" = "Carteira"; -"common.staking" = "Staking"; -"tabbar.settings.title" = "Configurações"; -"wallet.empty.description" = "Entradas e Saídas \ -as operações aparecerão aqui"; -"profile.title" = "Configurações"; -"profile.about.title" = "Sobre"; -"profile.language.title" = "Idioma"; -"about.version" = "Versão da aplicação"; -"about.contact.us" = "Email de contato"; -"about.title" = "Sobre"; -"about.privacy" = "Política de privacidade"; -"language.title" = "Idioma"; -"help.support.title" = "Apoio da Fearless"; -"no.email.bound.error.message" = "Por favor, certifique-se de que uma aplicação de e-mail está instalada no dispositivo."; -"connection.management.title" = "Redes"; -"onboarding.create.account" = "Criar conta"; "NSCameraUsageDescription" = "A câmara é usada para capturar o código QR"; "NSFaceIDUsageDescription" = "Identificação facial usada para autorização na aplicação"; -"NSPhotoLibraryUsageDescription" = "Fazer upload de fotos da biblioteca"; "NSPhotoLibraryAddUsageDescription" = "Guardar pedido de transferência como um código QR"; -"common.name" = "Nome"; -"username.setup.hint" = "Este nome será exibido apenas para si e armazenado localmente no seu dispositivo móvel."; -"username.setup.title" = "Criar conta"; -"common.no.screenshot.title" = "Não faça capturas de tela"; -"common.no.screenshot.message" = "Não faça capturas de tela, que podem ser coletadas por malware de terceiros"; -"common.ok" = "OK"; -"account.create.title" = "Backup mnemónico"; -"account.create.details" = "Use uma forma não digital para fazer o backup, como escrever em papel."; -"common.advanced" = "Avançado"; -"common.crypto.type" = "Criptografia do tipo par de chaves"; -"common.secret.derivation.path" = "Caminho de derivação secreto"; -"common.choose.network" = "Escolher a rede"; -"sr25519.selection.subtitle" = "sr25519 (recomendado)"; -"ed25519.selection.subtitle" = "ed25519 (alternativa)"; -"ecdsa.selection.subtitle" = "(compatível com BTC/ETH)"; -"sr25519.selection.title" = "Schnorrkel"; -"ed25519.selection.title" = "Edwards"; -"ecdsa.selection.title" = "ECDSA"; -"account.creation.info" = "A mnemónica é usada para recuperar o acesso à conta. Anote-a, nós não poderemos recuperar a sua conta sem ela!"; -"common.info" = "Info"; -"common.invalid.path.title" = "Formato inválido"; -"common.invalid.path.with.soft.message" = "Por favor, verifique se a entrada é uma mistura de //'hard' e /'soft' onde 'hard' e 'soft' podem ser qualquer sequência de caracteres não incluindo /. O caminho pode terminar com ///'password'. Por exemplo, //1/soramitsu///mypass."; -"common.invalid.path.without.soft.message" = "Por favor, verifique se a entrada é uma mistura de //'hard' onde 'hard' pode ser qualquer sequência de caracteres não incluindo /. O caminho pode terminar com ///'password'. Por exemplo, //1//soramitsu///mypass."; -"common.undefined.error.title" = "Erro indefinido"; -"common.undefined.error.message" = "Por favor, tente novamente com outro input. Se o erro aparecer novamente, por favor, contacte o apoio ao utilizador."; -"common.unsupported.network.message" = "O tipo de rede ainda não é suportado. Por favor, escolha outro."; -"import.source.picker.title" = "Tipo de fonte"; -"import.raw.seed" = "Seed primária"; -"import.recovery.json" = "Restaurar o JSON"; -"account.import.password.placeholder" = "Senha"; -"recover.json.hint" = "Colar o JSON ou fazer upload do ficheiro..."; -"account.import.recovery.json.placeholder" = "Colar JSON"; -"account.import.raw.seed.placeholder" = "Seed primária: 64 símbolos hexadecimais"; -"import.mnemonic" = "Frase Mnemónica"; -"account.import.invalid.seed" = "A seed é inválida. Por favor, certifique-se de que a sua entrada contém 64 símbolos hexadecimais."; -"account.import.invalid.keystore" = "Restauro do JSON inválido. Por favor, certifique-se de que a entrada contém um JSON válido."; -"account.import.keystore.decryption.error.message" = "Por favor, verifique se a senha está correta e tente novamente."; -"account.import.keystore.decryption.error.title" = "A descodificação da Keystore falhou"; -"account.confirmation.title" = "Confirmar a mnemónica"; -"account.confirmation.details" = "Escolha as palavras na ordem correta"; -"pincode.create.top.title" = "Criar código PIN"; -"profile.accounts.title" = "Contas"; -"profile.network.title" = "Redes"; -"profile.pincode.change.title" = "Alterar código PIN"; -"common.invalid.hard.soft.message" = "Por favor, verifique se a entrada é uma mistura de //'hard' e /'soft' onde 'hard' e 'soft' podem ser qualquer sequência de caracteres não incluindo /."; -"common.invalid.hard.message" = "Por favor, verifique se a entrada é uma mistura de //'hard' onde 'hard' pode ser qualquer sequência de caracteres não incluindo /."; -"confirm.mnemonic.mismatch.error.title" = "Mnemónica inválida"; -"confirm.mnemonic.mismatch.error.message" = "Por favor, verifique a ordem das palavras mais uma vez."; -"common.edit" = "Editar"; -"common.done" = "Concluído"; -"accounts.add.account" = "Adicionar conta"; -"account.add.already.exists.message" = "A conta já existe. Por favor, tente outra."; -"about.website" = "Website Oficial"; +"NSPhotoLibraryUsageDescription" = "Fazer upload de fotos da biblioteca"; +"about.announcement" = "Receber Alertas"; +"about.ask.for.support" = "Pedir Apoio"; +"about.contact.email" = "Email de Contacto"; +"about.contact.us" = "Email de contato"; +"about.crowdloans" = "Saiba mais sobre Crowdloans"; +"about.follow.on.twitter" = "Seguir no Twitter"; "about.github" = "Código Fonte no Github"; +"about.github.source.code" = "Código Fonte no Github"; +"about.instagram" = "Gostar no Instagram"; +"about.join.on.telegram" = "Juntar-se no Telegram"; +"about.learn.on.wiki" = "Aprender na Wiki"; +"about.like.on.instagram" = "Gostar no Instagram"; +"about.medium" = "Ler no Medium"; +"about.official.website" = "Website Oficial"; +"about.privacy" = "Política de privacidade"; +"about.privacy.policy" = "Política de Privacidade"; +"about.read.on.medium" = "Ler no Medium"; +"about.receive.announcements" = "Receber Anúncios"; +"about.subscribe.on.youTube" = "Subscrever no YouTube"; +"about.support" = "Solicitar Apoio"; "about.telegram" = "Juntar-se no Telegram"; -"confirmation.skip.action" = "Saltar processo"; -"account.info.title" = "Conta"; -"account.info.name.title" = "Nome"; -"common.export" = "Exportar conta"; -"common.save" = "Guardar"; -"common.copied" = "Copiado para a área de transferência"; -"connections.add.connection" = "Adicionar conexão"; -"connection.management.default.title" = "Nodes padrão"; -"connection.management.custom.title" = "Nodes personalizados"; -"account.delete.confirmation.title" = "Esqueceu a conta?"; -"account.delete.confirmation.description" = "Certifique-se de ter exportado a sua conta antes de prosseguir."; +"about.terms.and.conditions" = "Termos e Condições"; +"about.title" = "Sobre"; +"about.twitter" = "Seguir no Twitter"; +"about.version" = "Versão da aplicação"; +"about.website" = "Website Oficial"; +"about.wiki" = "Aprender na Wiki"; +"about.youtube" = "Subscrever no YouTube"; +"access.restore.phrase.error.message" = "A sua mnemónica é inválida. Por favor, tente outra."; +"access.restore.words.error.message" = "A mnemónica deve conter 24 palavras no máximo"; +"account.add.already.exists.message" = "A conta já existe. Por favor, tente outra."; +"account.confirmation.details" = "Escolha as palavras na ordem correta"; +"account.confirmation.title" = "Confirmar a mnemónica"; +"account.create.details" = "Use uma forma não digital para fazer o backup, como escrever em papel."; +"account.create.error.missing.crypto.type.title" = "Tipo de crypto ausente"; +"account.create.title" = "Backup mnemónico"; +"account.creation.info" = "A mnemónica é usada para recuperar o acesso à conta. Anote-a, nós não poderemos recuperar a sua conta sem ela!"; "account.delete.confirm" = "Esquecer"; -"account.needed.title" = "Conta necessária"; -"account.needed.message" = "Não há uma conta nesta rede, pode criar ou importar uma conta."; -"common.proceed" = "Proceder"; -"wallet.asset.receive" = "Receber"; -"wallet.assets.total.title" = "Valor dos ativos"; -"network.info.title" = "Informação do node"; -"network.info.name" = "Nome do node"; -"network.info.address" = "Endereço do node"; -"common.add" = "Adicionar"; -"common.update" = "Atualizar"; -"connection.add.invalid.error" = "Não se consegue estabelecer ligação com o node. Por favor, tente outro."; -"connection.add.unsupported.error" = "Infelizmente, a rede não é suportada. Por favor, tente uma dos seguintes: %@ ."; -"connection.add.already.exists.error" = "O node já foi adicionado anteriormente. Por favor, tente outro node."; -"wallet.send.title" = "Enviar"; -"wallet.send.asset.title" = "Ativo"; -"wallet.send.receiver.title" = "Para"; -"wallet.send.fee.title" = "Taxa de transferência"; -"common.continue" = "Continuar"; -"wallet.send.amount.title" = "Montante"; -"wallet.send.confirm.title" = "Confirmar transferência"; -"wallet.send.available.balance" = "Saldo disponível"; -"wallet.contacts.empty.title" = "A suas contas e contatos para quem estava enviando transferências aparecerão aqui"; -"wallet.search.empty.title" = "Certifique-se de que o endereço é\ -da rede correta"; -"wallet.contacts.search.placeholder" = "Endereço da conta ou nome da conta"; -"wallet.balance.available" = "Disponível"; -"common.total" = "Total"; -"wallet.search.contacts" = "Contatos"; -"common.retry" = "Tentar novamente"; -"wallet.balance.frozen" = "Congelado"; -"transaction.status.completed" = "Concluído"; -"transaction.status.failed" = "Falhou"; -"transaction.status.pending" = "Pendente"; -"transaction.detail.status" = "Status"; -"transaction.detail.date" = "Data"; -"transaction.details.from" = "De"; -"wallet.balance.locked" = "Bloqueado"; -"wallet.balance.reserved" = "Reservado"; -"wallet.balance.redeemable" = "Resgatável"; -"transaction.details.hash.title" = "Hash extrínseco"; -"transaction.details.copy.hash" = "Copiar hash"; -"transaction.details.view.polkascan" = "Ver na Polkascan"; -"transaction.details.view.subscan" = "Ver no Subscan"; -"wallet.send.balance.details" = "Detalhes do saldo"; -"wallet.send.balance.total" = "Saldo total"; -"wallet.send.balance.total.after.transfer" = "Total após transferência"; -"wallet.send.existential.warning" = "A sua transferência removerá a conta do blockstore, pois tornará o saldo total inferior ao depósito existente."; -"common.warning" = "Aviso"; -"connection.delete.title" = "Apagar rede?"; -"connection.delete.description" = "Confirme a exclusão de %@."; -"connection.delete.confirm" = "Eliminar"; -"network.status.connecting" = "Conectando..."; -"network.status.connected" = "Conectado"; -"wallet.receive.description" = "Mostrar este código QR ao remetente"; -"wallet.search.accounts" = "as minhas contas"; -"wallet.receive.share.message" = "O meu endereço %@ para receber %@ :"; -"import.json.invalid.format.title" = "O restauro do JSON é inválido"; -"import.json.invalid.format.message" = "Por favor, certifique-se de que a sua entrada contém um JSON válido."; -"import.seed.invalid.title" = "A seed é inválida"; -"import.seed.invalid.message" = "Por favor, certifique-se de que a sua entrada contém 64 símbolos hexadecimais."; -"mnemonic.error.try.another.one" = "Por favor, tente outra."; -"import.account.exists.title" = "A conta já existe"; +"account.delete.confirmation.description" = "Certifique-se de ter exportado a sua conta antes de prosseguir."; +"account.delete.confirmation.title" = "Esqueceu a conta?"; +"account.error.try.another.one" = "Por favor, tente outro."; "account.export.action" = "Exportar"; +"account.export.json.hint" = "A senha é necessária para encriptar a sua conta e armazenar como ficheiro de restauro JSON. Por favor, crie uma senha para continuar a operação."; "account.export.warning.message" = "Partilhar ou copiar o seu segredo é uma operação de alto risco, não o envie a ninguém. Gostaria de continuar com o processo de partilhar/copiar?"; "account.export.warning.title" = "Seja cuidadoso"; -"account.export.json.hint" = "A senha é necessária para encriptar a sua conta e armazenar como ficheiro de restauro JSON. Por favor, crie uma senha para continuar a operação."; -"common.set.password" = "Definir senha"; -"common.confirm.password" = "Confirmar senha"; -"common.error.password.mismatch" = "A senha não corresponde"; -"common.network" = "Rede"; -"common.change.password" = "Alterar senha"; -"export.mnemonic.with.dp.template" = "Rede: %@\ -Mnemónica: %@\ -Caminho de derivação: %@"; -"export.mnemonic.without.dp.template" = "Rede: %@\ -Mnemónica: %@"; -"export.mnemonic.hint" = "Use meios não digitais para fazer backup, como escrever a sequência de palavras mnemónicas e o caminho de derivação (se definido) em papel."; -"common.confirm" = "Confirmar"; -"common.confirmed" = "Confirmado"; -"export.seed.without.dp.template" = "Rede: %@\ -Seed: %@"; -"export.seed.with.dp.template" = "Rede: %@\ -Seed: %@\ -Caminho de derivação: %@"; -"common.changed" = "Alterado"; +"account.import.ethereum.raw.seed.placeholder" = "A seed original para contas da ETH (64 símbolos hexadecimais)"; +"account.import.invalid.keystore" = "Restauro do JSON inválido. Por favor, certifique-se de que a entrada contém um JSON válido."; +"account.import.invalid.seed" = "A seed é inválida. Por favor, certifique-se de que a sua entrada contém 64 símbolos hexadecimais."; "account.import.json.no.network" = "O ficheiro JSON não contém informações de rede. Por favor, especifique-o abaixo."; +"account.import.keystore.decryption.error.message" = "Por favor, verifique se a senha está correta e tente novamente."; +"account.import.keystore.decryption.error.title" = "A descodificação da Keystore falhou"; +"account.import.password.placeholder" = "Senha"; +"account.import.raw.seed.placeholder" = "Seed primária: 64 símbolos hexadecimais"; +"account.import.recovery.json.placeholder" = "Colar JSON"; +"account.import.recovery.select.file" = "Selecionar ficheiro"; +"account.import.substrate.raw.seed.placeholder" = "A seed original para contas da Substrate (64 símbolos hexadecimais)"; "account.import.wrong.network" = "A rede em JSON é %@, não corresponde à %@ selecionada. Esta última será usada para importar uma conta."; -"common.address" = "Endereço"; -"wallet.asset.buy" = "Comprar"; -"wallet.send.dead.recipient.title" = "O montante é demasiado baixo"; -"wallet.send.dead.recipient.message" = "A sua transferência falhará, pois o valor final na conta de destino será menor que o depósito existente. Por favor, tente aumentar a quantia."; -"buy.completed" = "Compra iniciada! Por favor aguarde até 60 minutos. Pode acompanhar o estado através do e-mail."; -"wallet.send.phishing.warning.text" = "O endereço seguinte: %@ é conhecido por ser utilizado em atividades de phishing, pelo que não recomendamos o envio de tokens para esse endereço. Gostaria de prosseguir na mesma?"; -"wallet.send.phishing.warning.title" = "Alerta de fraude"; -"staking.start.title" = "Começar staking"; -"common.network.fee" = "Taxa de rede"; -"staking.restake.title" = "Restake"; -"common.payout" = "Pagamento"; -"staking.reward.payout.account" = "Conta para pagamento"; -"common.learn.more" = "Saiba mais"; -"staking.recommended.title" = "Validadores"; -"staking.recommended.section.title" = "Validadores recomendados pelo algoritmo"; -"staking.recommended.hint1" = "Mais rentável"; -"staking.recommended.hint2" = "Sem excesso de subscrições"; -"staking.recommended.hint3" = "Ter identidade na rede"; -"staking.recommended.hint4" = "Não penalizado"; -"staking.recommended.validators.counter" = "%@ de %@"; -"staking.recommended.custom.title" = "Escolher validadores personalizados"; -"fee.not.yet.loaded.title" = "Cálculo das taxas em curso"; -"fee.not.yet.loaded.message" = "Por favor aguarde até que a taxa seja calculada"; -"staking.setup.amount.too.low" = "Não é possível fazer staking menor do que o valor mínimo (%@)"; -"common.confirm.title" = "Confirmação"; -"staking.stash.title" = "Conta de carteira"; -"staking.setup.sent.message" = "Transação de configuração de staking enviada"; -"staking.selected.validators.title" = "Validadores selecionados"; -"staking.validator.info.title" = "Informação do validador"; -"staking.validator.total.stake" = "Stake total"; -"staking.validator.nominators" = "Nomeadores"; -"staking.validator.estimated.reward" = "Recompensa estimada"; -"identity.title" = "Identidade"; -"identity.legal.name.title" = "Designação legal"; -"identity.email.title" = "Email"; -"identity.web.title" = "Web"; -"identity.riot.name.title" = "Nome do elemento"; -"staking.main.network.title" = "Rede %@"; -"staking.main.total.staked.title" = "Stake total"; -"staking.main.minimum.stake.title" = "Stake mínimo"; -"staking.main.active.nominators.title" = "Nomeadores ativos"; -"staking.validator.own.stake" = "Pertencente ao validador"; -"staking.stake" = "Stake"; -"staking.nominator.status.active" = "Ativo"; -"staking.nominator.status.inactive" = "Inativo"; -"staking.nominator.status.waiting" = "À espera da próxima Era"; -"staking.era.title" = "era #%@"; -"staking.validator.summary.title" = "Staking para validador"; -"staking.validator.summary.description" = "Em atualizações futuras, adicionaremos recursos de staking para os validadores. \n \n #StayFearless"; -"staking.nominator.status.alert.active.title" = "Estado ativo"; -"staking.nominator.status.alert.active.message" = "Um dos seus validadores foi eleito pela rede."; -"staking.nominator.status.alert.waiting.message" = "O seu staking começará na próxima era."; -"staking.nominator.status.alert.inactive.title" = "Estado inativo"; -"staking.nominator.status.alert.no.validators" = "Nenhum dos seus validadores foi eleito pela rede."; -"staking.nominator.status.alert.low.stake" = "O valor do seu stake é inferior ao stake mínimo para obter uma recompensa."; -"staking.manage.title" = "Gerir"; -"staking.reward.payouts.title" = "Recompensas pendentes"; -"staking.reward.payouts.payout.all" = "Pagar a todos (%@)"; -"staking.reward.details.title" = "Detalhes da recompensa"; -"staking.reward.details.payout" = "Pagamento"; -"staking.reward.details.status" = "Estado"; -"staking.reward.details.status.claimable" = "Reclamável"; -"staking.reward.details.status.received" = "Recebido"; -"staking.reward.details.date" = "Data"; -"staking.reward.details.era" = "Era"; -"staking.reward.details.reward" = "Recompensa"; -"staking.reward" = "Recompensa"; -"staking.slash" = "Penalização"; -"common.call" = "Ligar"; -"common.module" = "Módulo"; -"wallet.asset.buy.with" = "Comprar %s com"; -"wallet.filters.title" = "Filtros"; +"account.info.name.title" = "Nome"; +"account.info.title" = "Conta"; +"account.needed.message" = "Não há uma conta nesta rede, pode criar ou importar uma conta."; +"account.needed.title" = "Conta necessária"; +"account.option" = "Opção de conta"; +"account.template" = "%s conta"; +"account.unique.secret" = "Contas com segredos exclusivos"; +"accounts.add.account" = "Adicionar conta"; +"accounts.for.export" = "Contas para exportação"; +"accounts.with.changed.key" = "Contas com chave alterada"; +"accounts.with.one.key" = "Contas com uma chave"; +"accounts.with.shared.secret" = "Contas com um segredo partilhado"; +"add.node.button.title" = "Adicionar node"; +"alert.add.ethereum.message" = "Adicionar contas Moonbeam e Moonriver?"; +"alert.add.ethereum.title" = "Contas ETH"; +"alert.pool.created.text" = "Sua pool foi criada com sucesso. Agora você deve escolher validadores para a pool"; +"all.done.alert.all.done.stub" = "Tudo pronto"; +"all.done.alert.description.stub" = "Você pode retornar ao aplicativo"; +"all.done.alert.hash.stub" = "Hash"; +"all.done.alert.result.stub" = "Resultado"; +"all.done.alert.success.stub" = "Sucesso"; +"all.done.subscan.button.title" = "Subscan"; +"already.have.account" = "Já tenho uma conta"; +"app.version.json.loading.failed" = "Falha no carregamento dos dados necessários para o lançamento do pedido. Por favor, verifique a sua ligação à Internet e tente novamente."; +"app.version.unsupported.text" = "Já não apoiamos esta versão da aplicação. Por favor, atualize-na."; +"applecation.status.view.coppied.description" = "Endereço da carteira copiado com sucesso"; +"applecation.status.view.coppied.title" = "Endereço copiado"; +"applecation.status.view.offline.description" = "Por favor verifique sua conexão e tente novamente"; +"applecation.status.view.offline.title" = "Parece que você está off-line"; +"application.status.view.copied.description" = "Endereço da carteira copiado com sucesso"; +"application.status.view.copied.title" = "Endereço copiado"; +"application.status.view.hash.copied.description" = "Hash copiado com sucesso"; +"application.status.view.hash.copied.description" = "Hash copiado com sucesso"; +"application.status.view.hash.copied.title" = "Hash copiado"; +"application.status.view.hash.copied.title" = "Hash copiado"; +"application.status.view.offline.description" = "Por favor verifique sua conexão e tente novamente"; +"application.status.view.offline.title" = "Parece que você está off-line"; +"application.status.view.reconnected.description" = "Conexão restaurada com sucesso"; +"application.status.view.reconnected.description" = "Conexão restaurada com sucesso"; +"application.status.view.reconnected.title" = "Reconectado"; +"application.status.view.reconnected.title" = "Reconectado"; +"applied.fearless.wallet.bonus" = "Bónus da carteira Fearless aplicado"; +"apply.fearless.wallet.bonus" = "Tocar para aplicar o bónus da Fearless"; +"ask.biometry.reason" = "Autenticar para aceder à conta"; +"ask.faceid.message" = "Gostaria de usar a Identificação Facial para a autenticação?"; +"ask.faceid.title" = "ID Facial"; +"ask.touchid.message" = "Gostaria de usar a identificação digital para autenticação?"; +"ask.touchid.title" = "ID Touch"; +"assetdetails.balance.locked" = "Bloqueado"; +"assetdetails.balance.title" = "O seu saldo"; +"assetdetails.balance.total" = "Total"; +"assetdetails.balance.transferable" = "Transferível"; +"astar.bonus" = "Bónus para si"; +"astar.friend.bonus" = "Bónus para o seu amigo"; +"backup.chain.account" = "Conta de cadeia de backup"; +"backup.create.password.confirm.field.title" = "Confirmar senha"; +"backup.create.password.continue.button" = "Definir senha de backup"; +"backup.create.password.description" = "Definir uma senha criptografará seu backup do Google. Você precisará inserir isso ao restaurar sua carteira"; +"backup.create.password.matched" = "A palavra-passe corresponde"; +"backup.create.password.not.matched" = "A palavra-passe não corresponde"; +"backup.create.password.password.field.title" = "Definir senha"; +"backup.create.password.screen.title" = "Criar senha de backup"; +"backup.create.password.warning" = "Entendo que se eu esquecer minha senha não há como recuperá-la"; +"backup.mnemonic.description" = "Lembre-se de registrar suas palavras na mesma ordem em que aparecem abaixo. Use uma forma não digital de backup."; +"backup.mnemonic.title" = "Escreva a sua mnemónica"; +"backup.not.backed.up.confirm" = "Arrisco-me a fazê-lo"; +"backup.not.backed.up.message" = "Se o seu dispositivo for perdido ou roubado, você perderá sua carteira e todos os seus fundos para sempre"; +"backup.not.backed.up.title" = "Não foi feito backup!"; +"backup.password.description" = "Digite a senha de backup da carteira selecionada para importação"; +"backup.password.password.field.title" = "Digite a senha"; +"backup.password.title" = "Digite a senha de backup"; +"backup.risks.warnings.1" = "Se eu expor ou compartilhar minha frase mnemônica com alguém, meus fundos poderão ser roubados"; +"backup.risks.warnings.2" = "Se eu expor ou compartilhar minha frase mnemônica com alguém, meus fundos poderão ser roubados"; +"backup.risks.warnings.3" = "É minha total responsabilidade manter minha frase mnemônica segura"; +"backup.risks.warnings.continue.button" = "Mostrar frase mnemônica"; +"backup.risks.warnings.description" = "Sua frase mnemônica é a chave da sua carteira. Faça backup para poder restaurar sua carteira caso perca ou danifique seu dispositivo."; +"backup.risks.warnings.title" = "Faça backup do seu mnemônico"; +"backup.select.wallet.create.button" = "Criar nova conta"; +"backup.select.wallet.title" = "Selecione a carteira a ser importada"; +"backup.wallet.backup.google" = "Backup para o Google"; +"backup.wallet.delete.google" = "Excluir backup do Google"; +"backup.wallet.delete.message" = "Se você excluir seu backup do Google, só poderá recuperar sua carteira com um backup manual de sua senha"; +"backup.wallet.footer.view.text" = "Se você perder o acesso a este dispositivo, seus fundos serão perdidos, a menos que você faça backup!"; +"backup.wallet.imported.description" = "Você importou a carteira com sucesso"; +"backup.wallet.imported.import.more" = "Importar mais"; +"backup.wallet.imported.title" = "Carteira importada"; +"backup.wallet.json" = "Exportar JSON"; +"backup.wallet.name.create.bottom.decription" = "Visível apenas para você e armazenado localmente"; +"backup.wallet.name.create.description" = "Dê um nome para sua nova carteira, para que você possa identificá-la facilmente. Isso é opcional e ficará visível apenas para você"; +"backup.wallet.name.create.title" = "Nomeie sua nova carteira"; +"backup.wallet.name.editing.description" = "Exemplo: Poupança, Investimentos, Crowdloans, Staking. Este nome de conta será exibido apenas para você e armazenado localmente em seu dispositivo móvel"; +"backup.wallet.name.editing.title" = "Alterar nome da carteira"; +"backup.wallet.name.field.name.title" = "Nome da carteira"; +"backup.wallet.replace.accounts.alert" = "Atualmente você tem uma conta na cadeia %s e um endereço %s que foi adicionado substituindo o par de chaves principal e possui um par específico. No entanto, observe que nosso fluxo atual de backup (exportação) de carteira não suporta o armazenamento de vários pares de chaves. Como resultado, você só pode salvar seu par de chaves principal. \n \n Para garantir a segurança da sua conta chain substituída, recomendamos que você primeiro faça backup dela separadamente antes de prosseguir com o fluxo atual. Depois de fazer backup com êxito de sua conta em cadeia substituída, você poderá prosseguir para o fluxo atual sem preocupações."; +"backup.wallet.replace.several.alert" = "Atualmente você tem várias contas na cadeia que foram adicionadas substituindo o par de chaves principal e possui uma específica. No entanto, observe que nosso fluxo atual de backup (exportação) de carteira não suporta o armazenamento de vários pares de chaves. Como resultado, você só pode salvar seu par de chaves principal. \n \n Para garantir a segurança da sua conta da rede substituída, recomendamos que você primeiro faça backup dela separadamente antes de prosseguir com o fluxo atual. Depois de fazer backup com êxito de sua conta na cadeia substituída, você poderá prosseguir para o fluxo atual sem preocupações."; +"backup.wallet.seed" = "Mostrar semente crua"; +"backup.wallet.title" = "Carteira de backup"; +"balance.locks.blocked.row.title" = "Blocked"; +"balance.locks.governance.row.title" = "Governance"; +"balance.locks.liquidity.pools.row.title" = "Liquidity Pools"; +"balance.locks.nomination.pools.row.title" = "Nomination Pools"; +"balance.locks.screen.title" = "Locked details"; +"banners.view.factory.backup.action.title" = "Faça backup agora"; +"banners.view.factory.backup.subtitle" = "Se você perder seu dispositivo, seus fundos serão perdidos para sempre"; +"banners.view.factory.backup.title" = "Faça backup da sua carteira"; +"banners.view.factory.xor.action.title" = "Comprar XOR"; +"banners.view.factory.xor.subtitle" = "Comprar ou vender tokens XOR com Euros"; +"banners.view.factory.xor.title" = "Comprar token XOR"; +"btn.backup.with.google" = "Backup para o Google"; +"buy.completed" = "Compra iniciada! Por favor aguarde até 60 minutos. Pode acompanhar o estado através do e-mail."; +"chain.selection.all.networks" = "Todas as redes"; +"chain.selection.hide.zero.balances" = "Ocultar com saldo vazio"; +"chain.unsupported.text" = "Esta chain não está disponível na versão atual da aplicação. Por favor, atualize-a para a versão mais recente."; +"choose.pool.empty.title" = "Nenhuma pool \n foi criado ainda"; +"choose.recipient.next.button.title" = "Insira o valor"; +"choose.recipient.title" = "Selecionar destinatário"; +"common.account" = "Conta"; +"common.action.hide" = "Ocultar"; +"common.action.receive" = "Receber"; +"common.action.send" = "Enviar"; +"common.action.teleport" = "teleporte"; +"common.activation.required" = "Activation Required"; +"common.add" = "Adicionar"; +"common.address" = "Endereço"; +"common.advanced" = "Avançado"; +"common.and.others.placeholder" = "%s & others"; +"common.applied" = "Aplicado"; "common.apply" = "Aplicar"; -"common.reset" = "Reinicializar"; -"wallet.filters.transfers" = "Transferências"; -"wallet.filters.rewards.and.slashes" = "Recompensas e Penalizações"; -"wallet.filters.extrinsics" = "Outras transações"; -"wallet.filters.header" = "Mostrar"; -"transfer.title" = "Transferir"; +"common.approve" = "Aprovar"; +"common.attention" = "Atenção"; "common.available.format" = "Disponível: %@"; -"staking.rewards.learn.more" = "Saiba mais acerca das recompensas"; -"common.error.no.data.retrieved" = "Nenhuns dados recuperados."; -"staking.reward.payouts.empty.rewards" = "Perfeito! Todas as recompensas foram pagas."; -"staking.reward.details.validator" = "Validador"; -"staking.error.insufficient.balance.title" = "Saldo insuficiente"; -"common.confirmation.title" = "Tem a certeza?"; -"staking.warning.tiny.payout" = "A recompensa é menor que a taxa da rede."; -"staking.unbonding.limit.reached.title" = "Limite de pedidos de unstaking atingido"; -"staking.redeem" = "Resgatar"; +"common.available.networks" = "Available networks"; "common.balance" = "Saldo"; -"staking.balance.title" = "Saldo de staking"; -"staking.payout.sent" = "Transação de pagamento enviada"; -"common.transaction.submitted" = "Transação submetida"; -"staking.your.validators.title" = "Os seus validadores"; -"staking.rebond.all" = "Revincular todos"; -"staking.rebond.last" = "Revincular último"; -"staking.rebond.custom.amount" = "Revincular montante personalizado"; +"common.balance.format" = "Saldo: %@"; +"common.bonus" = "Bónus"; +"common.call" = "Ligar"; +"common.cancel" = "Cancelar"; +"common.cancel.operation.action" = "Cancelar operação"; +"common.cancel.operation.message" = "Tem certeza que deseja cancelar esta operação?"; "common.change" = "Alterar"; -"staking.your.nominated.format" = "Nomeado: %@"; -"staking.your.validators.changing.title" = "Os seus validadores mudarão na próxima era."; -"staking.validator.info.nominators" = "%@ (max %@)"; -"staking.bonded.format" = "Vinculado: %@"; -"staking.unbonding.all.title" = "Parar o staking"; -"staking.unbonding.all.message" = "O saldo restante de staking cairá abaixo do valor mínimo e será também adicionado ao valor sem staking"; -"staking.unbonding.hint" = "O seus tokens estarão disponíveis para resgatar após o período de desvinculação."; -"common.account" = "Conta"; -"staking.unbond.payee.reset.message" = "O destino da recompensa será alterado para sua conta para evitar um remanescente vinculado, uma vez que o staking será interrompido"; -"staking.redeemable.format" = "Resgatável: %@"; -"staking.redeem.no.tokens.message" = "Nenhum token resgatável encontrado"; -"staking.rebond" = "Retornar para stake"; -"staking.unbonding.format" = "Unstaking: %@"; -"staking.rebond.insufficient.bondings" = "A quantidade que deseja retornar para stake é maior do que o seu saldo sem stake"; -"stacking.stash.account" = "Conta stash"; -"staking.stash.missing.message" = "A conta stash %@ não está disponível para atualizar a configuração de staking."; -"staking.controller.account.title" = "Conta de controlador"; -"staking.bond.more.completion" = "A solicitação extra para stake foi enviada"; -"staking.account.is.used.as.controller" = "A conta selecionada já está a ser utilizada como controlador"; -"staking.month.period.format" = "%@ mensalmente"; -"staking.year.period.format" = "%@ anualmente"; -"staking.reward.info.title" = "Ganhos com restake"; -"staking.reward.info.max" = "APY máximo"; -"staking.reward.info.avg" = "APY médio"; -"staking.alerts.title" = "Alertas"; -"staking.recommended.hint5" = "Limite de 2 validadores por identidade"; -"staking.set.separate.account.controller" = "Defina uma conta separada como controlador para aumentar a segurança do gestão de staking."; -"staking.stash.can.hint" = "A conta de tipo stash pode meter mais stake e definir o controlador."; -"staking.controller.can.hint" = "O controlador pode retirar de stake, resgatar, repor em stake, alterar o destino da recompensa e validadores."; -"staking.switch.account.to.stash" = "Mude a sua conta para stash para definir o controlador."; -"staking.inactive.current.minimal.stake" = "O staking está inativo de momento. \n A participação mínima atual é %@."; -"staking.change.your.validators" = "Mude os seus validadores."; -"staking.bond.more.tokens" = "Faça stake de mais tokens."; -"staking.redeem.unbonded.tokens" = "Resgatar tokens sem stake."; -"staking.your.stake" = "O seu stake"; -"staking.controller.account.zero.balance" = "Descobrimos que esta conta não tem tokens gratuitos. Tem a certeza de que deseja alterar o controlador?"; -"wallet.send.balance.minimal" = "Saldo mínimo"; -"wallet.fee.over.existential.deposit" = "Você deve ter fundos para pagar a taxa e permanecer acima do saldo mínimo."; -"staking.your.allocated.description" = "O seu stake está alocado nos seguintes validadores."; -"staking.your.not.allocated.description" = "Outros, que estão ativos sem a sua alocação de stake."; -"staking.your.inactive.description" = "Validadores que não foram eleitos nesta era."; -"tabbar.crowdloan.title" = "Crowdloans"; -"crowdloan.list.section.format" = "Selecione uma campanha de Crowdloan para a qual deseje contribuir. Receberá recompensas se a campanha ganhar um slot de parachain no leilão."; -"crowdloan.you.contributions.title" = "As suas contribuições"; -"crowdloan.progress.format" = "Angariado: %@ de %@"; -"crowdloan.active.section.format" = "Ativo (%@)"; -"crowdloan.completed.section.format" = "Concluído (%@)"; -"crowdloan.empty.message" = "Informações sobre crowdloans \n aparecerão aqui"; -"common.time.left.format" = "%@ saiu"; +"common.change.password" = "Alterar senha"; +"common.changed" = "Alterado"; +"common.choose.action" = "Escolher ação"; +"common.choose.network" = "Escolher a rede"; +"common.close" = "Fechar"; +"common.commission" = "Commission"; +"common.confirm" = "Confirmar"; +"common.confirm.password" = "Confirmar senha"; +"common.confirm.title" = "Confirmação"; +"common.confirmation.title" = "Tem a certeza?"; +"common.confirmed" = "Confirmado"; +"common.connections" = "Conexões"; +"common.contacts" = "Contacts"; +"common.continue" = "Continuar"; +"common.copied" = "Copiado para a área de transferência"; +"common.copy" = "Copiar"; +"common.copy.address" = "Copiar endereço"; +"common.copy.id" = "Copiar id"; +"common.create" = "Criar"; +"common.crypto.type" = "Criptografia do tipo par de chaves"; +"common.currency" = "Moeda"; +"common.details" = "Detalhes"; +"common.disconnect" = "Desconectar"; +"common.done" = "Concluído"; +"common.edit" = "Editar"; +"common.error.general.title" = "Erro"; +"common.error.internal" = "Ocorreu um erro interno"; +"common.error.missing.crypto.type.message" = "Não é possível definir o tipo de crypto para esta carteira"; +"common.error.network" = "Ocorreu um erro durante a comunicação com o servidor remoto"; +"common.error.no.data.retrieved" = "Nenhuns dados recuperados."; +"common.error.password.mismatch" = "A senha não corresponde"; +"common.events" = "Eventos"; +"common.existential.error.message" = "Esta transação terá como resultado que a conta passe abaixo do Depósito Existencial(%@), o que fará com que ela seja colhida (a conta será apagada da blockchain para conservar espaço)."; +"common.existential.warning.max.amount" = "Set max amount"; "common.existential.warning.message" = "Esta transação terá como resultado que a conta passe abaixo do Depósito Existencial(%@), o que fará com que ela seja colhida (a conta será apagada da blockchain para conservar espaço). Se optar por continuar, perderá quaisquer fundos que estejam abaixo do montante do depósito existencial estabelecido pela rede. Para informações detalhadas, consulte a documentação oficial da rede (por exemplo, a Polkadot Wiki). A Fearless Wallet é uma aplicação totalmente não custodial, e não tem nenhum controlo ou conhecimento de qualquer das suas ações na própria rede. CONTINUE SÓ SE CONCORDAR PLENAMENTE E COMPREENDER AS IMPLICAÇÕES."; "common.existential.warning.title" = "A operação irá remover a conta"; -"crowdloan.ended.message" = "Não é possível contribuir para o crowdloan selecionado, porque já terminou."; -"crowdloan.ended.title" = "O crowdloan terminou"; +"common.expiry" = "Expiração"; +"common.export" = "Exportar conta"; +"common.filter.sort.header" = "Ordenar por"; +"common.important" = "Importante"; +"common.info" = "Info"; +"common.invalid.hard.message" = "Por favor, verifique se a entrada é uma mistura de //'hard' onde 'hard' pode ser qualquer sequência de caracteres não incluindo /."; +"common.invalid.hard.soft.message" = "Por favor, verifique se a entrada é uma mistura de //'hard' e /'soft' onde 'hard' e 'soft' podem ser qualquer sequência de caracteres não incluindo /."; +"common.invalid.hard.soft.numeric.message" = "Por favor, verifique se a entrada é uma mistura de //'hard' e /'soft' onde 'hard' e 'soft' podem ser qualquer sequência de caracteres não incluindo /."; +"common.invalid.hard.soft.numeric.password.message" = "Por favor, verifique se a entrada é uma mistura de //'hard' e /'soft' onde 'hard' e 'soft' podem ser qualquer sequência de caracteres não incluindo /. O caminho pode terminar com ///'password'. Por exemplo, //1/soramitsu///mypass."; +"common.invalid.path.title" = "Formato inválido"; +"common.invalid.path.with.soft.message" = "Por favor, verifique se a entrada é uma mistura de //'hard' e /'soft' onde 'hard' e 'soft' podem ser qualquer sequência de caracteres não incluindo /. O caminho pode terminar com ///'password'. Por exemplo, //1/soramitsu///mypass."; +"common.invalid.path.without.soft.message" = "Por favor, verifique se a entrada é uma mistura de //'hard' onde 'hard' pode ser qualquer sequência de caracteres não incluindo /. O caminho pode terminar com ///'password'. Por exemplo, //1//soramitsu///mypass."; +"common.keep.editing.action" = "Voltar à operação"; +"common.learn.more" = "Saiba mais"; +"common.max" = "max"; +"common.message" = "Mensagem"; +"common.methods" = "Métodos"; +"common.module" = "Módulo"; +"common.my.networks" = "My networks"; +"common.name" = "Nome"; +"common.network" = "Rede"; +"common.network.fee" = "Taxa de rede"; +"common.network.management" = "Gerência de rede"; +"common.next" = "Próximo"; +"common.no" = "Não"; +"common.no.screenshot.message" = "Não faça capturas de tela, que podem ser coletadas por malware de terceiros"; +"common.no.screenshot.title" = "Não faça capturas de tela"; +"common.not.available.short" = "N/A"; +"common.not.enough.balance.message" = "Desculpe, não tem fundos suficientes para gastar a quantia especificada"; +"common.not.enough.fee.message" = "Desculpe, não tem fundos suficientes para pagar a taxa da rede."; +"common.ok" = "OK"; +"common.paste" = "Colar"; +"common.payout" = "Pagamento"; +"common.preview" = "Visualização"; +"common.price" = "Price"; +"common.privacy.policy" = "Política de Privacidade"; +"common.proceed" = "Proceder"; +"common.referral.code.title" = "Código de referência"; +"common.reject" = "Rejeitar"; +"common.rejected" = "Rejeitado"; +"common.request" = "Solicitar"; +"common.reset" = "Reinicializar"; +"common.resolve" = "Resolver"; +"common.retry" = "Tentar novamente"; +"common.save" = "Guardar"; +"common.search" = "Pesquisa"; +"common.search.results.number" = "Resultados da pesquisa: %d"; +"common.search.start.title" = "Os resultados da pesquisa aparecerão aqui"; +"common.secret.derivation.path" = "Caminho de derivação secreto"; +"common.select" = "Selecione"; +"common.select" = "Selecione"; +"common.select.all" = "Selecionar tudo"; +"common.select.asset" = "Selecione o ativo"; +"common.select.network" = "Selecione a rede"; +"common.selected.count" = "Selecionado: %@"; +"common.set.password" = "Definir senha"; +"common.share" = "Compartilhar"; +"common.show" = "Mostrar"; +"common.sign" = "Assinar"; +"common.skip" = "Saltar"; +"common.staking" = "Staking"; +"common.start" = "Start"; +"common.terms.and.conditions" = "Termos e Condições"; +"common.till.date" = "até %s"; +"common.time.left" = "Tempo restante"; +"common.time.left.format" = "%@ saiu"; +"common.total" = "Total"; +"common.transaction.failed" = "Transação falhou. Por favor, tente novamente mais tarde."; +"common.transaction.raw.data" = "Dados brutos de transação"; +"common.transaction.submitted" = "Transação submetida"; +"common.undefined.error.message" = "Por favor, tente novamente com outro input. Se o erro aparecer novamente, por favor, contacte o apoio ao utilizador."; +"common.undefined.error.title" = "Erro indefinido"; +"common.unsupported" = "Não suportado"; +"common.unsupported.network.message" = "O tipo de rede ainda não é suportado. Por favor, escolha outro."; +"common.update" = "Atualizar"; +"common.use" = "Usar"; +"common.wallet" = "Carteira"; +"common.warning" = "Aviso"; +"common.warning.capitalized" = "Aviso:"; +"common.watch" = "Assistir"; +"common.yes" = "Sim"; +"confirm.mnemonic.mismatch.error.message" = "Por favor, verifique a ordem das palavras mais uma vez."; +"confirm.mnemonic.mismatch.error.message.2.0" = "Senha mnemónica inválida, verifique mais uma vez a ordem das palavras"; +"confirm.mnemonic.mismatch.error.title" = "Mnemónica inválida"; +"confirmation.skip.action" = "Saltar processo"; +"connect.details" = "Detalhes da conexão"; +"connection.add.already.exists.error" = "O node já foi adicionado anteriormente. Por favor, tente outro node."; +"connection.add.invalid.error" = "Não se consegue estabelecer ligação com o node. Por favor, tente outro."; +"connection.add.unsupported.error" = "Infelizmente, a rede não é suportada. Por favor, tente uma dos seguintes: %@ ."; +"connection.delete.confirm" = "Eliminar"; +"connection.delete.description" = "Confirme a exclusão de %@."; +"connection.delete.title" = "Apagar rede?"; +"connection.error.message" = "Por favor, verifique a ligação à Internet e tente novamente mais tarde."; +"connection.error.title" = "Falha na conexão"; +"connection.management.custom.title" = "Nodes personalizados"; +"connection.management.default.title" = "Nodes padrão"; +"connection.management.title" = "Redes"; +"connection.problems.text" = "Problemas de conexão"; +"connections.add.connection" = "Adicionar conexão"; +"contacts.contact.address" = "Endereço de contato"; +"contacts.contact.name" = "Nome de contato"; +"contacts.create.contact" = "Criar contato"; +"contacts.empty.message" = "No contacts found"; +"contacts.recent" = "Recente"; +"contacts.scan" = "Ler código QR"; +"contacts.undefined" = "Indefinido"; +"contribution.type.direct.dot" = "DOT direto"; +"contribution.type.lcdot" = "lcDOT"; +"controller.account.issue.action" = "Gerenciar conta do controlador"; +"controller.account.issue.message" = "Observe que o recurso de conta do Controlador foi descontinuado e, como resultado, você deve definir sua conta Stash como Controlador"; +"copy.referral.code" = "Copiar código de referência"; +"create.new.account" = "Criar uma nova conta"; +"create.new.connection" = "Criar nova conexão"; +"create.new.pincode" = "Criar novo código PIN"; +"crowdloan.active.section.format" = "Ativo (%@)"; +"crowdloan.app.bonus.format" = "Bónus da Fearless Wallet (%@)"; +"crowdloan.astar.referral.code.invalid" = "Endereço de referência inválido, apenas endereços da Polkadot são aceites, por favor tente novamente"; +"crowdloan.bonus.verification.error" = "A verificação da assinatura do bónus falhou. Por favor, tente novamente mais tarde."; "crowdloan.cap.reached.amount.message" = "Não pode contribuir com o valor escolhido, pois o valor arrecadado resultante excederá o limite do crowdloan. A contribuição máxima permitida é %@ ."; "crowdloan.cap.reached.raised.message" = "Não é possível contribuir para o crowdloan selecionado, pois seu limite já foi atingido."; "crowdloan.cap.reached.title" = "Limite de contribuições para o crowdloan excedido"; -"crowdloan.too.small.contribution.message" = "A quantia mínima permitida para contribuir é %@"; -"crowdloan.too.small.contribution.title" = "O montante da contribuição é demasiado pequeno"; -"crowdloan.learn" = "Aprenda sobre o crowdloan de %@"; -"common.time.left" = "Tempo restante"; -"crowdloan.placeholder" = "Os crowdloans ativos \n aparecerão aqui"; +"crowdloan.completed.section.format" = "Concluído (%@)"; +"crowdloan.confirmation.name" = "%@ Crowdloan"; "crowdloan.contribute.title" = "Contribuir para o Crowdloan"; -"crowdloan.unlock.hint" = "Os seus tokens de %@ serão devolvidos após o período de locação."; +"crowdloan.contribution.format" = "Você contribuiu: %@"; +"crowdloan.email.hint" = "Endereço de email"; +"crowdloan.empty.bonus.title" = "Obtenha um bónus especial"; +"crowdloan.empty.message" = "Informações sobre crowdloans \n aparecerão aqui"; +"crowdloan.ended.message" = "Não é possível contribuir para o crowdloan selecionado, porque já terminou."; +"crowdloan.ended.title" = "O crowdloan terminou"; "crowdloan.info" = "Informações do crowdloan"; -"crowdloan.raised.amount" = "%@ de %@"; -"crowdloan.raised" = "Angariado"; +"crowdloan.learn" = "Aprenda sobre o crowdloan de %@"; +"crowdloan.learn.bonuses" = "Saiba mais sobre os bónus da %@"; "crowdloan.leasing.period" = "Período de locação"; -"common.till.date" = "até %s"; +"crowdloan.list.section.format" = "Selecione uma campanha de Crowdloan para a qual deseje contribuir. Receberá recompensas se a campanha ganhar um slot de parachain no leilão."; +"crowdloan.placeholder" = "Os crowdloans ativos \n aparecerão aqui"; +"crowdloan.privacy.policy" = "Li e concordo com os Termos e Condições"; +"crowdloan.private.crowdloan.message" = "Crowdloans privados ainda não são suportados."; +"crowdloan.private.crowdloan.title" = "Crowdloan privado"; +"crowdloan.progress.format" = "Angariado: %@ de %@"; +"crowdloan.raised" = "Angariado"; +"crowdloan.raised.amount" = "%@ de %@"; +"crowdloan.referral.code.internal" = "A verificação do código de referência falhou. Por favor, tente novamente mais tarde."; +"crowdloan.referral.code.invalid" = "O código de referência é inválido. Por favor, tente com outro"; "crowdloan.reward" = "Recompensa estimada"; -"staking.bonded.inactive" = "Não está nomeando nem validando"; "crowdloan.terms.format" = "Li e concordo com %@"; "crowdloan.terms.value" = "Termos e Condições"; -"crowdloan.private.crowdloan.title" = "Crowdloan privado"; -"crowdloan.private.crowdloan.message" = "Crowdloans privados ainda não são suportados."; -"common.bonus" = "Bónus"; -"crowdloan.app.bonus.format" = "Bónus da Fearless Wallet (%@)"; -"common.referral.code.title" = "Código de referência"; -"karura.referral.code.action" = "Insira o seu código de referência"; -"staking.add.controller" = "Adicione conta de controlador %@ à aplicação para executar essa ação."; -"crowdloan.empty.bonus.title" = "Obtenha um bónus especial"; -"staking.select.validators.recommended.title" = "Fazer stake com os validadores recomendados"; -"staking.select.validators.custom.title" = "Fazer stake com os seus validadores"; -"common.applied" = "Aplicado"; -"karura.terms.action" = "Concordar com os Termos e Condições"; -"crowdloan.referral.code.invalid" = "O código de referência é inválido. Por favor, tente com outro"; -"crowdloan.referral.code.internal" = "A verificação do código de referência falhou. Por favor, tente novamente mais tarde."; -"crowdloan.bonus.verification.error" = "A verificação da assinatura do bónus falhou. Por favor, tente novamente mais tarde."; -"staking.select.validators.custom.desc" = "Deve confiar nas suas nomeações para agir de forma competente e honesta, baseando sua decisão puramente na sua rentabilidade atual pode levar a lucros reduzidos ou até mesmo perda de fundos."; -"staking.select.validators.recommended.button.title" = "Selecione o recomendado"; -"staking.select.validators.custom.button.title" = "Selecione você mesmo"; -"staking.filter.title.rewards" = "Recompensas (APY)"; -"staking.selected.validators.count_v1.9.1" = "selecionado %d (max %d)"; -"staking.select.validators.recommended.desc" = "O algoritmo da Fearless selecionou uma lista de validadores recomendados com base nos critérios:"; -"common.not.enough.fee.message" = "Desculpe, não tem fundos suficientes para pagar a taxa da rede."; -"common.not.enough.balance.message" = "Desculpe, não tem fundos suficientes para gastar a quantia especificada"; -"common.transaction.failed" = "Transação falhou. Por favor, tente novamente mais tarde."; -"staking.custom.validators.list.title" = "Selecione validadores"; -"staking.custom.fill.button.title" = "Preencher o restante com o recomendado"; -"staking.custom.clear.button.title" = "Limpar filtros"; -"staking.custom.deselect.button.title" = "Desselecionar todos"; -"staking.custom.header.validators.title" = "Validadores: %li de %li"; -"crowdloan.contribution.format" = "Você contribuiu: %@"; -"staking.set.validators.title" = "Selecione validadores para iniciar o stake"; -"staking.set.validators.message" = "Validadores não selecionados"; -"staking.max.nominators.reached.title" = "Não é possível começar a fazer staking"; -"staking.max.nominators.reached.message" = "O número máximo de nomeadores foi atingido"; -"staking.custom.blocked.warning" = "Este validador não está a aceitar nomeações de momento. Por favor, tente novamente na próxima era."; -"staking.alert.start.next.era.message" = "Por favor, aguarde pelo início da próxima era."; -"staking.custom.proceed.button.disabled.title" = "Selecionar validadores (max %d)"; -"staking.custom.proceed.button.enabled.title" = "Mostrar selecionados: %d (max %d)"; -"staking.filter.title.own.stake" = "Stake pertencente ao validador"; -"staking.filter.title.own.stake.token" = "Stake pertencente ao validador (%@)"; -"staking.custom.deselect.warning" = "Está prestes a desselecionar todos os validadores selecionados anteriormente."; -"common.search" = "Pesquisa"; -"staking.validator.search.placeholder" = "Pesquisar por endereço ou nome"; -"common.search.results.number" = "Resultados da pesquisa: %d"; -"common.search.start.title" = "Os resultados da pesquisa aparecerão aqui"; -"staking.validator.search.empty.title" = "Sem resultados de pesquisa.\nTenha a certeza de que digitou o endereço completo da conta"; -"common.filter.sort.header" = "Ordenar por"; -"staking.recommended.hint3.addition" = "com pelo menos um contacto de identidade"; -"staking.validator.total.stake.token" = "Stake total (%@)"; -"staking.validator.apy.percent" = "Recompensa estimada (% APY)"; -"staking.pending.rewards" = "Recompensas pendentes"; -"staking.rewards.destination.title" = "Destino das recompensas"; -"staking.your.oversubscribed.message" = "Os seus tokens estão alocados em validadores com excesso de assinaturas. Não receberá recompensas nesta era."; -"staking.your.not.elected.format" = "Não eleito (%@)"; -"staking.your.selected.format" = "Selecionado (%@)"; -"staking.your.elected.format" = "Eleito (%@)"; -"staking.common.rewards.apy" = "Recompensas (APY)"; -"staking.validator.status.elected" = "Eleito"; -"staking.validator.status.unelected" = "Não eleito"; -"staking.validator.slashed.desc" = "O Validador é penalizado por se comportar mal (por exemplo, ficar offline, atacar a rede ou executar softwares modificados) na rede."; -"staking.payout.expired" = "O pagamento expirou"; -"wallet.balance.unbonding_v1.9.0" = "Unstaking"; -"staking.unstaking.period" = "Período de Unstaking"; -"wallet.balance.bonded" = "Staked"; -"staking.total.rewards_v1.9.0" = "Recompensado"; -"staking.setup.reward.destination.section.title" = "Como utilizar as suas recompensas?"; -"staking.bond.more_v1.9.0" = "Fazer mais stake"; -"staking.unbond_v1.9.0" = "Unstake"; -"staking.unbonding.empty.list_v1.9.0" = "As transações de unstaking aparecerão aqui"; -"tabbar.crowdloan.title_v1.9.0" = "Crowdloans"; -"staking.validator.my.oversubscribed.message" = "Excesso de subscrições. Não receberá recompensas do validador nesta era."; -"staking.validator.other.oversubscribed.message" = "Excesso de subscrições. Apenas os nomeadores com maior valor de stake receberão recompensas."; -"wallet.history.title_v1.9.0" = "Histórico"; -"staking.main.lockup.period.title_v1.9.0" = "Período de desbloqueio"; -"staking.estimate.earning.title_v1.9.0" = "Ganhos estimados"; -"staking.main.stake.balance.staked" = "Staked"; -"staking.setup.restake.title" = "Restake"; -"staking.setup.payout.title" = "Pagamento"; -"staking.pending.rewards.explanation.message" = "Os validadores pagam as recompensas a cada 2 a 5 dias. No entanto, pode demandá-los sozinho, especialmente se as recompensas estão perto de expirar. Nesse caso precisará de pagar as taxas."; -"staking.custom.validators.update.list" = "Atualize a sua lista"; -"staking.hint.rewards.format" = "Os tokens em stake geram recompensas a cada época (%@)."; -"staking.hint.unstake.format" = "Se quiser fazer unstake, terá que esperar que período de unstake termine (%@)."; -"staking.hint.no.rewards" = "Os tokens em período de unstaking não geram recompensas."; -"staking.hint.redeem" = "Após o período de unstaking, precisará de resgatar os seus tokens."; -"staking.hint.reward.bond.more" = "As suas recompensas aumentarão a partir da próxima era."; -"staking.hint.unbond.kills.stash" = "A Fearless wallet mudará o destino das recompensas \ -para a sua conta para evitar o staking do restante."; -"crowdloan.privacy.policy" = "Li e concordo com os Termos e Condições"; -"contacts.scan" = "Ler código QR"; -"staking.story.staking.title" = "O que é staking?"; -"staking.story.staking.page.1" = "O staking é uma opção para obter rendimento passivo ao bloquear os seus tokens na rede. As recompensas de staking são alocadas a cada época (6 horas na Kusama e 24 horas na Polkadot). Pode fazer stake no prazo que quiser, e para desbloquear os seus tokens, precisa de esperar pelo fim do período de desbloqueamento, tornando os seus tokens disponíveis para serem resgatados."; -"staking.story.staking.page.2" = "O staking é uma parte importante da segurança e fiabilidade da rede. Qualquer pessoa pode executar nodes de validador, mas apenas aqueles que possuem tokens suficientes em stake serão eleitos pela rede para participar da composição de novos blocos e receber as recompensas. Os validadores muitas vezes não têm tokens suficientes, então são os nomeadores que os estão a ajudar, ao bloquear os seus tokens para que alcancem a quantia de stake necessária."; -"staking.story.nominator.title" = "Quem é um validador?"; -"staking.story.nominator.page.1" = "Os nomeadores obtêm um rendimento passivo por bloquearem os seus tokens para proteger a rede. Para tal, o nomeador deve selecionar um número de validadores para apoiar. O nomeador deve ter cuidado ao selecionar validadores. Se o validador selecionado não se comportar corretamente, as penalidades seriam aplicadas a ambos, com base na gravidade do incidente."; -"staking.story.nominator.page.2" = "A Fearless Wallet fornece apoio para os nomeadores, ajudando-os a selecionar os validadores. A aplicação móvel busca dados da blockchain e compõe uma lista de validadores, que possuem: maior lucro, identidade com informações de contacto, e disponibilidade para receber nomeações. A Fearless Wallet também se preocupa com a descentralização, portanto, se uma pessoa ou uma empresa executar vários nodes de validação, apenas até 2 nodes serão mostrados na lista recomendada."; -"staking.story.validator.title" = "Quem é um validador?"; -"staking.story.validator.page.1" = "O validador corre um node da blockchain 24 horas por dia, 7 dias por semana e é necessário ter um stake bloqueado suficientemente grande (pertencente e fornecido pelos nomeadores) para ser eleito pela rede. Os validadores devem manter o desempenho e a fiabilidade dos seus nodes para serem recompensados. Ser validador é quase um trabalho a tempo inteiro, há empresas que estão focadas em ser validadoras nas redes blockchain."; -"staking.story.validator.page.2" = "Qualquer pessoa pode ser validador e correr um node da blockchain, porém isso requer um certo nível de habilidade técnica e responsabilidade. As redes Polkadot e Kusama têm um programa, denominado Thousand Validators Program, para fornecer apoio a iniciantes. Além disso, a própria rede sempre recompensará mais validadores, que têm menos participação (mas o suficiente para serem eleitos) para melhorar a descentralização."; -"staking.story.reward.title" = "Recompensas recebidas"; -"staking.story.reward.page.1" = "As recompensas do staking estão disponíveis para pagamento no final de cada era (6 horas na Kusama e 24 horas na Polkadot). A rede armazena recompensas pendentes durante 84 eras e, na maioria dos casos, os validadores pagam as recompensas para todos. No entanto, os validadores podem-se esquecer ou algo pode acontecer, por isso os nomeadores também podem pagar as suas recompensas por si próprios."; -"staking.story.reward.page.2" = "Embora as recompensas sejam geralmente distribuídas por validadores, a Fearless Wallet ajuda alertando se há recompensas pendentes que estão perto de expirar. Receberá alertas sobre esta e outras atividades no separador de staking"; -"common.cancel.operation.message" = "Tem certeza que deseja cancelar esta operação?"; -"common.cancel.operation.action" = "Cancelar operação"; -"common.keep.editing.action" = "Voltar à operação"; +"crowdloan.too.small.contribution.message" = "A quantia mínima permitida para contribuir é %@"; +"crowdloan.too.small.contribution.title" = "O montante da contribuição é demasiado pequeno"; +"crowdloan.unlock.hint" = "Os seus tokens de %@ serão devolvidos após o período de locação."; +"crowdloan.you.contributions.title" = "As suas contribuições"; +"custom.collators.text" = "Você deve confiar que seus coletores agirão com competência e honestidade; basear sua decisão puramente na lucratividade atual pode levar à redução dos lucros ou até mesmo à perda de fundos."; +"custom.collators.title" = "Stake com coletores conhecidos"; +"custom.validators.empty.message" = "Nenhum validador encontrado. Por favor, tente alterar os filtros"; +"default.account.shared.secret" = "Contas padrão com um segredo partilhado"; +"delete.custom.node.title" = "Eliminar o node personalizado?"; +"ecdsa.selection.subtitle" = "(compatível com BTC/ETH)"; +"ecdsa.selection.title" = "ECDSA"; +"ed25519.selection.subtitle" = "ed25519 (alternativa)"; +"ed25519.selection.title" = "Edwards"; +"empty.state.message" = "Nada encontrado para sua solicitação"; +"empty.view.description" = "Nenhuma rede ou ativo foi encontrado :("; +"empty.view.title" = "Sumimasen!"; +"error.invalid.address" = "Endereço inválido para a rede selecionada"; +"error.message.enter.the.name" = "Introduza o nome..."; +"error.message.enter.the.url.address" = "Digite o endereço URL..."; +"error.scan.qr.disabled.asset" = "The asset you want to transfer is either unsupported or turned off. Go to Asset Management, activate the asset, and then scan the QR code again."; +"error.unsupported.asset" = "Você está tentando fazer uma transferência do ativo que atualmente não é compatível com o aplicativo. Escolha outro ativo ou solicite outro código QR."; +"ethereum.crypto.type" = "Criptografia de par de chaves do tipo Ethereum"; +"ethereum.secret.derivation.path" = "Caminho de derivação secreto da Ethereum"; +"example" = "Exemplo: %s"; +"existential.deposit.received.error" = "Depósito existencial não recebido"; +"export.ethereum.title" = "Exportar para Ethereum"; +"export.mnemonic.hint" = "Use meios não digitais para fazer backup, como escrever a sequência de palavras mnemónicas e o caminho de derivação (se definido) em papel."; +"export.mnemonic.with.dp.template" = "Rede: %@\ +Mnemónica: %@\ +Caminho de derivação: %@"; +"export.mnemonic.without.dp.template" = "Rede: %@\ +Mnemónica: %@"; +"export.seed.with.dp.template" = "Rede: %@\ +Seed: %@\ +Caminho de derivação: %@"; +"export.seed.without.dp.template" = "Rede: %@\ +Seed: %@"; +"export.substrate.title" = "Exportar para Substrate"; +"export.wallet" = "Exportar carteira"; +"export.wallet.chains.count" = "+%d OUTRAS"; +"fee.not.yet.loaded.message" = "Por favor aguarde até que a taxa seja calculada"; +"fee.not.yet.loaded.title" = "Cálculo das taxas em curso"; +"google.backup.button.title" = "Conecte-se com o Google"; +"google.backup.choice.google" = "Importar do Google"; +"google.backup.choice.json" = "JSON"; +"google.backup.choice.mnemonic" = "Frase mnemônica"; +"google.backup.choice.raw" = "Semente Crua"; +"google.backup.choice.title" = "Selecione a fonte para importação"; +"help.support.title" = "Apoio da Fearless"; +"hidden.assets" = "Ativos ocultos"; +"history.empty.description" = "Você não tem nenhuma transação aqui"; +"i.dont.need.account" = "Não preciso de uma conta"; +"identity.email.title" = "Email"; +"identity.legal.name.title" = "Designação legal"; +"identity.riot.name.title" = "Nome do elemento"; +"identity.title" = "Identidade"; +"identity.twitter.title" = "Twitter"; +"identity.web.title" = "Web"; +"import.account.exists.title" = "A conta já existe"; +"import.empty.derivation.cancel" = "Zero"; +"import.empty.derivation.confirm" = "Vazio"; +"import.empty.derivation.message" = "Existe a opção de estabelecer um caminho zero (//00///00//0/0/0/0), ou de colocar um caminho de derivação vazio. Se escolher um caminho de derivação vazio, não será capaz de exportar a sua conta. Escolha, por favor, qual o caminho de derivação que gostaria de utilizar"; +"import.eth.json.invalid.import.type.message" = "Seu arquivo JSON não é válido. Você está tentando importar contas em cadeia baseadas em Substrate em vez de contas ETH. Use um arquivo JSON adequado para importar contas da cadeia ETH."; +"import.ethereum.recovery.json" = "Restaurar o JSON para contas baseadas em ETH"; +"import.json.invalid.format.message" = "Por favor, certifique-se de que a sua entrada contém um JSON válido."; +"import.json.invalid.format.title" = "O restauro do JSON é inválido"; +"import.json.invalid.import.type.message" = "Seu arquivo JSON não é válido. Você está tentando importar contas em cadeia baseadas em ETH em vez de contas do Substrate. Use um arquivo JSON adequado para importar contas da cadeia do Substrate primeiro."; +"import.mnemonic" = "Frase Mnemónica"; "import.mnemonic.invalid.title" = "A sua chave mnemónica não é valida"; -"staking.your.validator.title" = "O seu validador"; -"account.error.try.another.one" = "Por favor, tente outro."; -"staking.analytics.title" = "Analíticos"; -"staking.rewards.title" = "Recompensas"; -"staking.analytics.activity" = "Atividade"; -"staking.analytics.stake.allocation" = "Alocação com validadores atuais"; -"wallet.extrinsic.details.title" = "Detalhes extrínsecos"; -"transaction.details.extrinsic.fee" = "Taxa"; -"staking.common.era" = "Era"; -"staking.common.validator" = "Validador"; -"staking.common.event.id" = "Evento"; -"common.copy.id" = "Copiar id"; -"wallet.contacts.search.placeholder_v1.10" = "Pesquisar por endereço ou nome"; -"wallet.contacts.empty.title_v1.10" = "Suas contas e contatos\ -para quem estava enviando\ -as transferências aparecerão aqui"; -"wallet.search.empty.title_v1.10.0" = "O formato do endereço é inválido.\ -Certifique-se de que o endereço\ -pertence à rede certa"; -"staking.analytics.period.7d" = "7dias"; -"staking.analytics.period.30d" = "30dias"; -"staking.analytics.period.1y" = "1ano"; -"staking.analytics.period.all" = "Tudo"; -"staking.analytics.rewards.empty.message" = "Sem recompensas recebidas\ -para o período selecionado"; -"staking.analytics.stake.empty.message" = "As suas alterações de stake\ -aparecerão aqui"; -"common.details" = "Detalhes"; -"staking.analytics.details.type" = "Tipo"; -"staking.analytics.era.range" = "%d de %d eras"; -"staking.analytics.7days.rewards" = "Recompensas de 7 dias"; -"staking.analytics.avg" = "%@ média"; -"staking.analytics.staking.was.active" = "O staking estava ativo"; -"staking.analytics.staking.was.inactive" = "O staking estava inativo"; -"common.choose.action" = "Escolher ação"; -"wallet.account.locks.vesting" = "Vesting"; -"wallet.account.locks.democracy" = "Democracia"; -"username.setup.choose.title" = "Nome da carteira"; -"common.select.network" = "Selecione a rede"; -"common.select.asset" = "Selecione o ativo"; -"parachain.crowdloans" = "Crowdloans de Parachains"; -"crowdloan.confirmation.name" = "%@ Crowdloan"; -"apply.fearless.wallet.bonus" = "Tocar para aplicar o bónus da Fearless"; -"applied.fearless.wallet.bonus" = "Bónus da carteira Fearless aplicado"; -"crowdloan.learn.bonuses" = "Saiba mais sobre os bónus da %@"; -"crowdloan.astar.referral.code.invalid" = "Endereço de referência inválido, apenas endereços da Polkadot são aceites, por favor tente novamente"; -"astar.bonus" = "Bónus para si"; -"astar.friend.bonus" = "Bónus para o seu amigo"; +"import.raw.seed" = "Seed primária"; +"import.recovery.json" = "Restaurar o JSON"; +"import.seed.invalid.message" = "Por favor, certifique-se de que a sua entrada contém 64 símbolos hexadecimais."; +"import.seed.invalid.title" = "A seed é inválida"; +"import.source.picker.title" = "Tipo de fonte"; +"import.substrate.recovery.json" = "Restaurar o JSON para contas de Substrate"; +"import.wallet" = "Importar carteira"; +"import.wallets.not.found" = "Nenhuma carteira de importação foi encontrada :("; +"index.common" = "Índice"; +"json.export.file.title" = "Exportar para ficheiro"; +"json.export.text.title" = "Exportar como texto"; +"karura.referral.code.action" = "Insira o seu código de referência"; +"karura.terms.action" = "Concordar com os Termos e Condições"; +"label.testnet" = "Rede de Testes"; +"language.title" = "Idioma"; +"learn.more.about.crowdloans" = "Saiba mais sobre os Crowdloans"; +"lp.apy.alert.text" = "Farming reward for liquidity provision"; +"lp.apy.alert.title" = "Strategic Bonus APY"; +"lp.apy.title" = "Strategic Bonus APY"; +"lp.available.pools.title" = "Available pools"; +"lp.confirm.liquidity.screen.title" = "Confirm Liquidity"; +"lp.confirm.liquidity.warning.text" = "Output is estimated. If the price changes more than 0.5% your transaction will revert."; +"lp.network.fee.alert.text" = "Network fee is used to ensure SORA system’s growth and stable performance."; +"lp.network.fee.alert.title" = "Network fee"; +"lp.pool.details.title" = "Pool details"; +"lp.pool.remove.warning.text" = "Removing pool tokens converts your position back into underlying tokens at the current rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive."; +"lp.pool.remove.warning.title" = "NOTE"; +"lp.remove.button.title" = "Remove Liquidity"; +"lp.remove.liquidity.screen.title" = "Remove Liquidity"; +"lp.reward.token.text" = "Earn %@"; +"lp.reward.token.title" = "Rewards Payout In"; +"lp.slippage.title" = "Slippage"; +"lp.supply.button.title" = "Supply Liquidity"; +"lp.supply.liquidity.screen.title" = "Supply Liquidity"; +"lp.token.pooled.text" = "Your %@ Pooled"; +"lp.user.pools.title" = "User pools"; +"manage.assets.account.missing.text" = "Acrescentar uma conta..."; +"manage.assets.search.hint" = "Pesquisa por token ou rede"; +"members.common" = "Membros"; +"min.staking.calculating.text" = "Calculando o montante mínimo para o ativo nomeado. Por favor, tente novamente em poucos segundos."; +"min.staking.warning.text" = "O montante vinculado é inferior ao atual montante mínimo ativo indicado de %s\ +e, dependendo do estado da rede, podem não ser selecionados para participar"; +"missing.account.skip" = "Não preciso de uma conta"; +"mnemonic.error.try.another.one" = "Por favor, tente outra."; "moonbeam.crowdloan.terms" = "Termos e Condições do Crowdloan da Moonbeam"; +"moonbeam.ethereum.address.incorrect" = "O endereço Moonbeam em formato Ethereum está incorreto, por favor tente novamente"; +"moonbeam.ethereum.reward" = "As recompensas serão pagas para este endereço. Poderá utilizar um endereço Ethereum existente. POR FAVOR, ASSEGURE-SE DE QUE POSSUI AS CHAVES PRIVADAS PARA ESTE ENDEREÇO."; +"moonbeam.location.unsupported.error" = "Infelizmente, parece que pode não estar qualificado para participar da Campanha de Crowdloan da Moonbeam devido à sua localização detetada."; "moonbeam.registration" = "Registo do Crowdloan da Moonbeam"; "moonbeam.registration.description" = "Esta transação confirma que você concorda com os Termos e Condições do Crowdloan da Moonbeam.\ Uma transação extrínseca assinada system.remark será enviada como prova de seu acordo.\ Estas informações serão processadas e armazenadas na Polkadot Relay Chain.\ Note que esta transação de assinatura estará sujeita a taxas de transação na rede Polkadot."; -"transaction.successful" = "Transação bem sucedida!"; +"moonbeam.registration.description.system.remark" = "system.remark"; "moonbeam.signed.successful" = "Assinou com sucesso os termos e condições do Crowdloan da Moonbeam e foi enviada uma transação.\ \ Este é o hash da sua transação:"; "moonbean.ethereum.address" = "Endereço da Moonbeam do tipo Ethereum"; -"about.twitter" = "Seguir no Twitter"; -"about.youtube" = "Subscrever no YouTube"; -"about.medium" = "Ler no Medium"; -"about.announcement" = "Receber Alertas"; -"about.support" = "Solicitar Apoio"; -"common.invalid.hard.soft.numeric.message" = "Por favor, verifique se a entrada é uma mistura de //'hard' e /'soft' onde 'hard' e 'soft' podem ser qualquer sequência de caracteres não incluindo /."; -"common.invalid.hard.soft.numeric.password.message" = "Por favor, verifique se a entrada é uma mistura de //'hard' e /'soft' onde 'hard' e 'soft' podem ser qualquer sequência de caracteres não incluindo /. O caminho pode terminar com ///'password'. Por exemplo, //1/soramitsu///mypass."; -"wallet.asset.receive.template" = "Receber %s"; -"username.setup.hint.2.0" = "Exemplo: Poupanças, Investimentos, Crowdloans, Staking. Este nome será exibido apenas para você e armazenado localmente em seu dispositivo móvel."; -"username.setup.title.2.0" = "Criar nova carteira"; -"pincode.confirm.pin.code" = "Confirmar código PIN"; -"substrate.crypto.type" = "Criptografia de par de chaves do tipo Substrate"; -"ethereum.crypto.type" = "Criptografia de par de chaves do tipo Ethereum"; -"substrate.secret.derivation.path" = "Caminho de derivação secreto da Substrate\ -"; -"ethereum.secret.derivation.path" = "Caminho de derivação secreto da Ethereum"; -"example" = "Exemplo: %s"; -"pincode.setup.top.title" = "Configurar código PIN"; -"confirm.mnemonic.mismatch.error.message.2.0" = "Senha mnemónica inválida, verifique mais uma vez a ordem das palavras"; -"share.referral.code" = "Partilhar código de referência"; -"copy.referral.code" = "Copiar código de referência"; -"scan.qr.title" = "Ler QR"; -"scan.qr.subtitle" = "Leia o código do destinatário."; -"scan.qr.upload.button.title" = "Carregar da galeria"; -"wallet.receive.qr.description" = "Mostre este código QR ao remetente"; -"common.error.network" = "Ocorreu um erro durante a comunicação com o servidor remoto"; -"common.error.internal" = "Ocorreu um erro interno"; -"moonbeam.location.unsupported.error" = "Infelizmente, parece que pode não estar qualificado para participar da Campanha de Crowdloan da Moonbeam devido à sua localização detetada."; -"tabbar.crowdloan.attention" = "Verifique o separador Crowdloans \n Atenção necessária"; -"common.max" = "max"; -"wallet.receive.navigation.title" = "Receba %s"; -"assetdetails.balance.total" = "Total"; -"assetdetails.balance.transferable" = "Transferível"; -"assetdetails.balance.locked" = "Bloqueado"; -"assetdetails.balance.title" = "O seu saldo"; -"wallet.send.navigation.title" = "Enviar %s"; -"profile.wallets.title" = "Carteiras"; -"settings.add.wallet" = "Adicionar carteira"; -"onboarding.create.wallet" = "Criar carteira"; -"onboarding.restore.wallet" = "Já tenho uma carteira"; -"import.wallet" = "Importar carteira"; -"about.crowdloans" = "Saiba mais sobre Crowdloans"; -"wallet.asset.your.balance" = "O seu saldo"; -"wallet.asset.transferable.caption" = "Transferível"; -"about.wiki" = "Aprender na Wiki"; -"about.instagram" = "Gostar no Instagram"; -"switch.node" = "Alternar node"; -"wallet.transaction.detail.copy.id" = "Copiar ID"; -"wallet.transaction.history.filter.title" = "Mostrar"; -"view.in" = "Ver em %s"; -"wallet.transaction.history.unsupported.message" = "O histórico das transações para esta rede ainda não é suportado"; -"wallet.transaction.history.empty.message" = "O histórico de transações aparecerá aqui"; -"account.option" = "Opção de conta"; -"switch.node.autoselect.title" = "Seleção automática de nodes"; -"add.node.button.title" = "Adicionar node"; -"profile.logout.title" = "Sair"; -"profile.logout.description" = "Esta ação resultará na eliminação de todas as contas deste dispositivo. Certifique-se de que fez o backup da sua frase-chave antes de prosseguir."; -"delete.custom.node.title" = "Eliminar o node personalizado?"; +"navigation.title.crowdloan" = "Crowdloans de Parachains"; +"network.info.address" = "Endereço do node"; +"network.info.name" = "Nome do node"; +"network.info.title" = "Informação do node"; +"network.issue.main" = "Connection Error: Unable to connect to the network. Please try again."; +"network.issue.network.unavailible" = "A rede não está disponível"; +"network.issue.node.unavailable" = "O node está indisponível"; +"network.issue.notofication" = "Notificação"; +"network.issue.stub" = "Problemas de rede"; +"network.issue.unavailable" = "A rede não está disponível, você pode aguardar ou solicitar suporte da comunidade"; +"network.issues.empty.state.title" = "Sem problemas de rede"; +"network.issues.hide.action.title" = "Não me mostre novamente"; +"network.issues.resolve.option.title" = "Opção de resolução"; +"network.management.popular" = "Popular"; +"network.managment.favourite" = "Favorito"; +"network.status.connected" = "Conectado"; +"network.status.connecting" = "Conectando..."; "network.url.address" = "Endereço URL"; -"error.message.enter.the.name" = "Introduza o nome..."; -"error.message.enter.the.url.address" = "Digite o endereço URL..."; +"nft.choose.recipient.title" = "Choose recipient"; +"nft.collection.available.nfts" = "Available NFTs in the %s collection"; +"nft.collection.my.nfts" = "My NFTs"; +"nft.collection.title" = "Coleção"; +"nft.creator.title" = "Creator"; +"nft.list.empty.message" = "There aren't any NFTs yet. Buy or mint NFTs to see them here."; +"nft.owner.title" = "Possuído"; +"nft.share.address" = "My public address to receive: %s"; +"nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; +"nft.stub.text" = "NFTs chegarão em breve"; +"nft.stub.title" = "Sumimasen!"; +"nft.tokenid.title" = "ID do token"; +"nfts.collection.count" = "Quantity: %d/%d"; +"nfts.filters.airdrop" = "AirDrop"; +"nfts.filters.spam" = "SPAM"; +"nfts.filters.title" = "Hide NFTs"; +"nfts.stub" = "NFTs"; +"no.access.to.google" = "Sem acesso ao Google"; +"no.account.found" = "Nenhuma conta encontrada"; +"no.email.bound.error.message" = "Por favor, certifique-se de que uma aplicação de e-mail está instalada no dispositivo."; "node" = "Node"; -"wallet.transaction.history.error.message" = "O serviço está atualmente indisponível"; "node.selection.delete.node.title" = "Eliminar o node personalizado?"; -"wallet.settings" = "Configurações da carteira"; -"view.wallet" = "Ver carteira"; -"export.wallet" = "Exportar carteira"; -"select.validators.disclaimer" = "RESPONSABILIDADE: As sugestões de validador algorítmico não constituem uma consulta ou conselho financeiro. O staking é uma atividade de alto risco, e as sugestões de validador algorítmico não mitigam necessariamente este risco. Um validador sugerido pelo algoritmo pode ainda ser penalizado. Um validador sugerido pelo algoritmo pode também alterar os seus parâmetros (por exemplo, taxas de comissão, etc.) em qualquer altura depois de ter sido sugerido e/ou selecionado. Pode perder tokens ou recompensas por estas, ou outras razões. Apenas faça stake de tokens e utilize as sugestões do validador ao seu próprio critério, após ter realizado a devida diligência e considerado cuidadosamente os riscos envolvidos."; -"accounts.with.one.key" = "Contas com uma chave"; -"accounts.with.changed.key" = "Contas com chave alterada"; -"select.save.type" = "Selecione o tipo de guardar"; -"accounts.for.export" = "Contas para exportação"; -"what.accounts.for.export" = "Que contas na carteira pretende exportar?"; -"replace.account" = "Substituir conta"; -"wallet.manage.assets" = "Gerir ativos"; -"moonbeam.ethereum.address.incorrect" = "O endereço Moonbeam em formato Ethereum está incorreto, por favor tente novamente"; -"moonbeam.ethereum.reward" = "As recompensas serão pagas para este endereço. Poderá utilizar um endereço Ethereum existente. POR FAVOR, ASSEGURE-SE DE QUE POSSUI AS CHAVES PRIVADAS PARA ESTE ENDEREÇO."; -"manage.assets.search.hint" = "Pesquisa por token ou rede"; -"crowdloan.email.hint" = "Endereço de email"; -"replace.account.template" = "Substituir %s conta"; -"create.new.account" = "Criar uma nova conta"; -"already.have.account" = "Já tenho uma conta"; -"json.export.file.title" = "Exportar para ficheiro"; -"json.export.text.title" = "Exportar como texto"; -"app.version.unsupported.text" = "Já não apoiamos esta versão da aplicação. Por favor, atualize-na."; -"chain.unsupported.text" = "Esta chain não está disponível na versão atual da aplicação. Por favor, atualize-a para a versão mais recente."; -"app.version.json.loading.failed" = "Falha no carregamento dos dados necessários para o lançamento do pedido. Por favor, verifique a sua ligação à Internet e tente novamente."; -"about.official.website" = "Website Oficial"; -"about.github.source.code" = "Código Fonte no Github"; -"about.follow.on.twitter" = "Seguir no Twitter"; -"about.subscribe.on.youTube" = "Subscrever no YouTube"; -"about.like.on.instagram" = "Gostar no Instagram"; -"about.read.on.medium" = "Ler no Medium"; -"about.learn.on.wiki" = "Aprender na Wiki"; -"about.join.on.telegram" = "Juntar-se no Telegram"; -"about.receive.announcements" = "Receber Anúncios"; -"about.ask.for.support" = "Pedir Apoio"; -"about.contact.email" = "Email de Contacto"; -"about.terms.and.conditions" = "Termos e Condições"; -"about.privacy.policy" = "Política de Privacidade"; -"update.needed.text" = "Atualização necessária"; -"connection.problems.text" = "Problemas de conexão"; -"learn.more.about.crowdloans" = "Saiba mais sobre os Crowdloans"; -"navigation.title.crowdloan" = "Crowdloans de Parachains"; -"common.yes" = "Sim"; -"common.no" = "Não"; -"alert.add.ethereum.title" = "Contas ETH"; -"alert.add.ethereum.message" = "Adicionar contas Moonbeam e Moonriver?"; -"account.import.substrate.raw.seed.placeholder" = "A seed original para contas da Substrate (64 símbolos hexadecimais)"; -"account.import.ethereum.raw.seed.placeholder" = "A seed original para contas da ETH (64 símbolos hexadecimais)"; -"import.substrate.recovery.json" = "Restaurar o JSON para contas de Substrate"; -"import.ethereum.recovery.json" = "Restaurar o JSON para contas baseadas em ETH"; -"common.unsupported" = "Não suportado"; -"min.staking.warning.text" = "O montante vinculado é inferior ao atual montante mínimo ativo indicado de %s\ -e, dependendo do estado da rede, podem não ser selecionados para participar"; -"staking.inactive.bond" = "Vínculo Inativo"; -"min.staking.calculating.text" = "Calculando o montante mínimo para o ativo nomeado. Por favor, tente novamente em poucos segundos."; -"manage.assets.account.missing.text" = "Acrescentar uma conta..."; -"stories.version2.slide1.title" = "Mais redes, Mais tokens"; -"stories.version2.slide2.title" = "Contas atualizadas para Carteiras"; -"stories.version2.slide3.title" = "A carteira é agora mais poderosa"; -"stories.version2.slide4.title" = "Personalize as suas carteiras"; -"stories.version2.slide5.title" = "Harmonia com o ecossistema"; -"stories.version2.slide1.subtitle" = "A Fearless Wallet suporta agora mais redes e tokens: Polkadot (DOT), Kusama (KSM), Moonriver (MOVR), Karura (KAR), Shiden (SDN), SORA (XOR), Bifrost (BNC), KILT (KILT) e outros..."; -"stories.version2.slide2.subtitle" = "Todas as suas contas permanecem na aplicação! Cada carteira contém agora muitas contas em redes diferentes. Cada carteira tem um ícone novo e colorido para o ajudar a diferenciar-se entre elas."; -"stories.version2.slide3.subtitle" = "Diferentes tokens do ecossistema Polkadot e Kusama estão agora no mesmo ecrã."; -"stories.version2.slide4.subtitle" = "Por defeito, as carteiras utilizam a mesma chave para todas as contas contidas. É possível substituir as contas dentro de uma carteira por quaisquer contas existentes ou criar novas contas."; -"stories.version2.slide5.subtitle" = "Pode ainda importar e exportar as suas contas a partir de outras aplicações no ecossistema.\ -\ -Lembre-se de fazer uma cópia de segurança da sua chave e guardá-la num local seguro e privado (por exemplo, num pedaço de papel)."; -"stories.bottom.close.button" = "Boas notícias!"; -"export.substrate.title" = "Exportar para Substrate"; -"export.ethereum.title" = "Exportar para Ethereum"; -"account.template" = "%s conta"; -"i.dont.need.account" = "Não preciso de uma conta"; -"export.wallet.chains.count" = "+%d OUTRAS"; -"import.empty.derivation.message" = "Existe a opção de estabelecer um caminho zero (//00///00//0/0/0/0), ou de colocar um caminho de derivação vazio. Se escolher um caminho de derivação vazio, não será capaz de exportar a sua conta. Escolha, por favor, qual o caminho de derivação que gostaria de utilizar"; -"import.empty.derivation.confirm" = "Vazio"; -"import.empty.derivation.cancel" = "Zero"; -"contribution.type.direct.dot" = "DOT direto"; -"contribution.type.lcdot" = "lcDOT"; -"common.currency" = "Moeda"; -"label.testnet" = "Rede de Testes"; -"identity.twitter.title" = "Twitter"; -"moonbeam.registration.description.system.remark" = "system.remark"; -"default.account.shared.secret" = "Contas padrão com um segredo partilhado"; -"account.unique.secret" = "Contas com segredos exclusivos"; -"account.import.recovery.select.file" = "Selecionar ficheiro"; -"account.create.error.missing.crypto.type.title" = "Tipo de crypto ausente"; -"common.error.missing.crypto.type.message" = "Não é possível definir o tipo de crypto para esta carteira"; -"no.account.found" = "Nenhuma conta encontrada"; -"missing.account.skip" = "Não preciso de uma conta"; -"create.new.pincode" = "Criar novo código PIN"; -"staking.collators" = "Validadores"; -"staking.start.change.collators.suggested.title" = "Fazer stake com validadores sugeridos pelo algoritmo"; -"staking.start.change.collators.suggested.subtitle" = "O algoritmo da FEARLESS selecionou uma lista de validadores recomendados com base nos seguintes critérios:"; -"staking.select.suggested" = "Selecionar sugeridos"; -"staking.start.change.collators.custom.title" = "Fazer stake com os seus validadores"; -"select.collators.warning" = "AVISO: Os detentores de tokens MOVR/GLMR devem realizar a cuidadosa devida diligência acerca dos validadores antes de delegar. Ser listado como um validador não representa um endosso ou recomendação da Moonbeam Network, da Moonriver Network ou da Moonbeam Foundation. A Moonbeam Network, a Moonriver Network e a Moonbeam Foundation não examinaram os validadores da lista e não assumem nenhuma responsabilidade com relação à seleção, desempenho, segurança, precisão ou uso de quaisquer ofertas de terceiros. Você é o único responsável por fazer a sua própria diligência para entender as taxas aplicáveis e todos os riscos presentes, incluindo o monitoramento ativo da atividade dos seus validadores."; -"staking.suggested.collators.title" = "Validadores sugeridos"; -"wallet.send.tip.title" = "Gorjeta"; -"staking.selected.collator" = "Coletor selecionado"; -"parachain.staking.minimum.bond" = "Fiança mínima"; -"parachain.staking.self.bonded" = "Auto-afiançado"; -"parachain.staking.effective.amount.bonded" = "Valor efetivo garantido"; -"chain.selection.all.networks" = "Todas as redes"; -"chain.selection.hide.zero.balances" = "Ocultar com saldo vazio"; -"parachain.staking.ready.for.revoking" = "Pronto para revogar"; -"parachain.staking.stake.less" = "Diminuir Stake"; -"parachain.staking.revoke" = "Revogar"; +"onboarding.create.account" = "Criar conta"; +"onboarding.create.wallet" = "Criar carteira"; +"onboarding.preinstalled.wallet.button.text" = "Obtenha uma carteira pré-instalada"; +"onboarding.restore.account" = "Importar conta"; +"onboarding.restore.wallet" = "Já tenho uma carteira"; +"onboarding.start.title" = "A carteira DeFi do futuro"; +"onboarding.terms.and.conditions.1" = "Li e concordei com\ +os Termos, Condições e a Política de Privacidade"; +"operation.error.message" = "Por favor, tente novamente mais tarde."; +"operation.error.title" = "A operação falhou"; +"optional.networks" = "Redes opcionais"; +"options.common" = "Opções"; +"parachain.crowdloans" = "Crowdloans de Parachains"; "parachain.staking.collator" = "Coletor"; -"staking.your.collator.title" = "Seu coletor"; -"parachain.staking.unlock" = "Desbloquear"; -"parachain.staking.filters.effective.amount.bonded" = "Valor efetivo garantido ( %@ )"; +"parachain.staking.delegate" = "Delegar"; +"parachain.staking.delegators.title" = "Delegadores"; +"parachain.staking.effective.amount.bonded" = "Valor efetivo garantido"; "parachain.staking.filters.collator.own.stake" = "Participação própria do Coletor ( %@ )"; "parachain.staking.filters.delegations" = "Delegações"; +"parachain.staking.filters.effective.amount.bonded" = "Valor efetivo garantido ( %@ )"; "parachain.staking.filters.minimum.bond" = "Fiança mínima"; -"staking.balance.ready.for.unlocking" = "Pronto para desbloquear"; -"custom.collators.title" = "Stake com coletores conhecidos"; -"custom.collators.text" = "Você deve confiar que seus coletores agirão com competência e honestidade; basear sua decisão puramente na lucratividade atual pode levar à redução dos lucros ou até mesmo à perda de fundos."; -"staking.next.round" = "PROXIMA RODADA"; -"staking.stake.with.selected.title" = "Stake com selecionado"; -"staking.filter.title.rewards.apr" = "Recompensas (APR)"; -"staking.custom.collators.title" = "Selecione um coletor"; -"staking.alert.leaving.collator.title" = "O coletor %s está saindo do conjunto de candidatos"; -"staking.alert.leaving.collator.text" = "Verifique sua delegação para o coletor %s e desbloqueie seus tokens se eles estiverem prontos"; -"staking.alert.low.stake.title" = "Sua delegação não está gerando nenhuma recompensa."; -"staking.alert.low.stake.text" = "O valor de seu stake é muito pequeno para gerar recompensas. Você pode apostar mais %s tokens ou remover sua aposta."; -"staking.alert.unlock.title" = "Seus tokens estão prontos para serem desbloqueados"; -"staking.alert.unlock.text" = "Por favor, complete a operação pendente (apostar menos/revogar) desbloqueando seus tokens"; -"staking.collator.info.title" = "Informações do coletor"; -"staking.round.title" = "rodada # %@"; -"parachain.staking.recommended.section.title" = "Coletores sugeridos por algoritmo"; -"staking.recommended.collators.empty.text" = "O valor de seu stake é inferior ao mínimo de todos os coletores sugeridos pelo algoritmo. Aumente o valor da sua aposta ou escolha um coletor manualmente"; -"parachain.staking.reward.info.max" = "APR máxima"; -"parachain.staking.reward.info.avg" = "APR média"; -"parachain.staking.recommended.list.title" = "Sugerido %d"; -"staking.stake.less.hint" = "Observação: você ainda pode cancelar esta solicitação de iniciação. Depois que o período de atraso de saída tiver passado (28 rodadas em Moonbeam- equivalente a 7 dias), você poderá retornar a este painel de staking e executar a solicitação, após a qual você verá os fundos não vinculados em seu saldo livre."; -"staking.collator.my.oversubscribed.message" = "Excesso de inscrições. Você não receberá recompensas do coletor nesta época."; -"staking.collator.other.oversubscribed.message" = "Excesso de inscrições. Apenas os delegadores com melhor aposta recebem recompensas."; -"staking.revoke.tokens" = "Revogar tokens"; "parachain.staking.hint.reward.bond.more" = "Suas recompensas aumentarão a partir da próxima rodada."; -"parachain.staking.delegators.title" = "Delegadores"; +"parachain.staking.minimum.bond" = "Fiança mínima"; +"parachain.staking.ready.for.revoking" = "Pronto para revogar"; +"parachain.staking.recommended.list.title" = "Sugerido %d"; +"parachain.staking.recommended.section.title" = "Coletores sugeridos por algoritmo"; "parachain.staking.request.finished" = "Finalizado"; -"parachain.staking.delegate" = "Delegar"; -"staking.history.title" = "Histórico de Staking"; -"wallets.managment.add.new.wallet" = "Adicionar nova carteira"; -"wallet.options.details" = "Detalhes da carteira"; -"wallet.options.export" = "Exportar carteira"; -"wallet.options.delete" = "Deletar carteira"; -"wallet.options.title" = "Opções da carteira"; -"staking.pool.rewards.delay.text" = "Faça o stake a qualquer momento. Você começará a ganhar juros após %s"; -"staking.pool.start.apr.text" = "Ganhe %s por ano"; -"staking.pool.start.unstake.period.text" = "Desfaça seu stake a qualquer momento. Você precisará esperar %s antes de poder gastar seus fundos e os juros que você ganhou."; -"staking.pool.start.reward.freq.text" = "As recompensas são pagas a cada %s"; -"staking.pool.start.join.button.title" = "Juntar-se à pool"; -"staking.pool.start.create.button.title" = "Crie uma pool"; -"staking.pool.start.earn.reward.title" = "Ganhe recompensas ao fazer stake do seu \n %s"; -"pool.staking.main.min.create.title" = "Mínimo para criar uma pool"; -"pool.staking.main.existing.pools.title" = "Pools existentes"; -"pool.staking.main.possible.pools.title" = "Possíveis pools"; -"pool.staking.main.max.members.inpool.title" = "Máximo de membros em uma pool"; -"pool.staking.main.max.pool.members.title" = "Máximo de membros da pool"; -"pool.staking.main.description.title" = "Os Stakers (membros) com uma pequena quantidade de tokens podem reunir seus fundos e atuar como um único nomeador. Os ganhos da pool são divididos proporcionalmente à participação de um membro na pool vinculado."; -"pool.staking.start.about.title" = "O que é Staking e como funciona, assista ao tutorial"; -"pool.staking.join.title" = "Juntar-se à pool"; -"pool.staking.join.account.title" = "Junte-se ao pool de"; -"pool.staking.join.button.title" = "Aderir"; -"pool.staking.choosepool.title" = "Escolher uma pool"; -"pool.staking.choosepool.button.title" = "Escolher"; -"pool.staking.choosepool.members.count.title" = "Membros: %d"; -"pool.staking.choosepool.staked.title" = "Staked %@"; -"pool.staking.selected.pool" = "Pool selecionada"; -"pool.staking.pool.name" = "Nome da Pool"; -"pool.staking.pool.id" = "ID da pool"; -"pool.staking.depositor" = "Depositante"; -"pool.staking.root" = "Raiz"; -"pool.staking.nominator" = "Nominador"; -"pool.staking.bouncer" = "Segurança"; -"pool.staking.title" = "Pool staking"; -"common.watch" = "Assistir"; -"hidden.assets" = "Ativos ocultos"; -"nft.stub.title" = "Sumimasen!"; -"nft.stub.text" = "NFTs chegarão em breve"; -"сurrencies.stub.text" = "Moedas"; -"nfts.stub" = "NFTs"; -"network.issue.unavailable" = "A rede não está disponível, você pode aguardar ou solicitar suporte da comunidade"; -"network.issue.notofication" = "Notificação"; -"network.issue.stub" = "Problemas de rede"; -"network.issue.network.unavailible" = "A rede não está disponível"; -"network.issue.node.unavailable" = "O node está indisponível"; -"pool.common" = "pool"; -"staking.pool.sort.pool.members" = "Número de membros"; -"pool.staking.main.min.join.title" = "Mínimo para se juntar à pool"; -"index.common" = "Índice"; -"state.common" = "Estado"; -"members.common" = "Membros"; -"staking.pool.info.title" = "Informações sobre a pool"; -"roles.common" = "Funções"; -"common.action.receive" = "Receber"; -"common.action.send" = "Enviar"; -"common.action.teleport" = "teleporte"; -"common.action.hide" = "Ocultar"; -"common.show" = "Mostrar"; -"common.balance.format" = "Saldo: %@"; -"send.confirm.amount.title" = "Enviando \n %@"; -"search.view.title" = "Enviar para"; -"search.textfield.placeholder" = "Endereço público"; -"choose.recipient.title" = "Selecionar destinatário"; -"choose.recipient.next.button.title" = "Insira o valor"; -"common.paste" = "Colar"; -"common.preview" = "Visualização"; -"scam.name.stub" = "Nome:"; -"scam.reason.stub" = "Motivo:"; -"scam.additional.stub" = "Adicional:"; -"scam.description.scam.stub" = "Este endereço foi sinalizado devido a evidências de fraude. Recomendamos fortemente que você não envie %s para esta conta."; -"scam.description.donation.stub" = "Este endereço foi sinalizado como suspeito. Recomendamos fortemente que você não envie %s para esta conta."; -"scam.description.exchange.stub" = "Este endereço está marcado como uma corretora, tome cuidado pois os endereços de depósito e saque podem ser diferentes."; -"scam.description.sanctions.stub" = "Este endereço foi sinalizado devido a uma entidade relacionada a um país sob sanções. Recomendamos fortemente que você não envie %s para esta conta."; -"scam.warning.alert.title" = "Este endereço foi sinalizado devido a uma entidade relacionada a um país sob sanções. Recomendamos fortemente que você não envie %@ para esta conta."; -"scam.warning.alert.subtitle" = "Recomendamos fortemente que você não envie %s para esta conta."; -"staking.pool.create.title" = "Crie uma pool"; -"common.copy" = "Copiar"; -"common.share" = "Compartilhar"; -"receive.note.text" = "NOTA: Esta é uma carteira Polkadot & Kusama. Envie apenas ativos da blockchain Polkadot & Kusama."; -"staking.pool.create.missing.pool.name" = "Não é possível criar a pool"; -"staking.pool.create.missing.name.description" = "O nome da pool está faltando"; -"staking.pool.create.poolId" = "ID da pool"; -"staking.pool.create.depositor" = "Depositante"; -"staking.pool.create.root" = "Raiz"; -"staking.pool.create.nominator" = "Nominador"; -"staking.pool.create.stateToggler" = "Alternador de estado"; -"common.create" = "Criar"; -"staking.pool.create.creating.pool" = "Criando pool \n %@"; -"staking.pool.create.management.account" = "Conta de gerenciamento da pool"; -"staking.pool.management.select.validators.title" = "Selecione validadores de pool"; -"staking.pool.management.select.validators.subtitle" = "Para continuar você deve \n selecionar validadores"; -"common.select" = "Selecione"; -"contacts.recent" = "Recente"; -"contacts.create.contact" = "Criar contato"; -"contacts.contact.name" = "Nome de contato"; -"contacts.contact.address" = "Endereço de contato"; -"contacts.undefined" = "Indefinido"; -"staking.pool.select.validators.manual" = "Selecione manualmente"; -"staking.pool.select.validators.suggested" = "Selecionar sugeridos"; -"pool.staking.management.option.nominees" = "Nomeados"; -"swipable.cell.button.send" = "Enviar"; -"swipable.cell.button.receive" = "Receber"; -"swipable.cell.button.teleport" = "teleporte"; -"swipable.cell.button.hide" = "Ocultar"; -"swipable.cell.button.show" = "Mostrar"; -"your.validators.validator.total.stake" = "Total em stake: %@"; -"your.validators.stop.nominating.title" = "Deixar de nomear"; -"your.validators.change.validators.title" = "Alterar validadores"; -"staking.select.validators.confirm.title" = "Selecionando validadores"; -"select.network.search.placeholder" = "Pesquisar por rede"; -"select.network.search.empty.subtitle" = "Nenhuma rede foi encontrada :("; -"applecation.status.view.coppied.title" = "Endereço copiado"; -"applecation.status.view.coppied.description" = "Endereço da carteira copiado com sucesso"; -"applecation.status.view.offline.title" = "Parece que você está off-line"; -"applecation.status.view.offline.description" = "Por favor verifique sua conexão e tente novamente"; -"application.status.view.reconnected.title" = "Reconectado"; -"application.status.view.reconnected.description" = "Conexão restaurada com sucesso"; -"send.fund.title" = "Enviar fundo"; -"all.done.alert.all.done.stub" = "Tudo pronto"; -"all.done.alert.description.stub" = "Você pode retornar ao aplicativo"; -"all.done.alert.hash.stub" = "Hash"; -"all.done.alert.result.stub" = "Resultado"; -"all.done.alert.success.stub" = "Sucesso"; -"pool.update.roles.title" = "Editar pool"; -"pool.join.no.validators.message" = "A pool ainda não escolheu nenhum validador. Se você aderir, não receberá nenhuma recompensa até que os validadores sejam selecionados"; -"pool.update.roles.warning" = "AVISO: A alteração da função é irreversível e pode levar à perda de acesso à pool."; -"application.status.view.hash.copied.title" = "Hash copiado"; -"application.status.view.hash.copied.description" = "Hash copiado com sucesso"; -"history.empty.description" = "Você não tem nenhuma transação aqui"; -"pool.claimable.title" = "Reclamável"; -"polkaswap.market.stub" = "Mercado:"; -"polkaswap.min.received" = "Mínimo recebido"; -"polkaswap.liquidity.provider.fee" = "Taxa do provedor de liquidez"; -"validators.list.empty.message" = "Não foram encontrados validadores"; -"custom.validators.empty.message" = "Nenhum validador encontrado. Por favor, tente alterar os filtros"; -"pools.limit.has.reached.error.title" = "Você não pode criar mais pools"; -"pools.limit.has.reached.error.message" = "O limite de pools nesta rede foi atingido"; -"staking.nominator.status.idle" = "Inativo"; -"staking.nominator.status.leaving" = "Saindo"; -"staking.status.low.stake" = "Stake baixo"; -"staking.status.ready.to.unlock" = "Pronto para desbloquear"; -"staking.recommended.filter.minimum.bond" = "Ter vínculo mínimo relevante"; -"pool.staking.start.confirm.amount.title" = "Juntando-se à pool \n%@"; -"transaction.list.header" = "Todas as transações"; -"staking.pool.stake.info.title" = "Informações sobre a pool de staking"; -"pool.staking.stake.more.amount.title" = "Aumentar Stake \n%@"; -"pool.staking.unstake.amount.title" = "Unstake \n%@"; -"pool.staking.redeem.amount.title" = "Resgatar \n %@"; -"pool.staking.claim.amount.title" = "Reivindicar \n %@"; -"pool.staking.total.stake.amount.title" = "Sua participação total:\n%@"; -"pool.staking.redeem.delay.title" = "Tempo antes do resgate"; -"pool.staking.management.claim.title" = "Reivindicar recompensa"; -"pool.staking.management.redeem.title" = "Resgatar tokens"; -"options.common" = "Opções"; -"empty.view.title" = "Sumimasen!"; -"empty.view.description" = "Nenhuma rede ou ativo foi encontrado :("; -"staking.pool.create.title" = "Crie uma pool"; -"staking.pool.create.missing.name.title" = "Não é possível criar a pool"; -"staking.pool.management.select.validators.title" = "Selecione validadores de pool"; -"staking.pool.management.select.validators.subtitle" = "Para continuar você deve \n selecionar validadores"; -"common.select" = "Selecione"; -"staking.select.validators.confirm.title" = "Selecionando validadores"; -"application.status.view.copied.title" = "Endereço copiado"; -"application.status.view.copied.description" = "Endereço da carteira copiado com sucesso"; -"application.status.view.hash.copied.title" = "Hash copiado"; -"application.status.view.hash.copied.description" = "Hash copiado com sucesso"; -"application.status.view.offline.title" = "Parece que você está off-line"; -"application.status.view.offline.description" = "Por favor verifique sua conexão e tente novamente"; -"application.status.view.reconnected.title" = "Reconectado"; -"application.status.view.reconnected.description" = "Conexão restaurada com sucesso"; -"choose.pool.empty.title" = "Nenhuma pool \n foi criado ainda"; -"select.asset.search.placeholder" = "Pesquisar por token"; -"pool.root.unbond.too.high.title" = "Você não pode retirar uma aposta por completo"; -"pool.root.unbond.too.high.text" = "Você é o proprietário de uma pool e sua aposta deve ser de pelo menos %s . Se você quiser destruir sua pool e expulsar seus membros, use o link Polkadot.js.plus"; -"polkadot.js.plus.action.title" = "Polkadot.js Plus"; -"select.suggested.validators.warning" = "As sugestões do validador algorítmico não constituem uma consulta ou aconselhamento financeiro. O staking é uma atividade de alto risco e as sugestões do validador algorítmico não atenuam necessariamente esse risco. Um validador sugerido pelo algoritmo ainda poderia sair do grupo de candidatos. Um validador sugerido pelo algoritmo também poderia alterar seus parâmetros (por exemplo, taxas de comissão, etc.) a qualquer momento após ter sido sugerido e/ou selecionado. Você pode perder recompensas por esses ou outros motivos. Aposte apenas tokens e use sugestões do validador a seu critério, após realizar a devida diligência e considerar cuidadosamente os riscos envolvidos."; -"select.asset.search.empty.subtitle" = "Nenhum ativo foi encontrado :("; -"parachain.staking.story.collator.title" = "Quem é um coletor?"; +"parachain.staking.revoke" = "Revogar"; +"parachain.staking.reward.info.avg" = "APR média"; +"parachain.staking.reward.info.max" = "APR máxima"; +"parachain.staking.self.bonded" = "Auto-afiançado"; +"parachain.staking.stake.less" = "Diminuir Stake"; "parachain.staking.story.collator.page.1" = "Os coletores mantêm parachains coletando transações dos usuários de parachains e produzindo provas de transição de estado para validadores da cadeia de retransmissão. Em outras palavras, os coletores agregam transações de parachains em blocos candidatos das parachains e produzem provas de transição de estado para validadores com base nesses blocos."; "parachain.staking.story.collator.page.2" = "Um colador executa um nó blockchain 24 horas por dia, 7 dias por semana, e é necessário ter stake suficiente bloqueado (de propriedade e fornecido por delegadores) para ser eleito pela rede. Os coletores devem manter o desempenho e a confiabilidade de seus nós para serem recompensados. Ser coletor é quase um trabalho em tempo integral.\ Todos podem ser um coletor e executar um nó de blockchain, mas fazer isso requer um certo nível de habilidades técnicas e responsabilidade."; +"parachain.staking.story.collator.title" = "Quem é um coletor?"; "parachain.staking.story.delegator.page.1" = "Os delegadores são detentores de tokens que apostam tokens, garantindo candidatos específicos a função de coletores. Qualquer usuário que possua uma quantidade mínima de tokens como saldo livre pode se tornar um delegador."; -"parachain.staking.story.delegator.title" = "Quem é um delegador?"; "parachain.staking.story.delegator.page.2" = "Os delegadores devem verificar regularmente os estados de seu staking. É possível que o saldo de staking fique abaixo do valor mínimo exigido para receber recompensas ou mesmo ser delegador. Na pior das hipóteses, o seu slot de staking pode ser substituído por outro delegador com uma aposta mais alta e a sua aposta pode ser retirada e imediatamente devolvida ao seu saldo."; -"parachain.staking.story.rewards.title" = "Recompensas"; +"parachain.staking.story.delegator.title" = "Quem é um delegador?"; "parachain.staking.story.rewards.page.1" = "Conjunto de recompensas - uma parte da inflação anual reservada para coletores e delegadores.\ As recompensas para os colerores e seus delegadores são calculadas no início de cada rodada pelo seu trabalho, antes do atraso no pagamento da recompensa.\ As recompensas calculadas são então pagas bloco por bloco. Para cada bloco, um colecionador será escolhido para receber todo o pagamento da recompensa da rodada anterior, junto com seus delegados, até que todas as recompensas dessa rodada tenham sido pagas."; "parachain.staking.story.rewards.page.2" = "Os delegadores recebem recompensas após um atraso no pagamento da recompensa. Atrasos de pagamento são uma certa quantidade de rodadas que devem passar antes que as recompensas das aposta sejam distribuídas automaticamente para o saldo grátis.\ A distribuição de recompensas a alguns delegadores pode ser interrompida por dois motivos possíveis: um coletor não foi escolhido pela rede para criar blocos ou decidiu sair do pool de candidatos. Outro motivo é ter um valor de participação inferior ao título mínimo do coletores."; -"alert.pool.created.text" = "Sua pool foi criada com sucesso. Agora você deve escolher validadores para a pool"; -"polkaswap.market.stub" = "Mercado"; -"polkaswap.min.received" = "Mínimo recebido"; -"polkaswap.max.received" = "Máximo Vendido"; -"polkaswap.liquidity.provider.fee" = "Taxa do provedor de liquidez"; -"polkaswap.network.fee" = "Taxa de rede"; -"polkaswap.confirmation.route.stub" = "Rota"; +"parachain.staking.story.rewards.title" = "Recompensas"; +"parachain.staking.unlock" = "Desbloquear"; +"pincode.confirm.pin.code" = "Confirmar código PIN"; +"pincode.confirm.your.pin.code" = "Confirme o seu código PIN"; +"pincode.create.top.title" = "Criar código PIN"; +"pincode.enter.pin.code" = "Insira o código PIN"; +"pincode.set.your.pin.code" = "Defina o seu código PIN"; +"pincode.setup.top.title" = "Configurar código PIN"; +"polkadot.js.plus.action.title" = "Polkadot.js Plus"; +"polkaswap.add.more.amount.message" = "Desculpe, você não tem fundos suficientes para pagar a taxa de rede. Não podemos cobrar a taxa nem do seu saldo atual, nem dos resultados do swap. Adicione mais tokens ou ajuste o valor da transação para prosseguir."; "polkaswap.confirmation.price.impact.stub" = "Impacto no preço"; +"polkaswap.confirmation.route.stub" = "Rota"; "polkaswap.confirmation.swap.stub" = "Trocar"; -"polkaswap.settings.reset" = "Redefinir para o padrão"; -"polkaswap.settings.slippage.stub" = "As suas transações serão revertidas se o preço mudar desfavoravelmente em quantidade superior a esta percentagem."; -"polkaswap.settings.slippage.title" = "Tolerância de Slippage"; -"polkaswap.settings.title" = "Configurações de transação"; -"polkaswap.settings.slippadge.fail" = "— Sua transação pode falhar"; -"polkaswap.settings.slippadge.frontrun" = "A sua transação pode ser antecipada por outra mais rápida"; -"polkaswap.market.smart.description" = "O roteamento de liquidez SMART assegura o melhor preço para qualquer transação, combinando apenas as melhores opções de preços de todos os mercados acessíveis. Quando disponível, a Curva Agregadora de Tokens (TBC) será usada para liquidez, desde que o preço do ativo seja mais econômico do que o de outras fontes, que utilizam a pool XYK."; -"polkaswap.market.tbc.description" = "TBC - compra apenas a partir da Curva Agregadora de Tokens (Mercado Primário). Há a possibilidade de o preço se tornar desfavorável em comparação com a pool XYK (Mercado Secundário), mas o valor das recompensas recebido pode vir a revelar-se muito mais favorável ao longo do tempo."; -"polkaswap.market.xyk.description" = "XYK — comprando apenas na pool XYK (Mercado Secundário). Swap tradicional da pool XYK, onde qualquer pessoa pode comprar ou vender ativos mudando a posição do formador de mercado na curva x*y=k."; -"polkaswap.minimum.received.info" = "A sua transação será revertida se houver um grande movimento de preço desfavorável antes de ser confirmada."; -"polkaswap.liqudity.fee.info" = "Uma parte de cada negociação (0,3%) vai para provedores de liquidez como incentivo do protocolo."; -"polkaswap.network.fee.info" = "A taxa de rede é usada para garantir o crescimento e o desempenho estável do sistema SORA."; -"polkaswap.price.impact.info" = "A diferença entre o preço de mercado e o preço estimado deve-se à dimensão da transação."; -"polkaswap.maximum.sold.info" = "A sua transação será revertida se houver um grande movimento de preço desfavorável antes de ser confirmada."; -"polkaswap.market.algorithm.title" = "Algoritmo do mercado"; -"polkaswap.market.alert.choose.action" = "Escolha o ativo"; -"polkaswap.market.alert.title" = "A seleção de mercado não está disponível"; -"polkaswap.market.alert.message" = "Você não concluiu os ajustes do swap. Um ou ambos os ativos não foram escolhidos, portanto a seleção de mercado não está disponível."; +"polkaswap.confirmation.swapped.stub" = "Trocado"; "polkaswap.dex.alert.message" = "Infelizmente, esse par não existe, mas você pode escolher outro"; "polkaswap.dex.alert.title" = "O par de tokens não foi criado"; -"polkaswap.quotes.not.available" = "Cotações não disponíveis"; -"all.done.subscan.button.title" = "Subscan"; -"same.address.transfer.warning.message" = "Você está tentando fazer uma transferência para a mesma conta. A operação cobrará uma taxa e não faz sentido nenhum"; -"terms.and.conditions.general.terms" = "Termos gerais de utilização"; -"terms.and.conditions.privacy.policy" = "Política de Privacidade"; -"terms.and.conditions.accept.and.continue" = "Aceitar e continuar"; -"terms.and.conditions.title" = "Termos e Condições"; -"terms.and.conditions.description" = "Queremos que você saiba exatamente como funcionam os serviços do Cartão SORA, quem e por que precisa dos seus dados. A revisão dessas políticas ajudará você a continuar usando o aplicativo com tranquilidade."; -"terms.and.conditions.sora.community.alert.main" = "A comunidade SORA não coleta nenhum dos seus dados pessoais,"; -"terms.and.conditions.sora.community.alert.secondary" = "mas para obter o cartão SORA e a conta IBAN você precisa passar pelo processo KYC com um emissor do cartão."; -"profile.soracard.title" = "Cartão SORA"; -"polkaswap.confirmation.swapped.stub" = "Trocado"; -"error.invalid.address" = "Endereço inválido para a rede selecionada"; -"polkaswap.add.more.amount.message" = "Desculpe, você não tem fundos suficientes para pagar a taxa de rede. Não podemos cobrar a taxa nem do seu saldo atual, nem dos resultados do swap. Adicione mais tokens ou ajuste o valor da transação para prosseguir."; -"polkaswap.disclaimer.settings" = "Declaração de exoneração de responsabilidade da Polkaswap"; -"polkaswap.disclaimer.title" = "Declaração de isenção de responsabilidade"; +"polkaswap.disclaimer.important" = "Confirmo que li todos os documentos mencionados e, pressionando 'Continuar', aceito-os."; +"polkaswap.disclaimer.number.1" = "É sua a exclusiva responsabilidade pelo cumprimento de todas as leis que se possam aplicar à utilização particular da Polkaswap na sua jurisdição legal;"; +"polkaswap.disclaimer.number.2" = "Compreende que a versão atual da Polkaswap é uma versão alfa: que não foi totalmente testada e algumas funções podem não funcionar como concebidas;"; +"polkaswap.disclaimer.number.3" = "Sua compreensão e aceitação voluntária dos riscos envolvidos no uso da Polkaswap, incluindo, mas não limitado a, o risco de perda de tokens."; "polkaswap.disclaimer.paragraph.1" = "A Polkaswap é mantida pela comunidade SORA. Antes de usar a Polkaswap, por favor reveja as %%FAQ da Polkaswap%% e a documentação, que inclui uma explicação detalhada sobre o funcionamento da Polkaswap, bem como o %%Memorandum da Polkaswap e Termos de Serviço%%, e a %%Política de Privacidade%%.\ \ Estes documentos são cruciais para uma experiência segura e positiva do utilizador. Ao usar a Polkaswap, está a reconhecer que leu e compreendeu estes documentos."; "polkaswap.disclaimer.paragraph.2" = "Estes documentos são cruciais para uma experiência segura e positiva do usuário. Ao usar a Polkaswap, reconhece que leu e compreendeu estes documentos."; "polkaswap.disclaimer.paragraph.3" = "Reconhece também o seguinte:"; "polkaswap.disclaimer.paragraph.4" = "Uma vez mais, por favor não continue sem ler os %%Polkaswap FAQ%%, %%Memorandum da Polkaswap e Termos de Serviços%%, e a %%Política de Privacidade%%%!"; -"polkaswap.disclaimer.number.1" = "É sua a exclusiva responsabilidade pelo cumprimento de todas as leis que se possam aplicar à utilização particular da Polkaswap na sua jurisdição legal;"; -"polkaswap.disclaimer.number.2" = "Compreende que a versão atual da Polkaswap é uma versão alfa: que não foi totalmente testada e algumas funções podem não funcionar como concebidas;"; -"polkaswap.disclaimer.number.3" = "Sua compreensão e aceitação voluntária dos riscos envolvidos no uso da Polkaswap, incluindo, mas não limitado a, o risco de perda de tokens."; -"polkaswap.disclaimer.important" = "Confirmo que li todos os documentos mencionados e, pressionando 'Continuar', aceito-os."; -"polkaswap.disclaimer.stub" = "ISENÇÃO DE RESPONSABILIDADE"; "polkaswap.disclaimer.read.before" = "Leia antes de continuar a usar Polkaswap"; +"polkaswap.disclaimer.settings" = "Declaração de exoneração de responsabilidade da Polkaswap"; +"polkaswap.disclaimer.stub" = "ISENÇÃO DE RESPONSABILIDADE"; "polkaswap.disclaimer.stub.read" = "Ler"; -"sora.card.status.success.text" = "A sua verificação KYC foi bem sucedida e já estamos a preparar o envio do cartão SORA!"; -"sora.card.status.failure.text" = "O KYC foi encerrado ou falhou."; -"sora.card.status.rejected.text" = "Seu aplicativo falhou. Para ler mais sobre o motivo da falha, leia a seguinte descrição adicional."; -"sora.card.status.pending.title" = "Verificação em andamento"; -"sora.card.status.success.title" = "Verificação bem-sucedida"; -"sora.card.status.failure.title" = "Falha na verificação"; -"sora.card.status.rejected.title" = "Verificação rejeitada"; -"network.issues.hide.action.title" = "Não me mostre novamente"; -"network.issues.empty.state.title" = "Sem problemas de rede"; +"polkaswap.disclaimer.title" = "Declaração de isenção de responsabilidade"; +"polkaswap.liqudity.fee.info" = "Uma parte de cada negociação (0,3%) vai para provedores de liquidez como incentivo do protocolo."; +"polkaswap.liquidity.provider.fee" = "Taxa do provedor de liquidez"; +"polkaswap.liquidity.provider.fee" = "Taxa do provedor de liquidez"; +"polkaswap.market.alert.choose.action" = "Escolha o ativo"; +"polkaswap.market.alert.message" = "Você não concluiu os ajustes do swap. Um ou ambos os ativos não foram escolhidos, portanto a seleção de mercado não está disponível."; +"polkaswap.market.alert.title" = "A seleção de mercado não está disponível"; +"polkaswap.market.algorithm.title" = "Algoritmo do mercado"; +"polkaswap.market.smart.description" = "O roteamento de liquidez SMART assegura o melhor preço para qualquer transação, combinando apenas as melhores opções de preços de todos os mercados acessíveis. Quando disponível, a Curva Agregadora de Tokens (TBC) será usada para liquidez, desde que o preço do ativo seja mais econômico do que o de outras fontes, que utilizam a pool XYK."; +"polkaswap.market.stub" = "Mercado:"; +"polkaswap.market.stub" = "Mercado"; +"polkaswap.market.tbc.description" = "TBC - compra apenas a partir da Curva Agregadora de Tokens (Mercado Primário). Há a possibilidade de o preço se tornar desfavorável em comparação com a pool XYK (Mercado Secundário), mas o valor das recompensas recebido pode vir a revelar-se muito mais favorável ao longo do tempo."; +"polkaswap.market.xyk.description" = "XYK — comprando apenas na pool XYK (Mercado Secundário). Swap tradicional da pool XYK, onde qualquer pessoa pode comprar ou vender ativos mudando a posição do formador de mercado na curva x*y=k."; +"polkaswap.max.received" = "Máximo Vendido"; +"polkaswap.maximum.sold.info" = "A sua transação será revertida se houver um grande movimento de preço desfavorável antes de ser confirmada."; +"polkaswap.min.received" = "Mínimo recebido"; +"polkaswap.min.received" = "Mínimo recebido"; +"polkaswap.minimum.received.info" = "A sua transação será revertida se houver um grande movimento de preço desfavorável antes de ser confirmada."; +"polkaswap.network.fee" = "Taxa de rede"; +"polkaswap.network.fee.info" = "A taxa de rede é usada para garantir o crescimento e o desempenho estável do sistema SORA."; +"polkaswap.price.impact.info" = "A diferença entre o preço de mercado e o preço estimado deve-se à dimensão da transação."; +"polkaswap.quotes.not.available" = "Cotações não disponíveis"; +"polkaswap.settings.reset" = "Redefinir para o padrão"; +"polkaswap.settings.slippadge.fail" = "— Sua transação pode falhar"; +"polkaswap.settings.slippadge.frontrun" = "A sua transação pode ser antecipada por outra mais rápida"; +"polkaswap.settings.slippage.stub" = "As suas transações serão revertidas se o preço mudar desfavoravelmente em quantidade superior a esta percentagem."; +"polkaswap.settings.slippage.title" = "Tolerância de Slippage"; +"polkaswap.settings.title" = "Configurações de transação"; +"pool.claimable.title" = "Reclamável"; +"pool.common" = "pool"; +"pool.join.no.validators.message" = "A pool ainda não escolheu nenhum validador. Se você aderir, não receberá nenhuma recompensa até que os validadores sejam selecionados"; +"pool.root.unbond.too.high.text" = "Você é o proprietário de uma pool e sua aposta deve ser de pelo menos %s . Se você quiser destruir sua pool e expulsar seus membros, use o link Polkadot.js.plus"; +"pool.root.unbond.too.high.title" = "Você não pode retirar uma aposta por completo"; +"pool.staking.bouncer" = "Segurança"; +"pool.staking.choosepool.button.title" = "Escolher"; +"pool.staking.choosepool.members.count.title" = "Membros: %d"; +"pool.staking.choosepool.staked.title" = "Staked %@"; +"pool.staking.choosepool.title" = "Escolher uma pool"; +"pool.staking.claim.amount.title" = "Reivindicar \n %@"; +"pool.staking.depositor" = "Depositante"; +"pool.staking.join.account.title" = "Junte-se ao pool de"; +"pool.staking.join.button.title" = "Aderir"; +"pool.staking.join.title" = "Juntar-se à pool"; +"pool.staking.main.description.title" = "Os Stakers (membros) com uma pequena quantidade de tokens podem reunir seus fundos e atuar como um único nomeador. Os ganhos da pool são divididos proporcionalmente à participação de um membro na pool vinculado."; +"pool.staking.main.existing.pools.title" = "Pools existentes"; +"pool.staking.main.max.members.inpool.title" = "Máximo de membros em uma pool"; +"pool.staking.main.max.pool.members.title" = "Máximo de membros da pool"; +"pool.staking.main.min.create.title" = "Mínimo para criar uma pool"; +"pool.staking.main.min.join.title" = "Mínimo para se juntar à pool"; +"pool.staking.main.possible.pools.title" = "Possíveis pools"; "pool.staking.management.claim.button.title" = "Reivindicar"; -"xcm.origin.network.fee.title" = "Taxa da rede de origem"; -"xcm.destination.network.fee.title" = "Taxa de rede de destino"; -"xcm.origin.network.title" = "Rede de origem"; -"xcm.destination.network.title" = "Rede de destino"; -"xcm.title" = "Taxa entre cadeias"; -"common.not.available.short" = "N/A"; -"xcm.mywallets.button.title" = "Minhas carteiras"; -"xcm.cross.chain.button.title" = "Taxa entre cadeias"; -"wallet.managment.select.wallet.title" = "Selecione sua carteira"; -"xcm.cross.chain.invalid.address.title" = "Não é endereço de rede"; -"xcm.cross.chain.invalid.address.message" = "De acordo com o endereço fornecido, você está tentando fazer uma transferência na rede errada."; -"common.disconnect" = "Desconectar"; -"common.important" = "Importante"; -"backup.mnemonic.description" = "Lembre-se de registrar suas palavras na mesma ordem em que aparecem abaixo. Use uma forma não digital de backup."; -"backup.mnemonic.title" = "Escreva a sua mnemónica"; -"backup.wallet.backup.google" = "Backup para o Google"; -"backup.wallet.delete.google" = "Excluir backup do Google"; -"backup.wallet.json" = "Exportar JSON"; -"backup.risks.warnings.continue.button" = "Mostrar frase mnemônica"; -"backup.wallet.footer.view.text" = "Se você perder o acesso a este dispositivo, seus fundos serão perdidos, a menos que você faça backup!"; -"btn.backup.with.google" = "Backup para o Google"; -"common.transaction.raw.data" = "Dados brutos de transação"; -"backup.wallet.name.create.description" = "Dê um nome para sua nova carteira, para que você possa identificá-la facilmente. Isso é opcional e ficará visível apenas para você"; -"backup.wallet.name.create.bottom.decription" = "Visível apenas para você e armazenado localmente"; -"backup.wallet.name.create.title" = "Nomeie sua nova carteira"; -"backup.wallet.imported.import.more" = "Importar mais"; -"backup.password.password.field.title" = "Digite a senha"; -"backup.password.description" = "Digite a senha de backup da carteira selecionada para importação"; -"backup.wallet.imported.title" = "Carteira importada"; -"backup.password.title" = "Digite a senha de backup"; -"backup.risks.warnings.1" = "Se eu expor ou compartilhar minha frase mnemônica com alguém, meus fundos poderão ser roubados"; -"backup.risks.warnings.2" = "Se eu expor ou compartilhar minha frase mnemônica com alguém, meus fundos poderão ser roubados"; -"backup.risks.warnings.3" = "É minha total responsabilidade manter minha frase mnemônica segura"; -"backup.risks.warnings.description" = "Sua frase mnemônica é a chave da sua carteira. Faça backup para poder restaurar sua carteira caso perca ou danifique seu dispositivo."; -"backup.risks.warnings.title" = "Faça backup do seu mnemônico"; +"pool.staking.management.claim.title" = "Reivindicar recompensa"; +"pool.staking.management.option.nominees" = "Nomeados"; +"pool.staking.management.redeem.title" = "Resgatar tokens"; +"pool.staking.nominator" = "Nominador"; +"pool.staking.pool.id" = "ID da pool"; +"pool.staking.pool.name" = "Nome da Pool"; +"pool.staking.redeem.amount.title" = "Resgatar \n %@"; +"pool.staking.redeem.delay.title" = "Tempo antes do resgate"; +"pool.staking.root" = "Raiz"; +"pool.staking.selected.pool" = "Pool selecionada"; +"pool.staking.stake.more.amount.title" = "Aumentar Stake \n%@"; +"pool.staking.start.about.title" = "O que é Staking e como funciona, assista ao tutorial"; +"pool.staking.start.confirm.amount.title" = "Juntando-se à pool \n%@"; +"pool.staking.title" = "Pool staking"; +"pool.staking.total.stake.amount.title" = "Sua participação total:\n%@"; +"pool.staking.unstake.amount.title" = "Unstake \n%@"; +"pool.update.roles.title" = "Editar pool"; +"pool.update.roles.warning" = "AVISO: A alteração da função é irreversível e pode levar à perda de acesso à pool."; +"pools.limit.has.reached.error.message" = "O limite de pools nesta rede foi atingido"; +"pools.limit.has.reached.error.title" = "Você não pode criar mais pools"; +"profile.about.title" = "Sobre"; +"profile.accounts.title" = "Contas"; +"profile.language.title" = "Idioma"; +"profile.logout.description" = "Esta ação resultará na eliminação de todas as contas deste dispositivo. Certifique-se de que fez o backup da sua frase-chave antes de prosseguir."; +"profile.logout.title" = "Sair"; +"profile.network.title" = "Redes"; +"profile.pincode.change.title" = "Alterar código PIN"; +"profile.soracard.title" = "Cartão SORA"; +"profile.title" = "Configurações"; +"profile.wallets.title" = "Carteiras"; +"receive.note.text" = "NOTA: Esta é uma carteira Polkadot & Kusama. Envie apenas ativos da blockchain Polkadot & Kusama."; +"recover.json.hint" = "Colar o JSON ou fazer upload do ficheiro..."; +"remove.backup.extension.error.message" = "O backup foi criado pela extensão do navegador Fearless Wallet. O aplicativo móvel não pode removê-lo devido à falta de credenciais"; +"replace.account" = "Substituir conta"; +"replace.account.template" = "Substituir %s conta"; +"required.accounts.not.satisfied" = "Não é possível prosseguir: sua carteira não possui as contas necessárias. Certifique-se de ter as contas necessárias configuradas antes de continuar"; +"required.chains.not.satisfied" = "Não é possível conectar-se à carteira: a rede blockchain solicitada não é suportada ou não está disponível"; +"required.events.not.satisfied" = "Notificações de eventos ausentes: o aplicativo não oferece suporte às notificações de eventos necessárias"; +"required.methods.not.satisfied" = "Certos métodos solicitados pelo Wallet Connect não são atualmente suportados pela aplicação. Embora a conexão possa ser estabelecida, certas funcionalidades do dApp podem não estar disponíveis."; +"required.networks" = "Redes necessárias"; +"review.optional.permissions" = "Rever as permissões opcionais"; +"review.permissions" = "Rever permissões"; +"review.required.permissions" = "Rever as permissões necessárias"; +"roles.common" = "Funções"; +"same.address.transfer.warning.message" = "Você está tentando fazer uma transferência para a mesma conta. A operação cobrará uma taxa e não faz sentido nenhum"; +"scam.additional.stub" = "Adicional:"; +"scam.description.donation.stub" = "Este endereço foi sinalizado como suspeito. Recomendamos fortemente que você não envie %s para esta conta."; +"scam.description.exchange.stub" = "Este endereço está marcado como uma corretora, tome cuidado pois os endereços de depósito e saque podem ser diferentes."; +"scam.description.sanctions.stub" = "Este endereço foi sinalizado devido a uma entidade relacionada a um país sob sanções. Recomendamos fortemente que você não envie %s para esta conta."; +"scam.description.scam.stub" = "Este endereço foi sinalizado devido a evidências de fraude. Recomendamos fortemente que você não envie %s para esta conta."; +"scam.name.stub" = "Nome:"; +"scam.reason.stub" = "Motivo:"; +"scam.warning.alert.subtitle" = "Recomendamos fortemente que você não envie %s para esta conta."; +"scam.warning.alert.title" = "Este endereço foi sinalizado devido a uma entidade relacionada a um país sob sanções. Recomendamos fortemente que você não envie %@ para esta conta."; +"scan.qr.subtitle" = "Leia o código do destinatário."; +"scan.qr.title" = "Ler QR"; +"scan.qr.upload.button.title" = "Carregar da galeria"; +"search.by.connection" = "Pesquisar por conexão"; +"search.textfield.placeholder" = "Endereço público"; +"search.view.title" = "Enviar para"; +"select.asset.search.empty.subtitle" = "Nenhum ativo foi encontrado :("; +"select.asset.search.placeholder" = "Pesquisar por token"; +"select.collators.warning" = "AVISO: Os detentores de tokens MOVR/GLMR devem realizar a cuidadosa devida diligência acerca dos validadores antes de delegar. Ser listado como um validador não representa um endosso ou recomendação da Moonbeam Network, da Moonriver Network ou da Moonbeam Foundation. A Moonbeam Network, a Moonriver Network e a Moonbeam Foundation não examinaram os validadores da lista e não assumem nenhuma responsabilidade com relação à seleção, desempenho, segurança, precisão ou uso de quaisquer ofertas de terceiros. Você é o único responsável por fazer a sua própria diligência para entender as taxas aplicáveis e todos os riscos presentes, incluindo o monitoramento ativo da atividade dos seus validadores."; +"select.network.search.empty.subtitle" = "Nenhuma rede foi encontrada :("; +"select.network.search.placeholder" = "Pesquisar por rede"; +"select.save.type" = "Selecione o tipo de guardar"; +"select.suggested.validators.warning" = "As sugestões do validador algorítmico não constituem uma consulta ou aconselhamento financeiro. O staking é uma atividade de alto risco e as sugestões do validador algorítmico não atenuam necessariamente esse risco. Um validador sugerido pelo algoritmo ainda poderia sair do grupo de candidatos. Um validador sugerido pelo algoritmo também poderia alterar seus parâmetros (por exemplo, taxas de comissão, etc.) a qualquer momento após ter sido sugerido e/ou selecionado. Você pode perder recompensas por esses ou outros motivos. Aposte apenas tokens e use sugestões do validador a seu critério, após realizar a devida diligência e considerar cuidadosamente os riscos envolvidos."; +"select.validators.disclaimer" = "RESPONSABILIDADE: As sugestões de validador algorítmico não constituem uma consulta ou conselho financeiro. O staking é uma atividade de alto risco, e as sugestões de validador algorítmico não mitigam necessariamente este risco. Um validador sugerido pelo algoritmo pode ainda ser penalizado. Um validador sugerido pelo algoritmo pode também alterar os seus parâmetros (por exemplo, taxas de comissão, etc.) em qualquer altura depois de ter sido sugerido e/ou selecionado. Pode perder tokens ou recompensas por estas, ou outras razões. Apenas faça stake de tokens e utilize as sugestões do validador ao seu próprio critério, após ter realizado a devida diligência e considerado cuidadosamente os riscos envolvidos."; +"send.all.title" = "Send all tokens and reap the account"; +"send.confirm.amount.title" = "Enviando \n %@"; +"send.fund.title" = "Enviar fundo"; +"settings.add.wallet" = "Adicionar carteira"; "settings.hide.zero.balances" = "Ocultar saldos nulos"; +"share.referral.code" = "Partilhar código de referência"; +"sign.this.message" = "Assinar esta mensagem?"; +"sora.bridge.amount.less.fee" = "The amount you're trying to transfer is insufficient to cover the transaction fees on the %s network. Although the transaction won't process, you'll still be charged the fees on the Sora network."; +"sora.bridge.low.amaunt.polkadot.alert" = "Currently, there's a min. amount 1.1 DOT for bridging to ensure the stability and security. We appreciate your understanding."; +"sora.bridge.low.amount.alert" = "Currently, there's a min. amount 0.05 KSM for bridging to ensure the stability and security of the SORA Network. We appreciate your understanding."; +"sora.card.status.failure.text" = "O KYC foi encerrado ou falhou."; +"sora.card.status.failure.title" = "Falha na verificação"; +"sora.card.status.pending.title" = "Verificação em andamento"; +"sora.card.status.rejected.text" = "Seu aplicativo falhou. Para ler mais sobre o motivo da falha, leia a seguinte descrição adicional."; +"sora.card.status.rejected.title" = "Verificação rejeitada"; +"sora.card.status.success.text" = "A sua verificação KYC foi bem sucedida e já estamos a preparar o envio do cartão SORA!"; +"sora.card.status.success.title" = "Verificação bem-sucedida"; +"sr25519.selection.subtitle" = "sr25519 (recomendado)"; +"sr25519.selection.title" = "Schnorrkel"; +"stacking.stash.account" = "Conta stash"; +"staking.account.is.used.as.controller" = "A conta selecionada já está a ser utilizada como controlador"; +"staking.add.controller" = "Adicione conta de controlador %@ à aplicação para executar essa ação."; +"staking.alert.leaving.collator.text" = "Verifique sua delegação para o coletor %s e desbloqueie seus tokens se eles estiverem prontos"; +"staking.alert.leaving.collator.title" = "O coletor %s está saindo do conjunto de candidatos"; +"staking.alert.low.stake.text" = "O valor de seu stake é muito pequeno para gerar recompensas. Você pode apostar mais %s tokens ou remover sua aposta."; +"staking.alert.low.stake.title" = "Sua delegação não está gerando nenhuma recompensa."; +"staking.alert.start.next.era.message" = "Por favor, aguarde pelo início da próxima era."; +"staking.alert.unlock.text" = "Por favor, complete a operação pendente (apostar menos/revogar) desbloqueando seus tokens"; +"staking.alert.unlock.title" = "Seus tokens estão prontos para serem desbloqueados"; +"staking.alerts.title" = "Alertas"; +"staking.analytics.7days.rewards" = "Recompensas de 7 dias"; +"staking.analytics.activity" = "Atividade"; +"staking.analytics.avg" = "%@ média"; +"staking.analytics.details.type" = "Tipo"; +"staking.analytics.era.range" = "%d de %d eras"; +"staking.analytics.period.1y" = "1ano"; +"staking.analytics.period.30d" = "30dias"; +"staking.analytics.period.7d" = "7dias"; +"staking.analytics.period.all" = "Tudo"; +"staking.analytics.rewards.empty.message" = "Sem recompensas recebidas\ +para o período selecionado"; +"staking.analytics.stake.allocation" = "Alocação com validadores atuais"; +"staking.analytics.stake.empty.message" = "As suas alterações de stake\ +aparecerão aqui"; +"staking.analytics.staking.was.active" = "O staking estava ativo"; +"staking.analytics.staking.was.inactive" = "O staking estava inativo"; +"staking.analytics.title" = "Analíticos"; +"staking.balance.ready.for.unlocking" = "Pronto para desbloquear"; +"staking.balance.title" = "Saldo de staking"; +"staking.bond.more.completion" = "A solicitação extra para stake foi enviada"; +"staking.bond.more.tokens" = "Faça stake de mais tokens."; +"staking.bond.more_v1.9.0" = "Fazer mais stake"; +"staking.bonded.format" = "Vinculado: %@"; +"staking.bonded.inactive" = "Não está nomeando nem validando"; +"staking.change.your.validators" = "Mude os seus validadores."; +"staking.collator.info.title" = "Informações do coletor"; +"staking.collator.my.oversubscribed.message" = "Excesso de inscrições. Você não receberá recompensas do coletor nesta época."; +"staking.collator.other.oversubscribed.message" = "Excesso de inscrições. Apenas os delegadores com melhor aposta recebem recompensas."; +"staking.collators" = "Validadores"; +"staking.common.era" = "Era"; +"staking.common.event.id" = "Evento"; +"staking.common.rewards.apy" = "Recompensas (APY)"; +"staking.common.validator" = "Validador"; +"staking.controller.account.title" = "Conta de controlador"; +"staking.controller.account.zero.balance" = "Descobrimos que esta conta não tem tokens gratuitos. Tem a certeza de que deseja alterar o controlador?"; +"staking.controller.can.hint" = "O controlador pode retirar de stake, resgatar, repor em stake, alterar o destino da recompensa e validadores."; "staking.controller.deprecated.description" = "Observe que na rede %s o recurso de conta do Controlador foi descontinuado e, como resultado, você deve definir sua conta Stash como Controlador."; -"network.issues.resolve.option.title" = "Opção de resolução"; -"backup.wallet.delete.message" = "Se você excluir seu backup do Google, só poderá recuperar sua carteira com um backup manual de sua senha"; -"backup.wallet.name.editing.title" = "Alterar nome da carteira"; -"backup.wallet.name.field.name.title" = "Nome da carteira"; -"controller.account.issue.message" = "Observe que o recurso de conta do Controlador foi descontinuado e, como resultado, você deve definir sua conta Stash como Controlador"; -"controller.account.issue.action" = "Gerenciar conta do controlador"; -"stash.account.issue.message" = "A conta stash %s não está disponível. Por favor, importe a conta Stash seguindo as etapas necessárias"; +"staking.custom.blocked.warning" = "Este validador não está a aceitar nomeações de momento. Por favor, tente novamente na próxima era."; +"staking.custom.clear.button.title" = "Limpar filtros"; +"staking.custom.collators.title" = "Selecione um coletor"; +"staking.custom.deselect.button.title" = "Desselecionar todos"; +"staking.custom.deselect.warning" = "Está prestes a desselecionar todos os validadores selecionados anteriormente."; +"staking.custom.fill.button.title" = "Preencher o restante com o recomendado"; +"staking.custom.header.validators.title" = "Validadores: %li de %li"; +"staking.custom.proceed.button.disabled.title" = "Selecionar validadores (max %d)"; +"staking.custom.proceed.button.enabled.title" = "Mostrar selecionados: %d (max %d)"; +"staking.custom.validators.list.title" = "Selecione validadores"; +"staking.custom.validators.update.list" = "Atualize a sua lista"; +"staking.era.title" = "era #%@"; +"staking.error.insufficient.balance.title" = "Saldo insuficiente"; +"staking.estimate.earning.title_v1.9.0" = "Ganhos estimados"; +"staking.filter.title.own.stake" = "Stake pertencente ao validador"; +"staking.filter.title.own.stake.token" = "Stake pertencente ao validador (%@)"; +"staking.filter.title.rewards" = "Recompensas (APY)"; +"staking.filter.title.rewards.apr" = "Recompensas (APR)"; +"staking.hint.no.rewards" = "Os tokens em período de unstaking não geram recompensas."; +"staking.hint.redeem" = "Após o período de unstaking, precisará de resgatar os seus tokens."; +"staking.hint.reward.bond.more" = "As suas recompensas aumentarão a partir da próxima era."; +"staking.hint.rewards.format" = "Os tokens em stake geram recompensas a cada época (%@)."; +"staking.hint.unbond.kills.stash" = "A Fearless wallet mudará o destino das recompensas \ +para a sua conta para evitar o staking do restante."; +"staking.hint.unstake.format" = "Se quiser fazer unstake, terá que esperar que período de unstake termine (%@)."; +"staking.history.title" = "Histórico de Staking"; +"staking.inactive.bond" = "Vínculo Inativo"; +"staking.inactive.current.minimal.stake" = "O staking está inativo de momento. \n A participação mínima atual é %@."; +"staking.main.active.nominators.title" = "Nomeadores ativos"; +"staking.main.lockup.period.title_v1.9.0" = "Período de desbloqueio"; +"staking.main.minimum.stake.title" = "Stake mínimo"; +"staking.main.network.title" = "Rede %@"; +"staking.main.stake.balance.staked" = "Staked"; +"staking.main.total.staked.title" = "Stake total"; +"staking.manage.title" = "Gerir"; +"staking.max.nominators.reached.message" = "O número máximo de nomeadores foi atingido"; +"staking.max.nominators.reached.title" = "Não é possível começar a fazer staking"; +"staking.month.period.format" = "%@ mensalmente"; +"staking.next.round" = "PROXIMA RODADA"; +"staking.nominator.status.active" = "Ativo"; +"staking.nominator.status.alert.active.message" = "Um dos seus validadores foi eleito pela rede."; +"staking.nominator.status.alert.active.title" = "Estado ativo"; +"staking.nominator.status.alert.inactive.title" = "Estado inativo"; +"staking.nominator.status.alert.low.stake" = "O valor do seu stake é inferior ao stake mínimo para obter uma recompensa."; +"staking.nominator.status.alert.no.validators" = "Nenhum dos seus validadores foi eleito pela rede."; +"staking.nominator.status.alert.waiting.message" = "O seu staking começará na próxima era."; +"staking.nominator.status.idle" = "Inativo"; +"staking.nominator.status.inactive" = "Inativo"; +"staking.nominator.status.leaving" = "Saindo"; +"staking.nominator.status.waiting" = "À espera da próxima Era"; +"staking.payout.expired" = "O pagamento expirou"; +"staking.payout.sent" = "Transação de pagamento enviada"; +"staking.pending.rewards" = "Recompensas pendentes"; +"staking.pending.rewards.explanation.message" = "Os validadores pagam as recompensas a cada 2 a 5 dias. No entanto, pode demandá-los sozinho, especialmente se as recompensas estão perto de expirar. Nesse caso precisará de pagar as taxas."; +"staking.pool.create.creating.pool" = "Criando pool \n %@"; +"staking.pool.create.depositor" = "Depositante"; +"staking.pool.create.management.account" = "Conta de gerenciamento da pool"; +"staking.pool.create.missing.name.description" = "O nome da pool está faltando"; +"staking.pool.create.missing.name.title" = "Não é possível criar a pool"; +"staking.pool.create.missing.pool.name" = "Não é possível criar a pool"; +"staking.pool.create.nominator" = "Nominador"; +"staking.pool.create.poolId" = "ID da pool"; +"staking.pool.create.root" = "Raiz"; +"staking.pool.create.stateToggler" = "Alternador de estado"; +"staking.pool.create.title" = "Crie uma pool"; +"staking.pool.create.title" = "Crie uma pool"; +"staking.pool.info.title" = "Informações sobre a pool"; +"staking.pool.management.select.validators.subtitle" = "Para continuar você deve \n selecionar validadores"; +"staking.pool.management.select.validators.subtitle" = "Para continuar você deve \n selecionar validadores"; +"staking.pool.management.select.validators.title" = "Selecione validadores de pool"; +"staking.pool.management.select.validators.title" = "Selecione validadores de pool"; +"staking.pool.rewards.delay.text" = "Faça o stake a qualquer momento. Você começará a ganhar juros após %s"; +"staking.pool.select.validators.manual" = "Selecione manualmente"; +"staking.pool.select.validators.suggested" = "Selecionar sugeridos"; +"staking.pool.sort.pool.members" = "Número de membros"; +"staking.pool.stake.info.title" = "Informações sobre a pool de staking"; +"staking.pool.start.apr.text" = "Ganhe %s por ano"; +"staking.pool.start.create.button.title" = "Crie uma pool"; +"staking.pool.start.earn.reward.title" = "Ganhe recompensas ao fazer stake do seu \n %s"; +"staking.pool.start.join.button.title" = "Juntar-se à pool"; +"staking.pool.start.reward.freq.text" = "As recompensas são pagas a cada %s"; +"staking.pool.start.unstake.period.text" = "Desfaça seu stake a qualquer momento. Você precisará esperar %s antes de poder gastar seus fundos e os juros que você ganhou."; +"staking.rebond" = "Retornar para stake"; +"staking.rebond.all" = "Revincular todos"; +"staking.rebond.custom.amount" = "Revincular montante personalizado"; +"staking.rebond.insufficient.bondings" = "A quantidade que deseja retornar para stake é maior do que o seu saldo sem stake"; +"staking.rebond.last" = "Revincular último"; +"staking.recommended.collators.empty.text" = "O valor de seu stake é inferior ao mínimo de todos os coletores sugeridos pelo algoritmo. Aumente o valor da sua aposta ou escolha um coletor manualmente"; +"staking.recommended.custom.title" = "Escolher validadores personalizados"; +"staking.recommended.filter.minimum.bond" = "Ter vínculo mínimo relevante"; +"staking.recommended.hint1" = "Mais rentável"; +"staking.recommended.hint2" = "Sem excesso de subscrições"; +"staking.recommended.hint3" = "Ter identidade na rede"; +"staking.recommended.hint3.addition" = "com pelo menos um contacto de identidade"; +"staking.recommended.hint4" = "Não penalizado"; +"staking.recommended.hint5" = "Limite de 2 validadores por identidade"; +"staking.recommended.section.title" = "Validadores recomendados pelo algoritmo"; +"staking.recommended.title" = "Validadores"; +"staking.recommended.validators.counter" = "%@ de %@"; +"staking.redeem" = "Resgatar"; +"staking.redeem.no.tokens.message" = "Nenhum token resgatável encontrado"; +"staking.redeem.unbonded.tokens" = "Resgatar tokens sem stake."; +"staking.redeemable.format" = "Resgatável: %@"; +"staking.restake.title" = "Restake"; +"staking.revoke.tokens" = "Revogar tokens"; +"staking.reward" = "Recompensa"; +"staking.reward.details.date" = "Data"; +"staking.reward.details.era" = "Era"; +"staking.reward.details.payout" = "Pagamento"; +"staking.reward.details.reward" = "Recompensa"; +"staking.reward.details.status" = "Estado"; +"staking.reward.details.status.claimable" = "Reclamável"; +"staking.reward.details.status.received" = "Recebido"; +"staking.reward.details.title" = "Detalhes da recompensa"; +"staking.reward.details.validator" = "Validador"; +"staking.reward.info.avg" = "APY médio"; +"staking.reward.info.max" = "APY máximo"; +"staking.reward.info.title" = "Ganhos com restake"; +"staking.reward.payout.account" = "Conta para pagamento"; +"staking.reward.payouts.empty.rewards" = "Perfeito! Todas as recompensas foram pagas."; +"staking.reward.payouts.payout.all" = "Pagar a todos (%@)"; +"staking.reward.payouts.title" = "Recompensas pendentes"; +"staking.rewards.destination.title" = "Destino das recompensas"; +"staking.rewards.learn.more" = "Saiba mais acerca das recompensas"; +"staking.rewards.title" = "Recompensas"; +"staking.round.title" = "rodada # %@"; +"staking.select.suggested" = "Selecionar sugeridos"; +"staking.select.validators.confirm.title" = "Selecionando validadores"; +"staking.select.validators.confirm.title" = "Selecionando validadores"; +"staking.select.validators.custom.button.title" = "Selecione você mesmo"; +"staking.select.validators.custom.desc" = "Deve confiar nas suas nomeações para agir de forma competente e honesta, baseando sua decisão puramente na sua rentabilidade atual pode levar a lucros reduzidos ou até mesmo perda de fundos."; +"staking.select.validators.custom.title" = "Fazer stake com os seus validadores"; +"staking.select.validators.recommended.button.title" = "Selecione o recomendado"; +"staking.select.validators.recommended.desc" = "O algoritmo da Fearless selecionou uma lista de validadores recomendados com base nos critérios:"; +"staking.select.validators.recommended.title" = "Fazer stake com os validadores recomendados"; +"staking.selected.collator" = "Coletor selecionado"; +"staking.selected.validators.count_v1.9.1" = "selecionado %d (max %d)"; +"staking.selected.validators.title" = "Validadores selecionados"; +"staking.set.separate.account.controller" = "Defina uma conta separada como controlador para aumentar a segurança do gestão de staking."; +"staking.set.validators.message" = "Validadores não selecionados"; +"staking.set.validators.title" = "Selecione validadores para iniciar o stake"; +"staking.setup.amount.too.low" = "Não é possível fazer staking menor do que o valor mínimo (%@)"; +"staking.setup.payout.title" = "Pagamento"; +"staking.setup.restake.title" = "Restake"; +"staking.setup.reward.destination.section.title" = "Como utilizar as suas recompensas?"; +"staking.setup.sent.message" = "Transação de configuração de staking enviada"; +"staking.slash" = "Penalização"; +"staking.stake" = "Stake"; +"staking.stake.less.hint" = "Observação: você ainda pode cancelar esta solicitação de iniciação. Depois que o período de atraso de saída tiver passado (28 rodadas em Moonbeam- equivalente a 7 dias), você poderá retornar a este painel de staking e executar a solicitação, após a qual você verá os fundos não vinculados em seu saldo livre."; +"staking.stake.with.selected.title" = "Stake com selecionado"; +"staking.start.change.collators.custom.title" = "Fazer stake com os seus validadores"; +"staking.start.change.collators.suggested.subtitle" = "O algoritmo da FEARLESS selecionou uma lista de validadores recomendados com base nos seguintes critérios:"; +"staking.start.change.collators.suggested.title" = "Fazer stake com validadores sugeridos pelo algoritmo"; +"staking.start.title" = "Começar staking"; +"staking.stash.can.hint" = "A conta de tipo stash pode meter mais stake e definir o controlador."; +"staking.stash.missing.message" = "A conta stash %@ não está disponível para atualizar a configuração de staking."; +"staking.stash.title" = "Conta de carteira"; +"staking.status.low.stake" = "Stake baixo"; +"staking.status.ready.to.unlock" = "Pronto para desbloquear"; +"staking.story.nominator.page.1" = "Os nomeadores obtêm um rendimento passivo por bloquearem os seus tokens para proteger a rede. Para tal, o nomeador deve selecionar um número de validadores para apoiar. O nomeador deve ter cuidado ao selecionar validadores. Se o validador selecionado não se comportar corretamente, as penalidades seriam aplicadas a ambos, com base na gravidade do incidente."; +"staking.story.nominator.page.2" = "A Fearless Wallet fornece apoio para os nomeadores, ajudando-os a selecionar os validadores. A aplicação móvel busca dados da blockchain e compõe uma lista de validadores, que possuem: maior lucro, identidade com informações de contacto, e disponibilidade para receber nomeações. A Fearless Wallet também se preocupa com a descentralização, portanto, se uma pessoa ou uma empresa executar vários nodes de validação, apenas até 2 nodes serão mostrados na lista recomendada."; +"staking.story.nominator.title" = "Quem é um validador?"; +"staking.story.reward.page.1" = "As recompensas do staking estão disponíveis para pagamento no final de cada era (6 horas na Kusama e 24 horas na Polkadot). A rede armazena recompensas pendentes durante 84 eras e, na maioria dos casos, os validadores pagam as recompensas para todos. No entanto, os validadores podem-se esquecer ou algo pode acontecer, por isso os nomeadores também podem pagar as suas recompensas por si próprios."; +"staking.story.reward.page.2" = "Embora as recompensas sejam geralmente distribuídas por validadores, a Fearless Wallet ajuda alertando se há recompensas pendentes que estão perto de expirar. Receberá alertas sobre esta e outras atividades no separador de staking"; +"staking.story.reward.title" = "Recompensas recebidas"; +"staking.story.staking.page.1" = "O staking é uma opção para obter rendimento passivo ao bloquear os seus tokens na rede. As recompensas de staking são alocadas a cada época (6 horas na Kusama e 24 horas na Polkadot). Pode fazer stake no prazo que quiser, e para desbloquear os seus tokens, precisa de esperar pelo fim do período de desbloqueamento, tornando os seus tokens disponíveis para serem resgatados."; +"staking.story.staking.page.2" = "O staking é uma parte importante da segurança e fiabilidade da rede. Qualquer pessoa pode executar nodes de validador, mas apenas aqueles que possuem tokens suficientes em stake serão eleitos pela rede para participar da composição de novos blocos e receber as recompensas. Os validadores muitas vezes não têm tokens suficientes, então são os nomeadores que os estão a ajudar, ao bloquear os seus tokens para que alcancem a quantia de stake necessária."; +"staking.story.staking.title" = "O que é staking?"; +"staking.story.validator.page.1" = "O validador corre um node da blockchain 24 horas por dia, 7 dias por semana e é necessário ter um stake bloqueado suficientemente grande (pertencente e fornecido pelos nomeadores) para ser eleito pela rede. Os validadores devem manter o desempenho e a fiabilidade dos seus nodes para serem recompensados. Ser validador é quase um trabalho a tempo inteiro, há empresas que estão focadas em ser validadoras nas redes blockchain."; +"staking.story.validator.page.2" = "Qualquer pessoa pode ser validador e correr um node da blockchain, porém isso requer um certo nível de habilidade técnica e responsabilidade. As redes Polkadot e Kusama têm um programa, denominado Thousand Validators Program, para fornecer apoio a iniciantes. Além disso, a própria rede sempre recompensará mais validadores, que têm menos participação (mas o suficiente para serem eleitos) para melhorar a descentralização."; +"staking.story.validator.title" = "Quem é um validador?"; +"staking.suggested.collators.title" = "Validadores sugeridos"; +"staking.switch.account.to.stash" = "Mude a sua conta para stash para definir o controlador."; +"staking.total.rewards_v1.9.0" = "Recompensado"; +"staking.unbond.payee.reset.message" = "O destino da recompensa será alterado para sua conta para evitar um remanescente vinculado, uma vez que o staking será interrompido"; +"staking.unbond_v1.9.0" = "Unstake"; +"staking.unbonding.all.message" = "O saldo restante de staking cairá abaixo do valor mínimo e será também adicionado ao valor sem staking"; +"staking.unbonding.all.title" = "Parar o staking"; +"staking.unbonding.empty.list_v1.9.0" = "As transações de unstaking aparecerão aqui"; +"staking.unbonding.format" = "Unstaking: %@"; +"staking.unbonding.hint" = "O seus tokens estarão disponíveis para resgatar após o período de desvinculação."; +"staking.unbonding.limit.reached.title" = "Limite de pedidos de unstaking atingido"; +"staking.unstaking.period" = "Período de Unstaking"; +"staking.validator.apy.percent" = "Recompensa estimada (% APY)"; +"staking.validator.estimated.reward" = "Recompensa estimada"; +"staking.validator.info.nominators" = "%@ (max %@)"; +"staking.validator.info.title" = "Informação do validador"; +"staking.validator.my.oversubscribed.message" = "Excesso de subscrições. Não receberá recompensas do validador nesta era."; +"staking.validator.nominators" = "Nomeadores"; +"staking.validator.other.oversubscribed.message" = "Excesso de subscrições. Apenas os nomeadores com maior valor de stake receberão recompensas."; +"staking.validator.own.stake" = "Pertencente ao validador"; +"staking.validator.search.empty.title" = "Sem resultados de pesquisa.\nTenha a certeza de que digitou o endereço completo da conta"; +"staking.validator.search.placeholder" = "Pesquisar por endereço ou nome"; +"staking.validator.slashed.desc" = "O Validador é penalizado por se comportar mal (por exemplo, ficar offline, atacar a rede ou executar softwares modificados) na rede."; +"staking.validator.status.elected" = "Eleito"; +"staking.validator.status.unelected" = "Não eleito"; +"staking.validator.summary.description" = "Em atualizações futuras, adicionaremos recursos de staking para os validadores. \n \n #StayFearless"; +"staking.validator.summary.title" = "Staking para validador"; +"staking.validator.total.stake" = "Stake total"; +"staking.validator.total.stake.token" = "Stake total (%@)"; +"staking.warning.tiny.payout" = "A recompensa é menor que a taxa da rede."; +"staking.year.period.format" = "%@ anualmente"; +"staking.your.allocated.description" = "O seu stake está alocado nos seguintes validadores."; +"staking.your.collator.title" = "Seu coletor"; +"staking.your.elected.format" = "Eleito (%@)"; +"staking.your.inactive.description" = "Validadores que não foram eleitos nesta era."; +"staking.your.nominated.format" = "Nomeado: %@"; +"staking.your.not.allocated.description" = "Outros, que estão ativos sem a sua alocação de stake."; +"staking.your.not.elected.format" = "Não eleito (%@)"; +"staking.your.oversubscribed.message" = "Os seus tokens estão alocados em validadores com excesso de assinaturas. Não receberá recompensas nesta era."; +"staking.your.selected.format" = "Selecionado (%@)"; +"staking.your.stake" = "O seu stake"; +"staking.your.validator.title" = "O seu validador"; +"staking.your.validators.changing.title" = "Os seus validadores mudarão na próxima era."; +"staking.your.validators.title" = "Os seus validadores"; "stash.account.issue.action" = "Importar conta stash"; -"banners.view.factory.backup.title" = "Faça backup da sua carteira"; -"backup.select.wallet.title" = "Selecione a carteira a ser importada"; -"backup.select.wallet.create.button" = "Criar nova conta"; -"backup.wallet.title" = "Carteira de backup"; -"backup.wallet.seed" = "Mostrar semente crua"; -"banners.view.factory.backup.action.title" = "Faça backup agora"; -"backup.wallet.imported.description" = "Você importou a carteira com sucesso"; -"backup.wallet.name.editing.description" = "Exemplo: Poupança, Investimentos, Crowdloans, Staking. Este nome de conta será exibido apenas para você e armazenado localmente em seu dispositivo móvel"; -"banners.view.factory.backup.subtitle" = "Se você perder seu dispositivo, seus fundos serão perdidos para sempre"; -"banners.view.factory.xor.title" = "Comprar token XOR"; -"banners.view.factory.xor.subtitle" = "Comprar ou vender tokens XOR com Euros"; -"banners.view.factory.xor.action.title" = "Comprar XOR"; -"backup.not.backed.up.title" = "Não foi feito backup!"; -"backup.not.backed.up.message" = "Se o seu dispositivo for perdido ou roubado, você perderá sua carteira e todos os seus fundos para sempre"; -"backup.not.backed.up.confirm" = "Arrisco-me a fazê-lo"; +"stash.account.issue.message" = "A conta stash %s não está disponível. Por favor, importe a conta Stash seguindo as etapas necessárias"; +"state.common" = "Estado"; +"stories.bottom.close.button" = "Boas notícias!"; +"stories.version2.slide1.subtitle" = "A Fearless Wallet suporta agora mais redes e tokens: Polkadot (DOT), Kusama (KSM), Moonriver (MOVR), Karura (KAR), Shiden (SDN), SORA (XOR), Bifrost (BNC), KILT (KILT) e outros..."; +"stories.version2.slide1.title" = "Mais redes, Mais tokens"; +"stories.version2.slide2.subtitle" = "Todas as suas contas permanecem na aplicação! Cada carteira contém agora muitas contas em redes diferentes. Cada carteira tem um ícone novo e colorido para o ajudar a diferenciar-se entre elas."; +"stories.version2.slide2.title" = "Contas atualizadas para Carteiras"; +"stories.version2.slide3.subtitle" = "Diferentes tokens do ecossistema Polkadot e Kusama estão agora no mesmo ecrã."; +"stories.version2.slide3.title" = "A carteira é agora mais poderosa"; +"stories.version2.slide4.subtitle" = "Por defeito, as carteiras utilizam a mesma chave para todas as contas contidas. É possível substituir as contas dentro de uma carteira por quaisquer contas existentes ou criar novas contas."; +"stories.version2.slide4.title" = "Personalize as suas carteiras"; +"stories.version2.slide5.subtitle" = "Pode ainda importar e exportar as suas contas a partir de outras aplicações no ecossistema.\ +\ +Lembre-se de fazer uma cópia de segurança da sua chave e guardá-la num local seguro e privado (por exemplo, num pedaço de papel)."; +"stories.version2.slide5.title" = "Harmonia com o ecossistema"; +"substrate.crypto.type" = "Criptografia de par de chaves do tipo Substrate"; +"substrate.secret.derivation.path" = "Caminho de derivação secreto da Substrate\ +"; +"swipable.cell.button.hide" = "Ocultar"; +"swipable.cell.button.receive" = "Receber"; +"swipable.cell.button.send" = "Enviar"; +"swipable.cell.button.show" = "Mostrar"; +"swipable.cell.button.teleport" = "teleporte"; +"switch.node" = "Alternar node"; +"switch.node.autoselect.title" = "Seleção automática de nodes"; +"tabbar.crowdloan.attention" = "Verifique o separador Crowdloans \n Atenção necessária"; +"tabbar.crowdloan.title" = "Crowdloans"; +"tabbar.crowdloan.title_v1.9.0" = "Crowdloans"; +"tabbar.settings.title" = "Configurações"; +"terms.and.conditions.accept.and.continue" = "Aceitar e continuar"; +"terms.and.conditions.description" = "Queremos que você saiba exatamente como funcionam os serviços do Cartão SORA, quem e por que precisa dos seus dados. A revisão dessas políticas ajudará você a continuar usando o aplicativo com tranquilidade."; +"terms.and.conditions.general.terms" = "Termos gerais de utilização"; +"terms.and.conditions.privacy.policy" = "Política de Privacidade"; +"terms.and.conditions.sora.community.alert.main" = "A comunidade SORA não coleta nenhum dos seus dados pessoais,"; +"terms.and.conditions.sora.community.alert.secondary" = "mas para obter o cartão SORA e a conta IBAN você precisa passar pelo processo KYC com um emissor do cartão."; +"terms.and.conditions.title" = "Termos e Condições"; +"tranaction.history.others.tab.title" = "Others"; +"transaction.detail.date" = "Data"; +"transaction.detail.status" = "Status"; +"transaction.details.copy.hash" = "Copiar hash"; +"transaction.details.extrinsic.fee" = "Taxa"; +"transaction.details.from" = "De"; +"transaction.details.hash.title" = "Hash extrínseco"; "transaction.details.view.etherscan" = "Ver no Etherscan"; -"accounts.with.shared.secret" = "Contas com um segredo partilhado"; +"transaction.details.view.oklink" = "View in OKX explorer"; +"transaction.details.view.polkascan" = "Ver na Polkascan"; +"transaction.details.view.reefscan" = "View in Reefscan"; +"transaction.details.view.subscan" = "Ver no Subscan"; +"transaction.list.header" = "Todas as transações"; +"transaction.status.completed" = "Concluído"; +"transaction.status.failed" = "Falhou"; +"transaction.status.pending" = "Pendente"; +"transaction.successful" = "Transação bem sucedida!"; +"transfer.title" = "Transferir"; "try.again" = "Tentar novamente"; -"no.access.to.google" = "Sem acesso ao Google"; -"remove.backup.extension.error.message" = "O backup foi criado pela extensão do navegador Fearless Wallet. O aplicativo móvel não pode removê-lo devido à falta de credenciais"; -"backup.wallet.replace.accounts.alert" = "Atualmente você tem uma conta na cadeia %s e um endereço %s que foi adicionado substituindo o par de chaves principal e possui um par específico. No entanto, observe que nosso fluxo atual de backup (exportação) de carteira não suporta o armazenamento de vários pares de chaves. Como resultado, você só pode salvar seu par de chaves principal. \n \n Para garantir a segurança da sua conta chain substituída, recomendamos que você primeiro faça backup dela separadamente antes de prosseguir com o fluxo atual. Depois de fazer backup com êxito de sua conta em cadeia substituída, você poderá prosseguir para o fluxo atual sem preocupações."; -"backup.wallet.replace.several.alert" = "Atualmente você tem várias contas na cadeia que foram adicionadas substituindo o par de chaves principal e possui uma específica. No entanto, observe que nosso fluxo atual de backup (exportação) de carteira não suporta o armazenamento de vários pares de chaves. Como resultado, você só pode salvar seu par de chaves principal. \n \n Para garantir a segurança da sua conta da rede substituída, recomendamos que você primeiro faça backup dela separadamente antes de prosseguir com o fluxo atual. Depois de fazer backup com êxito de sua conta na cadeia substituída, você poderá prosseguir para o fluxo atual sem preocupações."; -"google.backup.choice.google" = "Importar do Google"; -"backup.chain.account" = "Conta de cadeia de backup"; -"backup.create.password.confirm.field.title" = "Confirmar senha"; -"backup.create.password.continue.button" = "Definir senha de backup"; -"backup.create.password.description" = "Definir uma senha criptografará seu backup do Google. Você precisará inserir isso ao restaurar sua carteira"; -"backup.create.password.matched" = "A palavra-passe corresponde"; -"backup.create.password.not.matched" = "A palavra-passe não corresponde"; -"backup.create.password.password.field.title" = "Definir senha"; -"backup.create.password.screen.title" = "Criar senha de backup"; -"backup.create.password.warning" = "Entendo que se eu esquecer minha senha não há como recuperá-la"; -"common.resolve" = "Resolver"; -"google.backup.button.title" = "Conecte-se com o Google"; -"google.backup.choice.json" = "JSON"; -"google.backup.choice.mnemonic" = "Frase mnemônica"; -"google.backup.choice.raw" = "Semente Crua"; -"google.backup.choice.title" = "Selecione a fonte para importação"; -"import.wallets.not.found" = "Nenhuma carteira de importação foi encontrada :("; -"xcm.cross.chain.invalid.address.message" = "De acordo com o endereço fornecido, você está tentando fazer uma transferência na rede errada."; -"wallet.send.eth.dead.recipient.message" = "Saldo Ethereum insuficiente na conta do destinatário impede a conclusão da transferência do token ERC20. Certifique-se de que o destinatário tenha Ethereum suficiente para prosseguir com a transferência."; -"nft.owner.title" = "Possuído"; -"nft.collection.title" = "Coleção"; -"onboarding.preinstalled.wallet.button.text" = "Obtenha uma carteira pré-instalada"; -"nft.tokenid.title" = "ID do token"; -"import.json.invalid.import.type.message" = "Seu arquivo JSON não é válido. Você está tentando importar contas em cadeia baseadas em ETH em vez de contas do Substrate. Use um arquivo JSON adequado para importar contas da cadeia do Substrate primeiro."; -"import.eth.json.invalid.import.type.message" = "Seu arquivo JSON não é válido. Você está tentando importar contas em cadeia baseadas em Substrate em vez de contas ETH. Use um arquivo JSON adequado para importar contas da cadeia ETH."; -"error.unsupported.asset" = "Você está tentando fazer uma transferência do ativo que atualmente não é compatível com o aplicativo. Escolha outro ativo ou solicite outro código QR."; -"common.request" = "Solicitar"; -"required.chains.not.satisfied" = "Não é possível conectar-se à carteira: a rede blockchain solicitada não é suportada ou não está disponível"; -"required.accounts.not.satisfied" = "Não é possível prosseguir: sua carteira não possui as contas necessárias. Certifique-se de ter as contas necessárias configuradas antes de continuar"; -"required.methods.not.satisfied" = "Certos métodos solicitados pelo Wallet Connect não são atualmente suportados pela aplicação. Embora a conexão possa ser estabelecida, certas funcionalidades do dApp podem não estar disponíveis."; -"required.events.not.satisfied" = "Notificações de eventos ausentes: o aplicativo não oferece suporte às notificações de eventos necessárias"; -"onboarding.start.title" = "A carteira DeFi do futuro"; -"common.attention" = "Atenção"; -"common.network.management" = "Gerência de rede"; -"network.management.popular" = "Popular"; -"common.select.all" = "Selecionar tudo"; -"common.selected.count" = "Selecionado: %@"; -"common.connections" = "Conexões"; -"search.by.connection" = "Pesquisar por conexão"; -"common.sign" = "Assinar"; -"review.optional.permissions" = "Rever as permissões opcionais"; -"required.networks" = "Redes necessárias"; -"optional.networks" = "Redes opcionais"; -"review.required.permissions" = "Rever as permissões necessárias"; -"common.methods" = "Métodos"; -"common.rejected" = "Rejeitado"; +"update.needed.text" = "Atualização necessária"; +"username.setup.choose.title" = "Nome da carteira"; +"username.setup.hint" = "Este nome será exibido apenas para si e armazenado localmente no seu dispositivo móvel."; +"username.setup.hint.2.0" = "Exemplo: Poupanças, Investimentos, Crowdloans, Staking. Este nome será exibido apenas para você e armazenado localmente em seu dispositivo móvel."; +"username.setup.title" = "Criar conta"; +"username.setup.title.2.0" = "Criar nova carteira"; +"validator.info.comission.title" = "Comissão"; +"validator.info.min.stake.alert.text" = "Minimum stake among active nominators is %s. To get rewards you have to stake more."; +"validator.info.min.stake.among.active.nominators.text" = "Minimum stake among active nominators"; +"validators.list.empty.message" = "Não foram encontrados validadores"; +"verify.phone.number.title" = "Verify your phone number"; +"vesting.claim.disclaimer.text" = "Due to the unique vesting schedules of each parachain, our app is unable to display the quantity of locked tokens eligible for claiming. Please be advised that initiating a claim may be impractical if the prospective amount is marginal and comparable to the transaction fee involved. For comprehensive insights into your locked balances, consult the Subscan blockexplorer. We urge you to assess the information carefully and proceed at your own discretion."; +"vesting.claim.disclaimer.title" = "Important Notice Regarding Token Claims:"; +"vesting.locked.title" = "Vesting Locked"; +"view.in" = "Ver em %s"; +"view.wallet" = "Ver carteira"; +"wallet.account.locks.democracy" = "Democracia"; +"wallet.account.locks.vesting" = "Vesting"; +"wallet.all.assets.hidden" = "Você escondeu todos os ativos"; +"wallet.asset.buy" = "Comprar"; +"wallet.asset.buy.with" = "Comprar %s com"; +"wallet.asset.receive" = "Receber"; +"wallet.asset.receive.template" = "Receber %s"; +"wallet.asset.transferable.caption" = "Transferível"; +"wallet.asset.your.balance" = "O seu saldo"; +"wallet.assets.total.title" = "Valor dos ativos"; +"wallet.balance.available" = "Disponível"; +"wallet.balance.bonded" = "Staked"; +"wallet.balance.frozen" = "Congelado"; +"wallet.balance.locked" = "Bloqueado"; +"wallet.balance.redeemable" = "Resgatável"; +"wallet.balance.reserved" = "Reservado"; +"wallet.balance.unbonding_v1.9.0" = "Unstaking"; "wallet.connect.connection.complete" = "Conexão de %@ com ethers foi feita com sucesso"; "wallet.connect.connection.dissconnected" = "Desconexão de %@ ether(s) foi feita com sucesso"; -"common.approve" = "Aprovar"; -"common.reject" = "Rejeitar"; -"connect.details" = "Detalhes da conexão"; -"common.message" = "Mensagem"; -"wallet.connect.sign.warning.message" = "Assinar esta mensagem pode ter efeitos colaterais perigosos. Assine apenas mensagens de sites em que você confia totalmente com toda a sua conta."; -"common.warning.capitalized" = "Aviso:"; -"create.new.connection" = "Criar nova conexão"; -"review.permissions" = "Rever permissões"; -"common.events" = "Eventos"; -"common.expiry" = "Expiração"; -"sign.this.message" = "Assinar esta mensagem?"; -"existential.deposit.received.error" = "Depósito existencial não recebido"; -"empty.state.message" = "Nada encontrado para sua solicitação"; "wallet.connect.invalid.url.message" = "Nosso App tem compatibilidade apenas com a Wallet Connect SKD v2, não com a descontinuada SDK v1. Por favor, use a versão apropriada para conectar-se com sucesso."; "wallet.connect.invalid.url.title" = "Aviso: Wallet Connect SDK v1 não é compatível"; -"validator.info.comission.title" = "Comissão"; -"network.managment.favourite" = "Favorito"; -"nfts.collection.count" = "Quantity: %d/%d"; -"sora.bridge.low.amount.alert" = "Currently, there's a min. amount 0.05 KSM for bridging to ensure the stability and security of the SORA Network. We appreciate your understanding."; -"common.available.networks" = "Available networks"; -"common.my.networks" = "My networks"; -"nft.collection.my.nfts" = "My NFTs"; -"nft.collection.available.nfts" = "Available NFTs in the %s collection"; -"nft.creator.title" = "Creator"; -"common.price" = "Price"; -"nft.share.address" = "My public address to receive: %s"; -"nft.list.empty.message" = "There aren't any NFTs yet. Buy or mint NFTs to see them here."; -"transaction.details.view.reefscan" = "View in Reefscan"; -"sora.bridge.amount.less.fee" = "The amount you're trying to transfer is insufficient to cover the transaction fees on the %s network. Although the transaction won't process, you'll still be charged the fees on the Sora network."; -"nfts.filters.spam" = "SPAM"; -"nfts.filters.airdrop" = "AirDrop"; -"nfts.filters.title" = "Hide NFTs"; -"common.existential.warning.max.amount" = "Set max amount"; -"sora.bridge.low.amaunt.polkadot.alert" = "Currently, there's a min. amount 1.1 DOT for bridging to ensure the stability and security. We appreciate your understanding."; -"vesting.locked.title" = "Vesting Locked"; -"vesting.claim.disclaimer.text" = "Due to the unique vesting schedules of each parachain, our app is unable to display the quantity of locked tokens eligible for claiming. Please be advised that initiating a claim may be impractical if the prospective amount is marginal and comparable to the transaction fee involved. For comprehensive insights into your locked balances, consult the Subscan blockexplorer. We urge you to assess the information carefully and proceed at your own discretion."; -"vesting.claim.disclaimer.title" = "Important Notice Regarding Token Claims:"; -"nft.choose.recipient.title" = "Choose recipient"; -"balance.locks.liquidity.pools.row.title" = "Liquidity Pools"; -"balance.locks.governance.row.title" = "Governance"; -"balance.locks.nomination.pools.row.title" = "Nomination Pools"; -"balance.locks.screen.title" = "Locked details"; -"tranaction.history.others.tab.title" = "Others"; -"common.commission" = "Commission"; -"verify.phone.number.title" = "Verify your phone number"; -"send.all.title" = "Send all tokens and reap the account"; -"common.and.others.placeholder" = "%s & others"; -"common.contacts" = "Contacts"; -"contacts.empty.message" = "No contacts found"; -"common.existential.error.message" = "Esta transação terá como resultado que a conta passe abaixo do Depósito Existencial(%@), o que fará com que ela seja colhida (a conta será apagada da blockchain para conservar espaço)."; -"common.start" = "Start"; -"wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%s) on the destination account will be less than the minimal balance (%s). Please increase the amount by %s"; \ No newline at end of file +"wallet.connect.sign.warning.message" = "Assinar esta mensagem pode ter efeitos colaterais perigosos. Assine apenas mensagens de sites em que você confia totalmente com toda a sua conta."; +"wallet.contacts.empty.title" = "A suas contas e contatos para quem estava enviando transferências aparecerão aqui"; +"wallet.contacts.empty.title_v1.10" = "Suas contas e contatos\ +para quem estava enviando\ +as transferências aparecerão aqui"; +"wallet.contacts.search.placeholder" = "Endereço da conta ou nome da conta"; +"wallet.contacts.search.placeholder_v1.10" = "Pesquisar por endereço ou nome"; +"wallet.empty.description" = "Entradas e Saídas \ +as operações aparecerão aqui"; +"wallet.extrinsic.details.title" = "Detalhes extrínsecos"; +"wallet.fee.over.existential.deposit" = "Você deve ter fundos para pagar a taxa e permanecer acima do saldo mínimo."; +"wallet.filters.extrinsics" = "Outras transações"; +"wallet.filters.header" = "Mostrar"; +"wallet.filters.rewards.and.slashes" = "Recompensas e Penalizações"; +"wallet.filters.title" = "Filtros"; +"wallet.filters.transfers" = "Transferências"; +"wallet.history.title_v1.9.0" = "Histórico"; +"wallet.manage.assets" = "Gerir ativos"; +"wallet.managment.select.wallet.title" = "Selecione sua carteira"; +"wallet.options.delete" = "Deletar carteira"; +"wallet.options.details" = "Detalhes da carteira"; +"wallet.options.export" = "Exportar carteira"; +"wallet.options.title" = "Opções da carteira"; +"wallet.receive.description" = "Mostrar este código QR ao remetente"; +"wallet.receive.navigation.title" = "Receba %s"; +"wallet.receive.qr.description" = "Mostre este código QR ao remetente"; +"wallet.receive.share.message" = "O meu endereço %@ para receber %@ :"; +"wallet.search.accounts" = "as minhas contas"; +"wallet.search.contacts" = "Contatos"; +"wallet.search.empty.title" = "Certifique-se de que o endereço é\ +da rede correta"; +"wallet.search.empty.title_v1.10.0" = "O formato do endereço é inválido.\ +Certifique-se de que o endereço\ +pertence à rede certa"; +"wallet.send.amount.title" = "Montante"; +"wallet.send.asset.title" = "Ativo"; +"wallet.send.available.balance" = "Saldo disponível"; +"wallet.send.balance.details" = "Detalhes do saldo"; +"wallet.send.balance.minimal" = "Saldo mínimo"; +"wallet.send.balance.total" = "Saldo total"; +"wallet.send.balance.total.after.transfer" = "Total após transferência"; +"wallet.send.confirm.title" = "Confirmar transferência"; +"wallet.send.dead.recipient.message" = "A sua transferência falhará, pois o valor final na conta de destino será menor que o depósito existente. Por favor, tente aumentar a quantia."; +"wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%1$s) on the destination account will be less than the minimal balance (%2$s). Please increase the amount by %3$s"; +"wallet.send.dead.recipient.title" = "O montante é demasiado baixo"; +"wallet.send.eth.dead.recipient.message" = "Saldo Ethereum insuficiente na conta do destinatário impede a conclusão da transferência do token ERC20. Certifique-se de que o destinatário tenha Ethereum suficiente para prosseguir com a transferência."; +"wallet.send.existential.warning" = "A sua transferência removerá a conta do blockstore, pois tornará o saldo total inferior ao depósito existente."; +"wallet.send.fee.title" = "Taxa de transferência"; +"wallet.send.navigation.title" = "Enviar %s"; +"wallet.send.phishing.warning.text" = "O endereço seguinte: %@ é conhecido por ser utilizado em atividades de phishing, pelo que não recomendamos o envio de tokens para esse endereço. Gostaria de prosseguir na mesma?"; +"wallet.send.phishing.warning.title" = "Alerta de fraude"; +"wallet.send.receiver.title" = "Para"; +"wallet.send.tip.title" = "Gorjeta"; +"wallet.send.title" = "Enviar"; +"wallet.settings" = "Configurações da carteira"; +"wallet.transaction.detail.copy.id" = "Copiar ID"; +"wallet.transaction.history.empty.message" = "O histórico de transações aparecerá aqui"; +"wallet.transaction.history.error.message" = "O serviço está atualmente indisponível"; +"wallet.transaction.history.filter.title" = "Mostrar"; +"wallet.transaction.history.unsupported.message" = "O histórico das transações para esta rede ainda não é suportado"; +"wallets.managment.add.new.wallet" = "Adicionar nova carteira"; +"what.accounts.for.export" = "Que contas na carteira pretende exportar?"; +"xcm.cross.chain.button.title" = "Taxa entre cadeias"; +"xcm.cross.chain.invalid.address.message" = "De acordo com o endereço fornecido, você está tentando fazer uma transferência na rede errada."; +"xcm.cross.chain.invalid.address.message" = "De acordo com o endereço fornecido, você está tentando fazer uma transferência na rede errada."; +"xcm.cross.chain.invalid.address.title" = "Não é endereço de rede"; +"xcm.destination.network.fee.title" = "Taxa de rede de destino"; +"xcm.destination.network.title" = "Rede de destino"; +"xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; +"xcm.mywallets.button.title" = "Minhas carteiras"; +"xcm.origin.network.fee.title" = "Taxa da rede de origem"; +"xcm.origin.network.title" = "Rede de origem"; +"xcm.title" = "Taxa entre cadeias"; +"your.validators.change.validators.title" = "Alterar validadores"; +"your.validators.stop.nominating.title" = "Deixar de nomear"; +"your.validators.validator.total.stake" = "Total em stake: %@"; +"сurrencies.stub.text" = "Moedas"; \ No newline at end of file diff --git a/fearless/ru.lproj/Localizable.strings b/fearless/ru.lproj/Localizable.strings index a51e6e6ba1..0e8c612a15 100644 --- a/fearless/ru.lproj/Localizable.strings +++ b/fearless/ru.lproj/Localizable.strings @@ -91,11 +91,15 @@ "application.status.view.copied.description" = "Адрес кошелька скопирован"; "application.status.view.copied.title" = "Адрес скопирован"; "application.status.view.hash.copied.description" = "Хеш удачно скопирован"; +"application.status.view.hash.copied.description" = "Хеш скопирован"; +"application.status.view.hash.copied.title" = "Хеш скопирован"; "application.status.view.hash.copied.title" = "Хеш скопирован"; "application.status.view.offline.description" = "Пожалуйста, проверьте Ваше соединение и повторите попытку"; "application.status.view.offline.title" = "Похоже, что вы не в сети."; "application.status.view.reconnected.description" = "Соединения успешно восстановлено"; +"application.status.view.reconnected.description" = "Соединение успешно восстановлено"; "application.status.view.reconnected.title" = "Подключено"; +"application.status.view.reconnected.title" = "Переподключено"; "applied.fearless.wallet.bonus" = "Бонус кошелька Fearless применен"; "apply.fearless.wallet.bonus" = "Нажмите, чтобы применить бонус кошелька Fearless"; "ask.biometry.reason" = "Авторизуйтесь для доступа к аккаунту"; @@ -152,6 +156,7 @@ "backup.wallet.replace.several.alert" = "You currently have several chain accounts that has been added by replacing the main key pair, and it possesses a specific one. However, please note that our current wallet backup (export) flow does not support the storage of multiple key pairs. As a result, you can only save your main key pair. \n \n To ensure the safety of your replaced chain account, we recommend that you first back it up separately before proceeding with the current flow. Once you have successfully backed up your replaced chain account, you can proceed to the current flow without any concerns."; "backup.wallet.seed" = "Show Raw Seed"; "backup.wallet.title" = "Backup wallet"; +"balance.locks.blocked.row.title" = "Blocked"; "balance.locks.governance.row.title" = "Governance"; "balance.locks.liquidity.pools.row.title" = "Liquidity Pools"; "balance.locks.nomination.pools.row.title" = "Nomination Pools"; @@ -176,6 +181,7 @@ Euro cash"; "common.action.receive" = "Получить"; "common.action.send" = "Перевести"; "common.action.teleport" = "Телепорт"; +"common.activation.required" = "Activation Required"; "common.add" = "Добавить"; "common.address" = "Адрес"; "common.advanced" = "Продвинутый"; @@ -280,6 +286,7 @@ Euro cash"; "common.search.start.title" = "Здесь появятся результаты поиска"; "common.secret.derivation.path" = "Последовательность для вывода секрета"; "common.select" = "Выбрать"; +"common.select" = "Выбрать"; "common.select.all" = "Select all"; "common.select.asset" = "Выбор актива"; "common.select.network" = "Выбрать Сеть"; @@ -395,6 +402,7 @@ Euro cash"; "error.invalid.address" = "Неверный адрес для выбранной сети"; "error.message.enter.the.name" = "Введите имя…"; "error.message.enter.the.url.address" = "Введите URL-адрес…"; +"error.scan.qr.disabled.asset" = "The asset you want to transfer is either unsupported or turned off. Go to Asset Management, activate the asset, and then scan the QR code again."; "error.unsupported.asset" = "You're trying to make a transfer of the asset which isn't currently supported on the App. Please choose another asset or request another QR code."; "ethereum.crypto.type" = "Тип криптографической пары ключей Ethereum"; "ethereum.secret.derivation.path" = "Путь деривации секрета Ethereum"; @@ -460,6 +468,26 @@ Euro cash"; "label.testnet" = "Testnet"; "language.title" = "Язык"; "learn.more.about.crowdloans" = "Узнайте больше о Crowdloans"; +"lp.apy.alert.text" = "Farming reward for liquidity provision"; +"lp.apy.alert.title" = "Strategic Bonus APY"; +"lp.apy.title" = "Strategic Bonus APY"; +"lp.available.pools.title" = "Available pools"; +"lp.confirm.liquidity.screen.title" = "Confirm Liquidity"; +"lp.confirm.liquidity.warning.text" = "Output is estimated. If the price changes more than 0.5% your transaction will revert."; +"lp.network.fee.alert.text" = "Network fee is used to ensure SORA system’s growth and stable performance."; +"lp.network.fee.alert.title" = "Network fee"; +"lp.pool.details.title" = "Pool details"; +"lp.pool.remove.warning.text" = "Removing pool tokens converts your position back into underlying tokens at the current rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive."; +"lp.pool.remove.warning.title" = "NOTE"; +"lp.remove.button.title" = "Remove Liquidity"; +"lp.remove.liquidity.screen.title" = "Remove Liquidity"; +"lp.reward.token.text" = "Earn %@"; +"lp.reward.token.title" = "Rewards Payout In"; +"lp.slippage.title" = "Slippage"; +"lp.supply.button.title" = "Supply Liquidity"; +"lp.supply.liquidity.screen.title" = "Supply Liquidity"; +"lp.token.pooled.text" = "Your %@ Pooled"; +"lp.user.pools.title" = "User pools"; "manage.assets.account.missing.text" = "Добавить аккаунт..."; "manage.assets.search.hint" = "Поиск по токену"; "members.common" = "Участники"; @@ -483,6 +511,7 @@ Euro cash"; "network.info.address" = "Адрес ноды"; "network.info.name" = "Имя ноды"; "network.info.title" = "Информация ноды"; +"network.issue.main" = "Connection Error: Unable to connect to the network. Please try again."; "network.issue.network.unavailible" = "Сеть не доступа"; "network.issue.node.unavailable" = "Нода не доступна"; "network.issue.notofication" = "Уведомления"; @@ -504,6 +533,7 @@ Euro cash"; "nft.list.empty.message" = "There aren't any NFTs yet. Buy or mint NFTs to see them here."; "nft.owner.title" = "Owned"; "nft.share.address" = "My public address to receive: %s"; +"nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; "nft.stub.text" = "NFTs скоро будут здесь"; "nft.stub.title" = "Извините!"; "nft.tokenid.title" = "Token ID"; @@ -588,17 +618,20 @@ Euro cash"; "polkaswap.disclaimer.title" = "Disclaimer"; "polkaswap.liqudity.fee.info" = "A portion of each trade (0.3%) goes to liquidity providers as a protocol incentive."; "polkaswap.liquidity.provider.fee" = "Комиссия поставщика ликвидности"; +"polkaswap.liquidity.provider.fee" = "Liquidity Provider Fee "; "polkaswap.market.alert.choose.action" = "Choose asset"; "polkaswap.market.alert.message" = "You haven't completed the swap adjustments. One or both assets haven't been chosen, so the market selection isn't available."; "polkaswap.market.alert.title" = "Market selection isn't available"; "polkaswap.market.algorithm.title" = "Market algorithm"; "polkaswap.market.smart.description" = "SMART liquidity routing ensures the best price for any transaction by combining only the best price options from all available markets. When available, Token Bonding Curve (TBC) will be used for liquidity as long as the asset price is more affordable than from other sources, upon which the XYK pool is utilized."; "polkaswap.market.stub" = "Рынок:"; +"polkaswap.market.stub" = "Market"; "polkaswap.market.tbc.description" = "TBC — buying only from the Token Bonding Curve (Primary Market). There is a possibility that the price can become unfavorable compared to the XYK pool (Secondary Market), but the value received from the vested rewards might turn out to be much more favorable over time."; "polkaswap.market.xyk.description" = "XYK — buying only from the XYK pool (Secondary Market). Traditional XYK pool swap where anyone can buy or sell assets by shifting the market maker’s position on the x*y=k curve."; "polkaswap.max.received" = "Maximums sold "; "polkaswap.maximum.sold.info" = "Your transaction will revert if there is a large, unfavorable price movement before it is confirmed."; "polkaswap.min.received" = "Будет получено минимум"; +"polkaswap.min.received" = "Min received "; "polkaswap.minimum.received.info" = "Your transaction will revert if there is a large, unfavorable price movement before it is confirmed."; "polkaswap.network.fee" = "Network fee "; "polkaswap.network.fee.info" = "Network fee is used to ensure SORA system's growth and stable performance."; @@ -833,9 +866,12 @@ Euro cash"; "staking.pool.create.root" = "Root"; "staking.pool.create.stateToggler" = "State toggler"; "staking.pool.create.title" = "Создать пул"; +"staking.pool.create.title" = "Создать пул"; "staking.pool.info.title" = "Информация о пуле"; "staking.pool.management.select.validators.subtitle" = "Для продолжения необходимо выбрать валидаторов"; +"staking.pool.management.select.validators.subtitle" = "Чтобы продолжить, Вам нужно \n выбрать валидаторов"; "staking.pool.management.select.validators.title" = "Выберите валидаторов пула"; +"staking.pool.management.select.validators.title" = "Выберите валидаторов для пула"; "staking.pool.rewards.delay.text" = "Делайте ставки в любое время. Вы начнете зарабатывать проценты после %s"; "staking.pool.select.validators.manual" = "Выберите вручную"; "staking.pool.select.validators.suggested" = "Выбрать предложенный"; @@ -893,6 +929,7 @@ Euro cash"; "staking.round.title" = "раунд #%@"; "staking.select.suggested" = "Select suggested"; "staking.select.validators.confirm.title" = "Выбор валидаторов"; +"staking.select.validators.confirm.title" = "Выбор валидаторов"; "staking.select.validators.custom.button.title" = "Выбрать самому"; "staking.select.validators.custom.desc" = "Вы должны доверять своим валидаторам, чтобы они действовали грамотно и честно. Принятие решения исключительно на основе их текущей прибыльности может привести к снижению прибыли или даже потере средств."; "staking.select.validators.custom.title" = "Стейкинг с выбранными валидаторами"; @@ -1025,9 +1062,9 @@ Euro cash"; "transaction.details.from" = "От"; "transaction.details.hash.title" = "Хеш транзакции"; "transaction.details.view.etherscan" = "Посмотреть в Etherscan"; +"transaction.details.view.oklink" = "View in OKX explorer"; "transaction.details.view.polkascan" = "Посмотреть в Polkascan"; "transaction.details.view.reefscan" = "View in Reefscan"; -"transaction.details.view.oklink" = "View in OKX explorer"; "transaction.details.view.subscan" = "Просмотреть в Subscan"; "transaction.list.header" = "Все транзакции"; "transaction.status.completed" = "Успешно"; @@ -1043,6 +1080,8 @@ Euro cash"; "username.setup.title" = "Создать аккаунт"; "username.setup.title.2.0" = "Создать новый кошелек"; "validator.info.comission.title" = "Comission"; +"validator.info.min.stake.alert.text" = "Minimum stake among active nominators is %s. To get rewards you have to stake more."; +"validator.info.min.stake.among.active.nominators.text" = "Minimum stake among active nominators"; "validators.list.empty.message" = "Валидаторы не найдены"; "verify.phone.number.title" = "Verify your phone number"; "vesting.claim.disclaimer.text" = "Due to the unique vesting schedules of each parachain, our app is unable to display the quantity of locked tokens eligible for claiming. Please be advised that initiating a claim may be impractical if the prospective amount is marginal and comparable to the transaction fee involved. For comprehensive insights into your locked balances, consult the Subscan blockexplorer. We urge you to assess the information carefully and proceed at your own discretion."; @@ -1052,6 +1091,7 @@ Euro cash"; "view.wallet" = "Посмотреть кошелек"; "wallet.account.locks.democracy" = "Демократия"; "wallet.account.locks.vesting" = "Вестинг"; +"wallet.all.assets.hidden" = "You have hidden all assets"; "wallet.asset.buy" = "Купить"; "wallet.asset.buy.with" = "Купить %s с"; "wallet.asset.receive" = "Получить"; @@ -1113,6 +1153,7 @@ Euro cash"; "wallet.send.balance.total.after.transfer" = "Общий после перевода"; "wallet.send.confirm.title" = "Подтвердить"; "wallet.send.dead.recipient.message" = "Ваш перевод не состоится, так как окончательная сумма на целевом счете будет меньше, чем минимальный баланс. Пожалуйста, попробуйте увеличить сумму."; +"wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%1$s) on the destination account will be less than the minimal balance (%2$s). Please increase the amount by %3$s"; "wallet.send.dead.recipient.title" = "Сумма слишком мала"; "wallet.send.eth.dead.recipient.message" = "Insufficient Ethereum balance in the recipient's account prevents the completion of ERC20 token transfer. Please ensure the receiver has enough Ethereum to proceed with the transfer."; "wallet.send.existential.warning" = "Ваш перевод удалит аккаунт, так как общий баланс станет ниже минимального."; @@ -1133,9 +1174,11 @@ Euro cash"; "what.accounts.for.export" = "Какие аккаунты в кошельке вы хотите экспортировать?"; "xcm.cross.chain.button.title" = "Cross Chain"; "xcm.cross.chain.invalid.address.message" = "According to the address provided you're trying to make a transfer on the wrong network."; +"xcm.cross.chain.invalid.address.message" = "According to the address provided you're trying to make a transfer on the wrong network."; "xcm.cross.chain.invalid.address.title" = "Is not network address"; "xcm.destination.network.fee.title" = "Destination Network Fee"; "xcm.destination.network.title" = "Destination network"; +"xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; "xcm.mywallets.button.title" = "My wallets"; "xcm.origin.network.fee.title" = "Origin Network Fee"; "xcm.origin.network.title" = "Origin network"; @@ -1144,130 +1187,4 @@ Euro cash"; "your.validators.stop.nominating.title" = "Stop nominating"; "your.validators.validator.total.stake" = "Всего в стейкинге: %@"; "сurrencies.stub.text" = "Токены"; -"cant.fetch.refresh" = "Не удается получить данные, нажмите, чтобы обновить."; -"card.attention.text" = "Внимание"; -"card.hub.manage.card" = "Управление картой SORA"; -"card.hub.manage.card.alert.message" = "Для управления картой SORA Card установите официальное приложение SORA Card. Нажмите OK, чтобы перейти в App Store."; -"card.hub.manage.card.alert.title" = "Скачать приложение SORA Card"; -"card.hub.settings.logout.button" = "Выйти"; -"card.hub.settings.logout.description" = "Вы собираетесь выйти из SORA Card. У вас по-прежнему будет доступ непосредственно в приложении SORA Card, но баланс больше не будет доступен для вас в кошельке SORA."; -"card.hub.settings.logout.title" = "Выйти из SORA Card"; -"card.hub.settings.title" = "Настройки карты"; -"card.hub.title" = "Card details"; -"card.hub.update.button" = "Обновите сейчас"; -"card.hub.update.description" = "Вы используете устаревшую версию приложения, некоторые части могут работать не так, как задумано."; -"card.hub.update.title" = "Обновите приложение до последней версии"; -"card.issuance.screen.free.card.description" = "Чтобы выпустить карту БЕСПЛАТНО: \ -Храните, стейкайте или предоставляйте ликвидность XOR на сумму не менее €%@ на своем счете SORA"; -"card.issuance.screen.free.card.get.xor" = "Получить %@ XOR"; -"card.issuance.screen.paid.card.description" = "Единовременная плата за выпуск карты"; -"card.issuance.screen.paid.card.note" = "Примечание: выпуск платной карты будет доступен позднее."; -"card.issuance.screen.paid.card.pay.euro" = "Оплатить €%@ комиссию за выпуск"; -"card.issuance.screen.paid.card.title" = "€%@ комиссия за выпуск"; -"card.issuance.screen.title" = "Выпуск карты"; -"card.or" = "или"; -"card.update.button" = "Обновите приложение"; -"card.update.title" = "Обновите приложение до последней версии, чтобы получить доступ к SORA Card"; -"cardhub.coming.soon" = "Управление картами скоро появится"; -"cardhub.exchange" = "Обмен"; -"cardhub.freeze" = "Заморозить"; -"cardhub.iban.title" = "Данные счета IBAN"; -"cardhub.top.up" = "Пополнить"; -"cardhub.transfer" = "Передача"; -"change.email.title" = "Сменить свой email"; -"common.back" = "Назад"; -"common.change.email" = "Использовать другой email"; -"common.no.spam" = "Никакого спама! Только для защиты вашего аккаунта"; -"common.resend.code" = "Отправить код снова"; -"common.resend.link" = "Отправить ссылку повторно"; -"common.resend.timer" = "Отправить повторно в %02d:% 02d"; -"common.send.code" = "Отправить SMS-код"; -"common.send.link" = "Отправить ссылку"; -"common.support" = "Поддержка"; -"common.try.again" = "Повторить"; -"common.user.not.found" = "Номер мобильного телефона не найден, проверьте его еще раз и повторите попытку."; -"details.already.have.card" = "У меня уже есть карта"; -"details.already.used.free.try" = "Вы уже использовали свою бесплатную попытку"; -"details.annual.service.fee" = "€0 годовая плата за обслуживание"; -"details.description" = "Получите европейский IBAN-счет и дебетовую карту, доступные в вашем кошельке SORA"; -"details.enough.xor.desription" = "У вас достаточно XOR"; -"details.free.card.issuance" = "Бесплатный выпуск карты"; -"details.free.card.issuance.conditions.euro" = "или €%@ регистрационный взнос"; -"details.free.card.issuance.conditions.xor" = "Если вы храните, стейкаете или предоставляете ликвидность в XOR стоимостью не менее €%@ на вашем аккаунте SORA"; -"details.get.more.xor" = "Получить больше XOR"; -"details.issue.card" = "Получение карты бесплатно"; -"details.need.xor.desription" = "Вам нужно еще %@ XOR (€ %@ )"; -"details.title" = "Пополняйте SORA Card фиатом или криптовалютой и оплачивайте покупки онлайн, в магазине или снимайте деньги в банкомате"; -"enter.email.description" = "Волшебная ссылка будет отправлена\nна ваш email"; -"enter.email.input.field.label" = "Электронная почта"; -"enter.email.title" = "Введите свой email"; -"enter.phone.number.description" = "Вы получите\nкод подтверждения по SMS"; -"enter.phone.number.phone.input.field.label" = "Номер телефона"; -"entry.card.info" = "Информация о карте"; -"get.more.xor.dialog.buy.option" = "Купить XOR с помощью карты"; -"get.more.xor.dialog.deposit.option" = "Получить XOR"; -"get.more.xor.dialog.description" = "Вы можете обменять на XOR или купить за евро"; -"get.more.xor.dialog.swap.option" = "Обменять криптовалюту на XOR"; -"get.prepared.alert" = "У вас есть только %@ бесплатных попыток пройти процедуру KYC. Каждая следующая попытка после этого будет стоить € %@ . Платные попытки станут доступны после следующего обновления приложения."; -"get.prepared.need" = "Для завершения KYC вам необходимо:"; -"get.prepared.ok.title" = "Хорошо, я готов"; -"get.prepared.personal.info.description" = "Заполните форму, указав свое имя и адрес"; -"get.prepared.personal.info.title" = "Отправьте свою личную информацию"; -"get.prepared.proof.address.description" = "Официальный документ (счет за коммунальные услуги, выписка из банка, выписка из правительства или корреспонденция), содержащий ваше полное имя и адрес и не старше 3 месяцев."; -"get.prepared.proof.address.note" = "Примечание. Заявления таких необанков, как Revolut и N26, в настоящее время не считаются действительным подтверждением адреса."; -"get.prepared.proof.address.title" = "Предоставьте фотографию документа, подтверждающего адрес"; -"get.prepared.submit.id.photo.description" = "Паспорт, водительские права, удостоверение личности или вид на жительство"; -"get.prepared.submit.id.photo.title" = "Предоставьте фотографию документа, удостоверяющего вашу личность"; -"get.prepared.take.selfie.description" = "Руководство будет предоставлено в ходе процесса"; -"get.prepared.take.selfie.title" = "Сделайте селфи"; -"get.prepared.title" = "Подготовьтесь"; -"iban.frozen.description" = "Ваш IBAN был заморожен.\ -Чтобы узнать больше, свяжитесь с %@"; -"iban.pending.description" = "Выдача вашего IBAN ожидается.\ -В случае ожидания более 72 часов свяжитесь с нами по адресу %@"; -"iban.suspended.description" = "Ваш IBAN был приостановлен.\ -Чтобы узнать больше, свяжитесь с %@"; -"kyc.result.verification.in.progress" = "Выполняется проверка"; -"kyc.result.verification.in.progress.description" = "Вы успешно завершили подачу заявки на KYC. Решение на стадии рассмотрения, и вы можете ожидать его в ближайшее время.\n\nОбычно решение принимается в тот же день, но в некоторых случаях может потребоваться до 3 дней."; -"log.out" = "Выйти"; -"login.title" = "Войдите или зарегистрируйтесь"; -"no.free.kyc.attempts.description" = "Вы использовали все бесплатные попытки прохождения процедуры KYC.\n\nПросим вас дождаться следующего обновления приложения, чтобы продолжить с оплачиваемыми попытками."; -"no.free.kyc.attempts.title" = "Нет больше бесплатных попыток"; -"otp.error.message.wrong.code" = "Неверный код. Пожалуйста, попробуйте еще раз."; -"paid.attempts.available.later" = "Платные попытки будут доступны позднее"; -"payment.widget.unavailable.confirm" = "Да, я понимаю"; -"payment.widget.unavailable.description" = "Приносим извинения за неудобства. Мы прилагаем все усилия для решения данной проблемы. Пожалуйста, повторите попытку позже."; -"payment.widget.unavailable.message" = "Виджет оплаты в настоящее время недоступен"; -"payment.widget.unavailable.title" = "Виджет недоступен"; -"select.country.title" = "Выберите свою страну"; -"status.not.started" = "Получить SORA Card"; -"terms.and.conditions.confirm.description" = "Продолжая, вы подтверждаете, что прочитали, поняли и приняли эти правила"; -"terms.and.conditions.sora.community.alert" = "Чтобы получить учетную запись IBAN, необходимую для SORA Card, пользователи должны пройти процедуру KYC у эмитента карты. Это обязательное соответствие. Сообщество SORA не собирает и не будет собирать какие-либо ваши личные данные."; -"unsupported.countries.disclaimer" = "Жители некоторых стран не могут подать заявку на получение SORA Card в данный момент"; -"unsupported.countries.link" = "Посмотреть список"; -"user.registration.first.name.input.filed.label" = "Имя"; -"user.registration.last.name.input.filed.label" = "Фамилия"; -"user.registration.title" = "Представьтесь"; -"verification.failed.description" = "KYC был прекращен или не прошел иным образом."; -"verification.failed.title" = "Проверка не прошла"; -"verification.rejected.description" = "Ваша заявка была отклонена."; -"verification.rejected.screen.attempts.price.disclaimer" = "Любая другая попытка будет стоить вам €%@"; -"verification.rejected.screen.attempts.used" = "Вы использовали свои бесплатные попытки KYC."; -"verification.rejected.screen.try.again.for.euros" = "Повторить снова за €%@"; -"verification.rejected.screen.try.again.for.free" = "Попробуйте еще раз бесплатно"; -"verification.rejected.support" = "Поддержка"; -"verification.rejected.title" = "Заявка отклонена"; -"verification.successful.description" = "Ваша проверка KYC прошла успешно, и мы уже готовимся отправить вам SORA Card!"; -"verification.successful.title" = "Заявка одобрена"; -"verify.email.description" = "Перейдите по волшебной ссылке, которая была отправлена\nна %@ \nДоставка письма может занять несколько минут. Пожалуйста, проверьте папку со спамом, если вы не получили письмо в течение 5 минут."; -"verify.email.resend" = "ПОВТОРНО ОТПРАВИТЬ В %@"; -"verify.email.title" = "Подтвердите ваш email"; -"verify.phone.number.code.input.field.label" = "SMS-код"; -"verify.phone.number.description" = "Введите SMS-код, который был отправлен\n%@"; -"verify.phone.number.send.code" = "ОТПРАВИТЬ КОД"; -"balance.locks.blocked.row.title" = "Заблокировано"; -"wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%s) on the destination account will be less than the minimal balance (%s). Please increase the amount by %s"; -"common.more" = "more"; -"nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; -"wallet.all.assets.hidden" = "You have hidden all assets"; -"xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; +"common.more" = "More"; diff --git a/fearless/tr.lproj/Localizable.strings b/fearless/tr.lproj/Localizable.strings index 2903a3cabc..5443d46296 100644 --- a/fearless/tr.lproj/Localizable.strings +++ b/fearless/tr.lproj/Localizable.strings @@ -91,10 +91,14 @@ "application.status.view.copied.description" = "Cüzdan adresi başarıyla kopyalandı"; "application.status.view.copied.title" = "Adres kopyalandı"; "application.status.view.hash.copied.description" = "Hash başarıyla kopyalandı"; +"application.status.view.hash.copied.description" = "Hash başarıyla kopyalandı"; +"application.status.view.hash.copied.title" = "Hash kopyalandı"; "application.status.view.hash.copied.title" = "Hash kopyalandı"; "application.status.view.offline.description" = "Lütfen bağlantınızı kontrol edin ve tekrar deneyin"; "application.status.view.offline.title" = "Çevrimdışı olduğunuz anlaşılıyor"; "application.status.view.reconnected.description" = "Bağlantı başarıyla kuruldu"; +"application.status.view.reconnected.description" = "Bağlantı başarıyla kuruldu"; +"application.status.view.reconnected.title" = "Yeniden bağlandı"; "application.status.view.reconnected.title" = "Yeniden bağlandı"; "applied.fearless.wallet.bonus" = "Fearless wallet bonusu uygulandı"; "apply.fearless.wallet.bonus" = "Fearless wallet bonusuna katılmak için tıklayın"; @@ -157,6 +161,7 @@ Yolu olmadığını biliyorum"; "backup.wallet.replace.several.alert" = "Şu anda ana anahtar çiftinin değiştirilmesiyle eklenen birkaç zincir hesabınız var ve bu hesapta belirli bir anahtar çifti bulunuyor. Ancak mevcut cüzdan yedekleme (dışa aktarma) akışımızın birden fazla anahtar çiftinin depolanmasını desteklemediğini lütfen unutmayın. Sonuç olarak yalnızca ana anahtar çiftinizi kaydedebilirsiniz. \n \n Değiştirdiğiniz zincir hesabınızın güvenliğini sağlamak için mevcut akışa geçmeden önce öncelikle hesabınızı ayrı olarak yedeklemenizi öneririz. Değiştirdiğiniz zincir hesabınızı başarıyla yedekledikten sonra herhangi bir endişe yaşamadan mevcut akışa geçebilirsiniz."; "backup.wallet.seed" = "Ham Tohumu Göster"; "backup.wallet.title" = "Yedekleme cüzdanı"; +"balance.locks.blocked.row.title" = "Blocked"; "balance.locks.governance.row.title" = "Yönetim"; "balance.locks.liquidity.pools.row.title" = "Likidite Havuzları"; "balance.locks.nomination.pools.row.title" = "Adaylık havuzları"; @@ -180,6 +185,7 @@ Yolu olmadığını biliyorum"; "common.action.receive" = "Almak"; "common.action.send" = "Gönder"; "common.action.teleport" = "Işınlanma"; +"common.activation.required" = "Activation Required"; "common.add" = "Ekle"; "common.address" = "Adres"; "common.advanced" = "Gelişmiş"; @@ -284,6 +290,7 @@ Yolu olmadığını biliyorum"; "common.search.start.title" = "Arama sonuçları burada görünecek."; "common.secret.derivation.path" = "Özel anahtar türetme yolu"; "common.select" = "Seçmek"; +"common.select" = "Seçme"; "common.select.all" = "Hepsini seç"; "common.select.asset" = "Varlık seçin"; "common.select.network" = "Ağ Seçin"; @@ -399,6 +406,7 @@ Yolu olmadığını biliyorum"; "error.invalid.address" = "Seçilen zincir için geçersiz adres"; "error.message.enter.the.name" = "İsmi girin…"; "error.message.enter.the.url.address" = "URL bağlantısını girin..."; +"error.scan.qr.disabled.asset" = "The asset you want to transfer is either unsupported or turned off. Go to Asset Management, activate the asset, and then scan the QR code again."; "error.unsupported.asset" = "Şu anda Uygulamada desteklenmeyen bir varlığın aktarımını yapmaya çalışıyorsunuz. Lütfen başka bir varlık seçin veya başka bir QR kodu isteyin."; "ethereum.crypto.type" = "Ethereum anahtar çifti türü"; "ethereum.secret.derivation.path" = "Ethereum gizli türetme yolu"; @@ -464,6 +472,26 @@ Kaynak: %@"; "label.testnet" = "Testnet"; "language.title" = "Dil"; "learn.more.about.crowdloans" = "Crowdloan'lar hakkında daha fazla öğrenin"; +"lp.apy.alert.text" = "Farming reward for liquidity provision"; +"lp.apy.alert.title" = "Strategic Bonus APY"; +"lp.apy.title" = "Strategic Bonus APY"; +"lp.available.pools.title" = "Available pools"; +"lp.confirm.liquidity.screen.title" = "Confirm Liquidity"; +"lp.confirm.liquidity.warning.text" = "Output is estimated. If the price changes more than 0.5% your transaction will revert."; +"lp.network.fee.alert.text" = "Network fee is used to ensure SORA system’s growth and stable performance."; +"lp.network.fee.alert.title" = "Network fee"; +"lp.pool.details.title" = "Pool details"; +"lp.pool.remove.warning.text" = "Removing pool tokens converts your position back into underlying tokens at the current rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive."; +"lp.pool.remove.warning.title" = "NOTE"; +"lp.remove.button.title" = "Remove Liquidity"; +"lp.remove.liquidity.screen.title" = "Remove Liquidity"; +"lp.reward.token.text" = "Earn %@"; +"lp.reward.token.title" = "Rewards Payout In"; +"lp.slippage.title" = "Slippage"; +"lp.supply.button.title" = "Supply Liquidity"; +"lp.supply.liquidity.screen.title" = "Supply Liquidity"; +"lp.token.pooled.text" = "Your %@ Pooled"; +"lp.user.pools.title" = "User pools"; "manage.assets.account.missing.text" = "Bir hesap ekle"; "manage.assets.search.hint" = "Tokene veya ağa göre ara"; "members.common" = "Üyeler"; @@ -484,6 +512,7 @@ Kaynak: %@"; "network.info.address" = "Node adresi"; "network.info.name" = "Node adı"; "network.info.title" = "Node Bilgisi"; +"network.issue.main" = "Connection Error: Unable to connect to the network. Please try again."; "network.issue.network.unavailible" = "Ağ kullanılamıyor"; "network.issue.node.unavailable" = "Düğüm kullanılamıyor"; "network.issue.notofication" = "Bildiri"; @@ -506,6 +535,7 @@ Kaynak: %@"; burada görmek için NFT'leri satın alın veya bastırın"; "nft.owner.title" = "Sahip olunan."; "nft.share.address" = "Alacağım genel adresim:%s"; +"nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; "nft.stub.text" = "NFT'ler yakında geliyor"; "nft.stub.title" = "Sumimasen!"; "nft.tokenid.title" = "Jeton kimliği"; @@ -589,17 +619,20 @@ burada görmek için NFT'leri satın alın veya bastırın"; "polkaswap.disclaimer.title" = "Sorumluluk reddi beyanı"; "polkaswap.liqudity.fee.info" = "Her işlemin bir kısmı (%0,3) protokol teşviki olarak likidite sağlayıcılarına gidiyor."; "polkaswap.liquidity.provider.fee" = "Likidite Sağlayıcı Ücreti"; +"polkaswap.liquidity.provider.fee" = "Likidite Sağlayıcı Ücreti"; "polkaswap.market.alert.choose.action" = "Varlık seçin"; "polkaswap.market.alert.message" = "Takas ayarlamalarını tamamlamadınız. Varlıklardan biri veya her ikisi de seçilmediğinden pazar seçimi kullanılamıyor."; "polkaswap.market.alert.title" = "Pazar seçimi kullanılamıyor"; "polkaswap.market.algorithm.title" = "Piyasa algoritması"; "polkaswap.market.smart.description" = "SMART likidite yönlendirmesi, mevcut tüm piyasalardan yalnızca en iyi fiyat seçeneklerini birleştirerek herhangi bir işlem için en iyi fiyatı sağlar. Mevcut olduğunda, varlık fiyatı XYK havuzunun kullanıldığı diğer kaynaklardan daha uygun olduğu sürece likidite için Token Bağlama Eğrisi (TBC) kullanılacaktır."; "polkaswap.market.stub" = "Pazar:"; +"polkaswap.market.stub" = "Pazar"; "polkaswap.market.tbc.description" = "TBC - yalnızca Token Bağlama Eğrisinden (Birincil Piyasa) satın alma. Fiyatın XYK havuzuna (İkincil Piyasa) göre olumsuz hale gelme olasılığı vardır, ancak kazanılmış ödüllerden elde edilen değer zamanla çok daha avantajlı hale gelebilir."; "polkaswap.market.xyk.description" = "XYK — yalnızca XYK havuzundan (İkincil Piyasa) satın alma. Piyasa yapıcının x*y=k eğrisindeki konumunu değiştirerek herkesin varlık alıp satabildiği geleneksel XYK havuz takası."; "polkaswap.max.received" = "Maksimum satılanlar"; "polkaswap.maximum.sold.info" = "Onaylanmadan önce büyük, olumsuz bir fiyat hareketi olması durumunda işleminiz geri alınacaktır."; "polkaswap.min.received" = "Minimum alınan miktar"; +"polkaswap.min.received" = "Minimum alınan miktar"; "polkaswap.minimum.received.info" = "Onaylanmadan önce büyük, olumsuz bir fiyat hareketi olması durumunda işleminiz geri alınacaktır."; "polkaswap.network.fee" = "Ağ ücreti"; "polkaswap.network.fee.info" = "Ağ ücreti, SORA sisteminin büyümesini ve istikrarlı performansını sağlamak için kullanılır."; @@ -832,9 +865,12 @@ hiç ödül alınmadı"; "staking.pool.create.root" = "Kök"; "staking.pool.create.stateToggler" = "Durum değiştirici"; "staking.pool.create.title" = "Havuz oluştur"; +"staking.pool.create.title" = "Havuz oluştur"; "staking.pool.info.title" = "Havuz Bilgisi"; "staking.pool.management.select.validators.subtitle" = "Devam etmek için \n doğrulayıcıları seçmelisiniz"; +"staking.pool.management.select.validators.subtitle" = "Devam etmek için \n doğrulayıcıları seçmelisiniz"; "staking.pool.management.select.validators.title" = "Select pool validators"; +"staking.pool.management.select.validators.title" = "Havuz doğrulayıcılarını seçin"; "staking.pool.rewards.delay.text" = "İstediğiniz zaman bahis yapın. Sonrasında faiz kazanmaya başlayacaksınız%s"; "staking.pool.select.validators.manual" = "Manuel olarak seç"; "staking.pool.select.validators.suggested" = "Önerileni seç"; @@ -893,6 +929,7 @@ hiç ödül alınmadı"; "staking.round.title" = "yuvarlak #%@"; "staking.select.suggested" = "Önerileni seçin"; "staking.select.validators.confirm.title" = "Doğrulayıcıları seçme"; +"staking.select.validators.confirm.title" = "Doğrulayıcıları seçme"; "staking.select.validators.custom.button.title" = "Kendiniz seçin"; "staking.select.validators.custom.desc" = "Adaylarınızın yetkin ve dürüst hareket edeceğine güvenmelisiniz, kararınızı tamamen mevcut karlılıklarına dayandırmak kârın azalmasına ve hatta fon kaybına yol açabilir"; "staking.select.validators.custom.title" = "Doğrulayıcılarınız ile stake yapın."; @@ -1023,9 +1060,9 @@ Anahtarınızın yedeğini almayı ve onu güvenli ve gizli bir yerde (ö.o., bi "transaction.details.from" = "Gönderen"; "transaction.details.hash.title" = "Harici Hash"; "transaction.details.view.etherscan" = "Etherscan'da görüntüle"; +"transaction.details.view.oklink" = "View in OKX explorer"; "transaction.details.view.polkascan" = "Polkascan'de görüntüle"; "transaction.details.view.reefscan" = "Reefscan'da görüntüle"; -"transaction.details.view.oklink" = "View in OKX explorer"; "transaction.details.view.subscan" = "Subscan'da Görüntüle"; "transaction.list.header" = "Tüm işlemler"; "transaction.status.completed" = "Tamamlandı"; @@ -1041,6 +1078,8 @@ Anahtarınızın yedeğini almayı ve onu güvenli ve gizli bir yerde (ö.o., bi "username.setup.title" = "Hesap oluştur"; "username.setup.title.2.0" = "Yeni bir cüzdan oluştur"; "validator.info.comission.title" = "Komisyon"; +"validator.info.min.stake.alert.text" = "Minimum stake among active nominators is %s. To get rewards you have to stake more."; +"validator.info.min.stake.among.active.nominators.text" = "Minimum stake among active nominators"; "validators.list.empty.message" = "Doğrulayıcı bulunamadı"; "verify.phone.number.title" = "Verify your phone number"; "vesting.claim.disclaimer.text" = "Her parachain'in benzersiz hak kazanma programları nedeniyle, uygulamamız talep edilmeye uygun kilitli tokenlerin miktarını görüntüleyemiyor. Olası tutarın marjinal olması ve söz konusu işlem ücretiyle karşılaştırılabilir olması durumunda, hak talebinde bulunmanın pratik olmayabileceğini lütfen unutmayın. Kilitli bakiyelerinize ilişkin kapsamlı bilgiler için Subscan blok araştırmacısına başvurun. Bilgileri dikkatlice değerlendirmenizi ve kendi takdirinize göre ilerlemenizi öneririz."; @@ -1050,6 +1089,7 @@ Anahtarınızın yedeğini almayı ve onu güvenli ve gizli bir yerde (ö.o., bi "view.wallet" = "Cüzdanı görün"; "wallet.account.locks.democracy" = "Demokrasi"; "wallet.account.locks.vesting" = "Vesting"; +"wallet.all.assets.hidden" = "Değiştirilebilir jetonlar"; "wallet.asset.buy" = "Satın Alın"; "wallet.asset.buy.with" = "%s ile al"; "wallet.asset.receive" = "Al"; @@ -1109,6 +1149,7 @@ ait olduğundan emin olun."; "wallet.send.balance.total.after.transfer" = "Transfer sonrası toplamı"; "wallet.send.confirm.title" = "Transferi onaylayın"; "wallet.send.dead.recipient.message" = "Hedef hesaptaki son tutar, yaratılış bakiyesinden daha az olacağından dolayı transferiniz başarısız olacak. Lütfen miktarı arttırın."; +"wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%1$s) on the destination account will be less than the minimal balance (%2$s). Please increase the amount by %3$s"; "wallet.send.dead.recipient.title" = "Miktar çok düşük"; "wallet.send.eth.dead.recipient.message" = "Alıcının hesabındaki yetersiz Ethereum bakiyesi, ERC20 token transferinin tamamlanmasını engeller. Lütfen alıcının aktarıma devam etmek için yeterli Ethereum'a sahip olduğundan emin olun."; "wallet.send.existential.warning" = "Transferiniz, toplam bakiyeyi yaratılış bakiyesinden daha düşük yapacağı için hesabı blockstore'dan kaldıracaktır"; @@ -1129,9 +1170,13 @@ ait olduğundan emin olun."; "what.accounts.for.export" = "Dışa aktarmak istediğin hesaplar hangileri?"; "xcm.cross.chain.button.title" = "Çapraz Zincir"; "xcm.cross.chain.invalid.address.message" = "Verilen adrese göre yanlış ağ üzerinden transfer yapmaya çalışıyorsunuz."; +"xcm.cross.chain.invalid.address.message" = "Verilen adrese göre yanlış ağ \ +Üzerinden transfer yapmaya \ +Çalışıyorsunuz."; "xcm.cross.chain.invalid.address.title" = "Ağ adresi değil"; "xcm.destination.network.fee.title" = "Hedef Ağ ücreti"; "xcm.destination.network.title" = "Hedef ağ"; +"xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; "xcm.mywallets.button.title" = "Cüzdanlarım"; "xcm.origin.network.fee.title" = "Menşe ağ ücreti"; "xcm.origin.network.title" = "Köken ağı"; @@ -1140,9 +1185,4 @@ ait olduğundan emin olun."; "your.validators.stop.nominating.title" = "Aday göstermeyi bırak"; "your.validators.validator.total.stake" = "Toplam yatırılan miktar:%@"; "сurrencies.stub.text" = "Para birimleri"; -"balance.locks.blocked.row.title" = "Blocked"; -"wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%s) on the destination account will be less than the minimal balance (%s). Please increase the amount by %s"; -"common.more" = "more"; -"nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; -"wallet.all.assets.hidden" = "You have hidden all assets"; -"xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; +"common.more" = "More"; diff --git a/fearless/vi.lproj/Localizable.strings b/fearless/vi.lproj/Localizable.strings index 29030b7d81..cb07373e2b 100644 --- a/fearless/vi.lproj/Localizable.strings +++ b/fearless/vi.lproj/Localizable.strings @@ -91,10 +91,14 @@ "application.status.view.copied.description" = "Đã sao chép địa chỉ ví thành công"; "application.status.view.copied.title" = "Địa chỉ được sao chép"; "application.status.view.hash.copied.description" = "Mã hash được sao chép thành công"; +"application.status.view.hash.copied.description" = "Hash được sao chép thành công"; "application.status.view.hash.copied.title" = "Mã hash được sao chép"; +"application.status.view.hash.copied.title" = "Đã sao chép mã hash"; "application.status.view.offline.description" = "Vui lòng kiểm tra kết nối của bạn và thử lại"; "application.status.view.offline.title" = "Có vẻ như bạn đang ngoại tuyến"; "application.status.view.reconnected.description" = "Kết nối đã được khôi phục thành công"; +"application.status.view.reconnected.description" = "Đã khôi phục kết nối thành công"; +"application.status.view.reconnected.title" = "Đã kết nối lại"; "application.status.view.reconnected.title" = "Đã kết nối lại"; "applied.fearless.wallet.bonus" = "Tiền thưởng ví Fearless được áp dụng"; "apply.fearless.wallet.bonus" = "Nhấn để áp dụng phần thưởng ví Fearless"; @@ -152,6 +156,7 @@ "backup.wallet.replace.several.alert" = "Bạn hiện có một số tài khoản chuỗi đã được thêm bằng cách thay thế cặp khóa chính và tài khoản này sở hữu một tài khoản cụ thể. Tuy nhiên, xin lưu ý rằng luồng sao lưu (xuất) ví hiện tại của chúng tôi không hỗ trợ lưu trữ nhiều cặp khóa. Do đó, bạn chỉ có thể lưu cặp khóa chính của mình. \n \n Để đảm bảo an toàn cho tài khoản chuỗi đã thay thế của bạn, trước tiên chúng tôi khuyên bạn nên sao lưu tài khoản đó một cách riêng biệt trước khi tiếp tục với quy trình hiện tại. Khi bạn đã sao lưu thành công tài khoản chuỗi đã thay thế của mình, bạn có thể tiếp tục quy trình hiện tại mà không có bất kỳ lo ngại nào."; "backup.wallet.seed" = "Hiển thị Raw Seed"; "backup.wallet.title" = "Sao lưu ví"; +"balance.locks.blocked.row.title" = "Bị chặn"; "balance.locks.governance.row.title" = "Quản trị"; "balance.locks.liquidity.pools.row.title" = "Pool thanh khoản"; "balance.locks.nomination.pools.row.title" = "Nhóm Nomination"; @@ -177,6 +182,7 @@ tiền Euro"; "common.action.receive" = "Nhận"; "common.action.send" = "Gửi"; "common.action.teleport" = "Teleport"; +"common.activation.required" = "Activation Required"; "common.add" = "Thêm"; "common.address" = "Địa chỉ"; "common.advanced" = "Nâng cao"; @@ -281,6 +287,7 @@ tiền Euro"; "common.search.start.title" = "Kết quả tìm kiếm sẽ xuất hiện tại đây"; "common.secret.derivation.path" = "Đường dẫn khôi phục ví bí mật (derivation path)"; "common.select" = "Chọn"; +"common.select" = "Chọn"; "common.select.all" = "Chọn tất cả"; "common.select.asset" = "Chọn tài sản"; "common.select.network" = "Chọn mạng"; @@ -396,6 +403,7 @@ tiền Euro"; "error.invalid.address" = "Địa chỉ không hợp lệ cho chuỗi đã chọn"; "error.message.enter.the.name" = "Nhập tên..."; "error.message.enter.the.url.address" = "Nhập địa chỉ URL…"; +"error.scan.qr.disabled.asset" = "The asset you want to transfer is either unsupported or turned off. Go to Asset Management, activate the asset, and then scan the QR code again."; "error.unsupported.asset" = "Bạn đang cố gắng thực hiện chuyển nhượng nội dung hiện không được hỗ trợ trên Ứng dụng. Vui lòng chọn nội dung khác hoặc yêu cầu mã QR khác."; "ethereum.crypto.type" = "Ethereum keypair crypto type"; "ethereum.secret.derivation.path" = "Ethereum secret derivation path"; @@ -461,6 +469,26 @@ Seed: %@"; "label.testnet" = "Testnet"; "language.title" = "Ngôn ngữ"; "learn.more.about.crowdloans" = "Tìm hiểu thêm về Crowdloans"; +"lp.apy.alert.text" = "Farming reward for liquidity provision"; +"lp.apy.alert.title" = "Strategic Bonus APY"; +"lp.apy.title" = "Strategic Bonus APY"; +"lp.available.pools.title" = "Available pools"; +"lp.confirm.liquidity.screen.title" = "Confirm Liquidity"; +"lp.confirm.liquidity.warning.text" = "Output is estimated. If the price changes more than 0.5% your transaction will revert."; +"lp.network.fee.alert.text" = "Network fee is used to ensure SORA system’s growth and stable performance."; +"lp.network.fee.alert.title" = "Network fee"; +"lp.pool.details.title" = "Pool details"; +"lp.pool.remove.warning.text" = "Removing pool tokens converts your position back into underlying tokens at the current rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive."; +"lp.pool.remove.warning.title" = "NOTE"; +"lp.remove.button.title" = "Remove Liquidity"; +"lp.remove.liquidity.screen.title" = "Remove Liquidity"; +"lp.reward.token.text" = "Earn %@"; +"lp.reward.token.title" = "Rewards Payout In"; +"lp.slippage.title" = "Slippage"; +"lp.supply.button.title" = "Supply Liquidity"; +"lp.supply.liquidity.screen.title" = "Supply Liquidity"; +"lp.token.pooled.text" = "Your %@ Pooled"; +"lp.user.pools.title" = "User pools"; "manage.assets.account.missing.text" = "Thêm một tài khoản..."; "manage.assets.search.hint" = "Tìm kiếm theo tài sản"; "members.common" = "Các thành viên"; @@ -486,6 +514,7 @@ Xin lưu ý rằng việc ký giao dịch này sẽ phải chịu phí giao dị "network.info.address" = "Địa chỉ node"; "network.info.name" = "Tên node"; "network.info.title" = "Thông tin node"; +"network.issue.main" = "Lỗi kết nối: Không thể kết nối mạng. Vui lòng thử lại."; "network.issue.network.unavailible" = "Mạng không khả dụng"; "network.issue.node.unavailable" = "Node không khả dụng"; "network.issue.notofication" = "Thông báo"; @@ -507,6 +536,7 @@ Xin lưu ý rằng việc ký giao dịch này sẽ phải chịu phí giao dị "nft.list.empty.message" = "Chưa có bất kỳ NFT nào. Mua hoặc đúc NFT để xem chúng tại đây."; "nft.owner.title" = "Sở hữu"; "nft.share.address" = "Địa chỉ công khai của tôi để nhận: %s"; +"nft.spam.warning" = "Cảnh giác với hoạt động thu thập NFT lừa đảo/spam – hãy xác minh tính xác thực trước khi tham gia. Giữ an toàn!"; "nft.stub.text" = "NFTs sẽ sớm ra mắt"; "nft.stub.title" = "Sumimasen!"; "nft.tokenid.title" = "Token ID"; @@ -591,17 +621,20 @@ Xin lưu ý rằng việc ký giao dịch này sẽ phải chịu phí giao dị "polkaswap.disclaimer.title" = "Từ chối trách nhiệm"; "polkaswap.liqudity.fee.info" = "Một phần của mỗi giao dịch (0,3%) được chuyển đến các nhà cung cấp thanh khoản như một biện pháp khuyến khích."; "polkaswap.liquidity.provider.fee" = "Phí cung cấp thanh khoản"; +"polkaswap.liquidity.provider.fee" = "Phí nhà cung cấp thanh khoản "; "polkaswap.market.alert.choose.action" = "Chọn tài sản"; "polkaswap.market.alert.message" = "Bạn chưa hoàn thành việc điều chỉnh hoán đổi. Một hoặc cả hai nội dung chưa được chọn, vì vậy lựa chọn thị trường không khả dụng."; "polkaswap.market.alert.title" = "Lựa chọn thị trường không có sẵn"; "polkaswap.market.algorithm.title" = "Thuật toán thị trường"; "polkaswap.market.smart.description" = "SMART liquidity routing đảm bảo giá tốt nhất cho giao dịch bất kỳ bằng cách kết hợp mức giá tốt nhất hiện có trên thị trường. Khi thuận lợi, Token Bonding Curve (TBC) sẽ được dùng để tạo thanh khoản miễn là giá của tài sản hợp lý hơn các nguồn khác như pool (XYK)."; "polkaswap.market.stub" = "Thị trường:"; +"polkaswap.market.stub" = "Thị trường"; "polkaswap.market.tbc.description" = "TBC - chỉ mua từ Token Bonding Curve (Primary Market). Giá có thể trở nên bất lợi so với XYK pool (Secondary Market), nhưng về lâu dài giá trị nhận được từ phần thưởng có thể lớn hơn nhiều."; "polkaswap.market.xyk.description" = "XYK — chỉ mua từ nhóm XYK (Thị trường thứ cấp). Hoán đổi nhóm XYK truyền thống nơi mọi người có thể mua hoặc bán tài sản bằng cách thay đổi vị trí của nhà tạo lập thị trường trên đường cong x*y=k."; "polkaswap.max.received" = "Số lượng tối đa đã bán"; "polkaswap.maximum.sold.info" = "Giao dịch của bạn sẽ hoàn nguyên nếu có biến động giá lớn, không thuận lợi trước khi được xác nhận."; "polkaswap.min.received" = "Tối thiểu nhận được"; +"polkaswap.min.received" = "Tối thiểu nhận được "; "polkaswap.minimum.received.info" = "Giao dịch của bạn sẽ được hoàn tác nếu có một biến động giá lớn hay một biến động không thuận lợi trước khi nó được xác nhận."; "polkaswap.network.fee" = "Phí mạng"; "polkaswap.network.fee.info" = "Phí mạng được sử dụng để đảm bảo sự tăng trưởng và hiệu suất ổn định của hệ thống SORA."; @@ -684,8 +717,8 @@ Xin lưu ý rằng việc ký giao dịch này sẽ phải chịu phí giao dị "scam.additional.stub" = "Bổ sung:"; "scam.description.donation.stub" = "Địa chỉ này đã được gắn cờ là đáng ngờ. Chúng tôi thực sự khuyên bạn không nên gửi %s tới tài khoản này."; "scam.description.exchange.stub" = "Địa chỉ này được đánh dấu là một sàn giao dịch, hãy cẩn thận, địa chỉ gửi tiền và địa chỉ rút tiền có thể khác nhau."; -"scam.description.sanctions.stub" = "Địa chỉ này đã bị gắn cờ do một thực thể có liên quan đến một quốc gia đang bị trừng phạt. Chúng tôi thực sự khuyên bạn không nên gửi %s tới tài khoản này."; -"scam.description.scam.stub" = "Địa chỉ này đã bị gắn cờ do có bằng chứng lừa đảo. Chúng tôi thực sự khuyên bạn không nên gửi %s tới tài khoản này."; +"scam.description.sanctions.stub" = "Địa chỉ này đã bị gắn cờ do có liên quan đến một quốc gia đang bị trừng phạt. Chúng tôi thực sự khuyên bạn không nên gửi %s vào tài khoản này."; +"scam.description.scam.stub" = "Địa chỉ này đã bị gắn cờ do có bằng chứng lừa đảo. Chúng tôi thực sự khuyên bạn không nên gửi {asset} tới tài khoản này."; "scam.name.stub" = "Tên:"; "scam.reason.stub" = "Lý do:"; "scam.warning.alert.subtitle" = "Chúng tôi thực sự khuyên bạn không nên gửi %s vào tài khoản này."; @@ -836,8 +869,11 @@ vào tài khoản của bạn để phân biệt với lượng stake còn lại "staking.pool.create.root" = "Nguồn gốc"; "staking.pool.create.stateToggler" = "Trình chuyển đổi trạng thái"; "staking.pool.create.title" = "Tạo pool"; +"staking.pool.create.title" = "Tạo pool"; "staking.pool.info.title" = "Thông tin pool"; "staking.pool.management.select.validators.subtitle" = "Để tiếp tục, bạn nên \n chọn các validator"; +"staking.pool.management.select.validators.subtitle" = "Để tiếp tục, bạn nên\nchọn validator"; +"staking.pool.management.select.validators.title" = "Chọn pool validator"; "staking.pool.management.select.validators.title" = "Chọn pool validator"; "staking.pool.rewards.delay.text" = "Stake bất cứ lúc nào. Bạn sẽ bắt đầu kiếm được tiền lãi sau %s"; "staking.pool.select.validators.manual" = "Chọn thủ công"; @@ -896,6 +932,7 @@ vào tài khoản của bạn để phân biệt với lượng stake còn lại "staking.round.title" = "vòng #%@"; "staking.select.suggested" = "Chọn đề xuất"; "staking.select.validators.confirm.title" = "Chọn validator"; +"staking.select.validators.confirm.title" = "Chọn validator"; "staking.select.validators.custom.button.title" = "Tự chọn"; "staking.select.validators.custom.desc" = "Bạn nên tin tưởng các nominator của mình có hiểu hiểu biết và trung thực, vì quyết định của bạn hầu như chỉ dựa trên lợi nhuận hiện tại của họ. Quyết định này có thể dẫn đến giảm lợi nhuận hoặc thậm chí mất vốn."; "staking.select.validators.custom.title" = "Stake với validator của bạn"; @@ -1027,9 +1064,9 @@ vào tài khoản của bạn để phân biệt với lượng stake còn lại "transaction.details.from" = "Từ"; "transaction.details.hash.title" = "Hash bên ngoài"; "transaction.details.view.etherscan" = "Xem trong Etherscan"; +"transaction.details.view.oklink" = "Xem trong OKX explorer"; "transaction.details.view.polkascan" = "Xem trên Polkascan"; "transaction.details.view.reefscan" = "Xem trong Reefscan"; -"transaction.details.view.oklink" = "View in OKX explorer"; "transaction.details.view.subscan" = "Xem trong Subscan"; "transaction.list.header" = "Tất cả giao dịch"; "transaction.status.completed" = "Đã hoàn thành"; @@ -1045,6 +1082,8 @@ vào tài khoản của bạn để phân biệt với lượng stake còn lại "username.setup.title" = "Tạo tài khoản"; "username.setup.title.2.0" = "Tạo một ví mới"; "validator.info.comission.title" = "Sứ mệnh"; +"validator.info.min.stake.alert.text" = "Lượng token tối thiểu để stake ở nominator này là %s . Để nhận được phần thưởng, bạn phải stake nhiều hơn."; +"validator.info.min.stake.among.active.nominators.text" = "Lượng token tối thiểu để stake với những nominator đang hoạt động"; "validators.list.empty.message" = "Không tìm thấy validator nào"; "verify.phone.number.title" = "Xác minh số điện thoại của bạn"; "vesting.claim.disclaimer.text" = "Do lịch trình trao quyền duy nhất của mỗi parachain, ứng dụng của chúng tôi không thể hiển thị số lượng mã thông báo bị khóa đủ điều kiện để yêu cầu. Xin lưu ý rằng việc bắt đầu yêu cầu bồi thường có thể không thực tế nếu số tiền tiềm năng là rất nhỏ và có thể so sánh được với phí giao dịch liên quan. Để có thông tin chi tiết toàn diện về số dư bị khóa của bạn, hãy tham khảo Subscan blockexplorer. Chúng tôi khuyên bạn nên đánh giá thông tin một cách cẩn thận và tiến hành theo ý riêng của bạn."; @@ -1054,6 +1093,7 @@ vào tài khoản của bạn để phân biệt với lượng stake còn lại "view.wallet" = "Xem ví"; "wallet.account.locks.democracy" = "Dân chủ"; "wallet.account.locks.vesting" = "Vesting"; +"wallet.all.assets.hidden" = "Bạn đã ẩn tất cả nội dung"; "wallet.asset.buy" = "Mua"; "wallet.asset.buy.with" = "Mua %s với"; "wallet.asset.receive" = "Nhận"; @@ -1113,6 +1153,7 @@ thuộc đúng mạng"; "wallet.send.balance.total.after.transfer" = "Tổng số sau khi chuyển"; "wallet.send.confirm.title" = "Xác nhận chuyển khoản"; "wallet.send.dead.recipient.message" = "Giao dịch chuyển tiền của bạn sẽ không thành công vì số tiền cuối cùng trên tài khoản đích sẽ ít hơn số dư tối thiểu. Hãy cố gắng tăng số lượng."; +"wallet.send.dead.recipient.message.v2" = "Chuyển khoản của bạn sẽ không thành công vì số tiền cuối cùng (%1$s) trên tài khoản đích sẽ nhỏ hơn số dư tối thiểu (%2$s). Vui lòng tăng thêm %3$s"; "wallet.send.dead.recipient.title" = "Số tiền quá ít"; "wallet.send.eth.dead.recipient.message" = "Số dư Ethereum không đủ trong tài khoản của người nhận sẽ ngăn cản việc hoàn tất chuyển mã thông báo ERC20. Vui lòng đảm bảo người nhận có đủ Ethereum để tiến hành chuyển khoản."; "wallet.send.existential.warning" = "Chuyển khoản của bạn sẽ xóa tài khoản khỏi blockstore vì nó sẽ làm cho tổng số dư còn lại thấp hơn số dư tối thiểu."; @@ -1133,9 +1174,11 @@ thuộc đúng mạng"; "what.accounts.for.export" = "Bạn muốn xuất tài khoản nào trong ví?"; "xcm.cross.chain.button.title" = "Chuỗi chéo"; "xcm.cross.chain.invalid.address.message" = "Theo địa chỉ được cung cấp, bạn đang cố gắng thực hiện chuyển khoản trên mạng sai."; +"xcm.cross.chain.invalid.address.message" = "Theo địa chỉ được cung cấp, bạn đang cố thực hiện chuyển khoản trên mạng sai."; "xcm.cross.chain.invalid.address.title" = "Không phải là địa chỉ mạng"; "xcm.destination.network.fee.title" = "Phí mạng đích"; "xcm.destination.network.title" = "Mạng đích"; +"xcm.low.amaunt.assetSymbol.alert" = "Hiện tại, có một phút. số tiền %@ để bridge (bắc cầu) nhằm đảm bảo tính ổn định và bảo mật. Cảm ơn vì sự thấu hiểu của bạn."; "xcm.mywallets.button.title" = "Ví của tôi"; "xcm.origin.network.fee.title" = "Phí mạng ban đầu"; "xcm.origin.network.title" = "Mạng ban đầu"; @@ -1144,130 +1187,4 @@ thuộc đúng mạng"; "your.validators.stop.nominating.title" = "Dừng đề cử"; "your.validators.validator.total.stake" = "Tổng đã stake: %@"; "сurrencies.stub.text" = "Tiền tệ"; -"cant.fetch.refresh" = "Không thể tìm nạp dữ liệu, nhấn để làm mới."; -"card.attention.text" = "Chú ý"; -"card.hub.manage.card" = "Quản lý thẻ SORA"; -"card.hub.manage.card.alert.message" = "Để quản lý Thẻ SORA của bạn, vui lòng cài đặt Ứng dụng chính thức Thẻ SORA. Nhấn OK để được chuyển đến App Store."; -"card.hub.manage.card.alert.title" = "Tải ứng dụng thẻ SORA"; -"card.hub.settings.logout.button" = "Đăng xuất"; -"card.hub.settings.logout.description" = "Bạn sắp đăng xuất khỏi Thẻ SORA. Bạn sẽ vẫn có quyền truy cập vào ứng dụng độc lập Thẻ SORA, nhưng số dư sẽ không còn khả dụng cho bạn trong Ví SORA."; -"card.hub.settings.logout.title" = "Đăng xuất khỏi SORA Card"; -"card.hub.settings.title" = "Cài đặt thẻ"; -"card.hub.title" = "Chi tiết thẻ"; -"card.hub.update.button" = "Cập nhật ngay"; -"card.hub.update.description" = "Bạn đang sử dụng phiên bản hoặc ứng dụng lỗi thời, một số phần có thể không hoạt động như dự kiến."; -"card.hub.update.title" = "Cập nhật ứng dụng lên phiên bản mới nhất"; -"card.issuance.screen.free.card.description" = "Để phát hành thẻ MIỄN PHÍ: \ -Giữ, stake hoặc cung cấp thanh khoản cho XOR trị giá ít nhất €%@ trong tài khoản SORA của bạn"; -"card.issuance.screen.free.card.get.xor" = "Nhận %@ XOR"; -"card.issuance.screen.paid.card.description" = "Phí cấp thẻ một lần"; -"card.issuance.screen.paid.card.note" = "Lưu ý: việc phát hành thẻ trả phí sẽ được thực hiện ở giai đoạn sau."; -"card.issuance.screen.paid.card.pay.euro" = "Trả phí phát hành €%@"; -"card.issuance.screen.paid.card.title" = "€%@ phí phát hành"; -"card.issuance.screen.title" = "Phát hành thẻ"; -"card.or" = "hoặc"; -"card.update.button" = "Cập nhật ứng dụng"; -"card.update.title" = "Cập nhật ứng dụng lên phiên bản mới nhất để truy cập Thẻ SORA"; -"cardhub.coming.soon" = "Quản lý thẻ sắp ra mắt"; -"cardhub.exchange" = "Giao dịch"; -"cardhub.freeze" = "Đông lạnh"; -"cardhub.iban.title" = "Chi tiết tài khoản IBAN"; -"cardhub.top.up" = "Nạp tiền"; -"cardhub.transfer" = "Chuyển"; -"change.email.title" = "Thay đổi email của bạn"; -"common.back" = "Trở lại"; -"common.change.email" = "Sử dụng email khác"; -"common.no.spam" = "Không có thư rác! Chỉ để bảo mật tài khoản của bạn"; -"common.resend.code" = "Gửi lại mã"; -"common.resend.link" = "Gửi lại liên kết"; -"common.resend.timer" = "Gửi lại sau %02d:%02d"; -"common.send.code" = "Gửi mã SMS"; -"common.send.link" = "Gửi link"; -"common.support" = "Hỗ trợ"; -"common.try.again" = "Thử lại"; -"common.user.not.found" = "Không tìm thấy số điện thoại di động nào, vui lòng kiểm tra lại và thử lại."; -"details.already.have.card" = "Tôi đã có thẻ"; -"details.already.used.free.try" = "Bạn đã sử dụng bản dùng thử miễn phí của mình"; -"details.annual.service.fee" = "€0 phí dịch vụ hàng năm"; -"details.description" = "Nhận tài khoản Euro IBAN và thẻ ghi nợ có thể truy cập trong Ví SORA của bạn"; -"details.enough.xor.desription" = "Bạn có đủ XOR"; -"details.free.card.issuance" = "Miễn phí phát hành thẻ"; -"details.free.card.issuance.conditions.euro" = "hoặc đăng ký với %@ €"; -"details.free.card.issuance.conditions.xor" = "Nếu bạn nắm giữ, stake hoặc cung cấp thanh khoản cho XOR trị giá ít nhất là €%@ trong tài khoản SORA của mình"; -"details.get.more.xor" = "Nhận thêm XOR"; -"details.issue.card" = "Phát hành thẻ miễn phí"; -"details.need.xor.desription" = "Bạn cần thêm %@ XOR (€%@)"; -"details.title" = "Nạp vào SORA Card của bạn bằng tiền pháp định hoặc tiền điện tử và thanh toán trực tuyến, tại cửa hàng hoặc rút tiền từ máy ATM."; -"enter.email.description" = "Liên kết ma thuật sẽ được gửi\nđến email của bạn"; -"enter.email.input.field.label" = "Email"; -"enter.email.title" = "Nhập email của bạn"; -"enter.phone.number.description" = "Bạn sẽ nhận được\nmã xác minh qua SMS"; -"enter.phone.number.phone.input.field.label" = "Số điện thoại"; -"entry.card.info" = "Thông tin thẻ"; -"get.more.xor.dialog.buy.option" = "Mua XOR bằng EUR"; -"get.more.xor.dialog.deposit.option" = "Nạp XOR"; -"get.more.xor.dialog.description" = "Bạn có thể đổi lấy XOR hoặc mua bằng Euro"; -"get.more.xor.dialog.swap.option" = "Hoán đổi tiền điện tử lấy XOR"; -"get.prepared.alert" = "Bạn chỉ có %@ lần thử miễn phí để vượt qua quy trình KYC. Mọi lần thử khác sau đó sẽ có giá € %@ . Các nỗ lực trả phí sẽ khả dụng sau lần cập nhật tiếp theo của ứng dụng."; -"get.prepared.need" = "Để hoàn tất KYC, bạn sẽ cần:"; -"get.prepared.ok.title" = "OK, tôi đã sẵn sàng"; -"get.prepared.personal.info.description" = "Điền vào biểu mẫu với tên và địa chỉ của bạn"; -"get.prepared.personal.info.title" = "Gửi thông tin cá nhân của bạn"; -"get.prepared.proof.address.description" = "Một tài liệu chính thức (hóa đơn tiện ích, bảng sao kê ngân hàng, bảng sao kê của chính phủ hoặc thư từ) có chứa tên đầy đủ và địa chỉ của bạn, và không cũ hơn 3 tháng."; -"get.prepared.proof.address.note" = "Lưu ý: Các báo cáo từ các neobank như Revolut và N26 hiện không được coi là bằng chứng địa chỉ hợp lệ."; -"get.prepared.proof.address.title" = "Gửi ảnh giấy tờ chứng minh địa chỉ"; -"get.prepared.submit.id.photo.description" = "Hộ chiếu, Giấy phép lái xe, Chứng minh nhân dân hoặc Giấy phép cư trú"; -"get.prepared.submit.id.photo.title" = "Gửi ảnh giấy tờ tùy thân của bạn"; -"get.prepared.take.selfie.description" = "Hướng dẫn sẽ được cung cấp trong quá trình này"; -"get.prepared.take.selfie.title" = "Chụp ảnh selfie"; -"get.prepared.title" = "Chuẩn bị sẵn sàng"; -"iban.frozen.description" = "IBAN của bạn đã bị đóng băng.\ - Để tìm hiểu thêm, hãy liên hệ %@"; -"iban.pending.description" = "Việc cấp IBAN của bạn đang chờ xử lý.\ - Trong trường hợp chờ đợi lâu hơn 72h, hãy liên hệ với chúng tôi qua %@"; -"iban.suspended.description" = "IBAN của bạn đã bị đình chỉ.\ - Để tìm hiểu thêm, hãy liên hệ %@"; -"kyc.result.verification.in.progress" = "Đang xác minh"; -"kyc.result.verification.in.progress.description" = "Bạn đã hoàn thành thành công đơn đăng ký KYC của mình. Việc xem xét đang chờ xử lý và bạn có thể mong đợi một quyết định trong thời gian ngắn.\n\nThông thường quyết định được đưa ra cùng ngày và trong một số trường hợp có thể mất đến 3 ngày."; -"log.out" = "Đăng xuất"; -"login.title" = "Đăng nhập hoặc đăng ký"; -"no.free.kyc.attempts.description" = "Bạn đã sử dụng nỗ lực miễn phí của mình để vượt qua quy trình KYC.\n\nChúng tôi đề nghị bạn đợi cho đến lần nâng cấp tiếp theo của ứng dụng để tiếp tục với các lần dùng thử trả phí."; -"no.free.kyc.attempts.title" = "Không còn nỗ lực miễn phí"; -"otp.error.message.wrong.code" = "Mã không chính xác. Vui lòng thử lại."; -"paid.attempts.available.later" = "Các nỗ lực trả phí sẽ có sẵn ở giai đoạn sau"; -"payment.widget.unavailable.confirm" = "Vâng, tôi hiểu"; -"payment.widget.unavailable.description" = "Xin lỗi vì sự bất tiện. \n Chúng tôi đang nỗ lực làm việc để giải quyết vấn đề này. \n Vui lòng thử lại sau."; -"payment.widget.unavailable.message" = "Tiện ích thanh toán hiện không khả dụng"; -"payment.widget.unavailable.title" = "Tiện ích không khả dụng"; -"select.country.title" = "Chọn đất nước của bạn"; -"status.not.started" = "Nhận thẻ SORA"; -"terms.and.conditions.confirm.description" = "Bằng cách tiếp tục, bạn xác nhận rằng bạn đã đọc, hiểu và chấp nhận các chính sách này"; -"terms.and.conditions.sora.community.alert" = "Cộng đồng SORA không thu thập bất kỳ dữ liệu cá nhân nào, để có được Thẻ SORA và tài khoản IBAN, bạn sẽ cần hoàn thành quy trình KYC trực tiếp với tổ chức phát hành thẻ."; -"unsupported.countries.disclaimer" = "Cư dân từ một số quốc gia nhất định không thể đăng ký Thẻ SORA tại thời điểm này"; -"unsupported.countries.link" = "Xem danh sách"; -"user.registration.first.name.input.filed.label" = "Họ"; -"user.registration.last.name.input.filed.label" = "Tên"; -"user.registration.title" = "Giới thiệu bản thân"; -"verification.failed.description" = "KYC đã bị chấm dứt hoặc không thành công."; -"verification.failed.title" = "Xác minh không thành công"; -"verification.rejected.description" = "Đơn đăng ký của bạn đã bị từ chối."; -"verification.rejected.screen.attempts.price.disclaimer" = "Mọi nỗ lực khác sẽ khiến bạn mất €%@"; -"verification.rejected.screen.attempts.used" = "Bạn đã sử dụng các lần thử KYC miễn phí của mình."; -"verification.rejected.screen.try.again.for.euros" = "Hãy thử lại với giá €%@"; -"verification.rejected.screen.try.again.for.free" = "Thử lại miễn phí"; -"verification.rejected.support" = "Hỗ trợ"; -"verification.rejected.title" = "Xác minh bị từ chối"; -"verification.successful.description" = "Xác minh KYC của bạn đã thành công và chúng tôi đã chuẩn bị gửi cho bạn thẻ SORA!"; -"verification.successful.title" = "Đơn đăng ký được chấp thuận"; -"verify.email.description" = "Đi theo liên kết ma thuật đã được gửi \n tới %@ \n Có thể mất vài phút để email đến nơi. Vui lòng kiểm thư mục thư rác nếu bạn không nhận được email sau 5 phút."; -"verify.email.resend" = "GỬI LẠI VÀO %@"; -"verify.email.title" = "Xác minh email của bạn"; -"verify.phone.number.code.input.field.label" = "Mã SMS"; -"verify.phone.number.description" = "Nhập mã SMS đã được gửi đến\n%@"; -"verify.phone.number.send.code" = "GỬI MÃ"; -"balance.locks.blocked.row.title" = "Blocked"; -"wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%s) on the destination account will be less than the minimal balance (%s). Please increase the amount by %s"; -"common.more" = "more"; -"nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; -"wallet.all.assets.hidden" = "You have hidden all assets"; -"xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; +"common.more" = "More"; diff --git a/fearless/zh-Hans.lproj/Localizable.strings b/fearless/zh-Hans.lproj/Localizable.strings index b7a3715575..293328bf4c 100644 --- a/fearless/zh-Hans.lproj/Localizable.strings +++ b/fearless/zh-Hans.lproj/Localizable.strings @@ -91,10 +91,14 @@ "application.status.view.copied.description" = "钱包地址已成功复制"; "application.status.view.copied.title" = "地址已复制"; "application.status.view.hash.copied.description" = "哈希值已成功复制"; +"application.status.view.hash.copied.description" = "哈希成功复制"; +"application.status.view.hash.copied.title" = "哈希已复制"; "application.status.view.hash.copied.title" = "哈希已复制"; "application.status.view.offline.description" = "请检查您的连接并重试"; "application.status.view.offline.title" = "看起来你离线了"; "application.status.view.reconnected.description" = "连接成功恢复"; +"application.status.view.reconnected.description" = "连接成功恢复"; +"application.status.view.reconnected.title" = "重新连接"; "application.status.view.reconnected.title" = "重新连接"; "applied.fearless.wallet.bonus" = "Fearless钱包奖励已生效"; "apply.fearless.wallet.bonus" = "点按即可申请Fearless钱包奖金"; @@ -154,6 +158,7 @@ 为了确保替换后的链账户的安全性,在继续当前流程之前,我们建议您先单独备份它。一旦成功备份了替换后的链账户,就可以放心地进行当前流程。"; "backup.wallet.seed" = "显示原始种子"; "backup.wallet.title" = "备份钱包"; +"balance.locks.blocked.row.title" = "受阻"; "balance.locks.governance.row.title" = "治理"; "balance.locks.liquidity.pools.row.title" = "流动性池"; "balance.locks.nomination.pools.row.title" = "提名池"; @@ -177,6 +182,7 @@ "common.action.receive" = "接收"; "common.action.send" = "发送"; "common.action.teleport" = "传送"; +"common.activation.required" = "需要激活"; "common.add" = "添加"; "common.address" = "地址"; "common.advanced" = "高级"; @@ -281,6 +287,7 @@ "common.search.start.title" = "搜索结果将会在这里显示"; "common.secret.derivation.path" = "秘密派生路径"; "common.select" = "选择"; +"common.select" = "选择"; "common.select.all" = "全选"; "common.select.asset" = "选择资产"; "common.select.network" = "选择网络"; @@ -291,7 +298,7 @@ "common.sign" = "签署"; "common.skip" = "跳过"; "common.staking" = "质押"; -"common.start" = "Start"; +"common.start" = "开始"; "common.terms.and.conditions" = "条款和条件"; "common.till.date" = "截止到 %s"; "common.time.left" = "剩余时间"; @@ -396,6 +403,7 @@ "error.invalid.address" = "所选链的地址无效"; "error.message.enter.the.name" = "输入名称..."; "error.message.enter.the.url.address" = "输入URL地址..."; +"error.scan.qr.disabled.asset" = "您要转移的资产要么不受支持,要么已被关闭。请前往资产管理,激活该资产,然后再次扫描二维码。"; "error.unsupported.asset" = "您正在尝试转移一种目前该应用不支持的资产。请选择另一种资产或请求另一个二维码。"; "ethereum.crypto.type" = "以太坊密钥对加密类型"; "ethereum.secret.derivation.path" = "以太坊密钥派生路径"; @@ -461,6 +469,26 @@ "label.testnet" = "测试网"; "language.title" = "语言"; "learn.more.about.crowdloans" = "了解更多关于众贷的信息"; +"lp.apy.alert.text" = "Farming reward for liquidity provision"; +"lp.apy.alert.title" = "Strategic Bonus APY"; +"lp.apy.title" = "Strategic Bonus APY"; +"lp.available.pools.title" = "Available pools"; +"lp.confirm.liquidity.screen.title" = "Confirm Liquidity"; +"lp.confirm.liquidity.warning.text" = "Output is estimated. If the price changes more than 0.5% your transaction will revert."; +"lp.network.fee.alert.text" = "Network fee is used to ensure SORA system’s growth and stable performance."; +"lp.network.fee.alert.title" = "Network fee"; +"lp.pool.details.title" = "Pool details"; +"lp.pool.remove.warning.text" = "Removing pool tokens converts your position back into underlying tokens at the current rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive."; +"lp.pool.remove.warning.title" = "NOTE"; +"lp.remove.button.title" = "Remove Liquidity"; +"lp.remove.liquidity.screen.title" = "Remove Liquidity"; +"lp.reward.token.text" = "Earn %@"; +"lp.reward.token.title" = "Rewards Payout In"; +"lp.slippage.title" = "Slippage"; +"lp.supply.button.title" = "Supply Liquidity"; +"lp.supply.liquidity.screen.title" = "Supply Liquidity"; +"lp.token.pooled.text" = "Your %@ Pooled"; +"lp.user.pools.title" = "User pools"; "manage.assets.account.missing.text" = "添加一个账户..."; "manage.assets.search.hint" = "按资产搜索"; "members.common" = "成员"; @@ -486,6 +514,7 @@ "network.info.address" = "节点地址"; "network.info.name" = "节点名称"; "network.info.title" = "节点信息"; +"network.issue.main" = "连接错误:无法连接到网络。请重试。"; "network.issue.network.unavailible" = "网络不可用"; "network.issue.node.unavailable" = "节点不可用"; "network.issue.notofication" = "通知"; @@ -507,6 +536,7 @@ "nft.list.empty.message" = "目前还没有任何NFT。购买或铸造NFT以在此处查看。"; "nft.owner.title" = "拥有"; "nft.share.address" = "我要接收的公共地址:%s"; +"nft.spam.warning" = "小心某些 NFT 收藏品可能是骗局或垃圾邮件,在参与之前请先验证其真实性。保持安全!"; "nft.stub.text" = "NFT即将到来"; "nft.stub.title" = "对不起!"; "nft.tokenid.title" = "代币ID"; @@ -591,17 +621,20 @@ "polkaswap.disclaimer.title" = "免责声明"; "polkaswap.liqudity.fee.info" = "每笔交易的一部分(0.3%)作为协议激励,将支付给流动性提供者。"; "polkaswap.liquidity.provider.fee" = "流动性提供者费用"; +"polkaswap.liquidity.provider.fee" = "流动性提供者费用"; "polkaswap.market.alert.choose.action" = "选择资产"; "polkaswap.market.alert.message" = "您还没有完成兑换调整。一个或两个资产尚未选择,因此市场选择不可用。"; "polkaswap.market.alert.title" = "市场选择不可用"; "polkaswap.market.algorithm.title" = "市场算法"; "polkaswap.market.smart.description" = "SMART流动性路由通过从所有可用市场中仅选择最佳价格选项,确保任何交易的最佳价格。在可用时,将使用Token Bonding Curve(TBC)作为流动性,前提是资产价格比其他来源更实惠,在此之后才会利用XYK池。"; "polkaswap.market.stub" = "市场:"; +"polkaswap.market.stub" = "市场"; "polkaswap.market.tbc.description" = "TBC — 仅从Token Bonding Curve(一级市场)购买。价格可能与XYK池(二级市场)相比不利,但随着时间推移,通过获得的奖励价值可能会更加有利。"; "polkaswap.market.xyk.description" = "XYK — 只从XYK池(二级市场)购买。传统的XYK池兑换,任何人都可以通过改变做市商在x*y=k曲线上的位置来买卖资产。"; "polkaswap.max.received" = "最大可卖数量"; "polkaswap.maximum.sold.info" = "如果在确认之前出现大幅不利的价格波动,您的交易将会撤销。"; "polkaswap.min.received" = "最小接收量"; +"polkaswap.min.received" = "最小接收数量"; "polkaswap.minimum.received.info" = "如果在确认之前出现大幅不利的价格波动,您的交易将会撤销。"; "polkaswap.network.fee" = "网络费用"; "polkaswap.network.fee.info" = "网络费用是为了确保SORA系统的增长和稳定性能而使用的。"; @@ -684,8 +717,8 @@ "scam.additional.stub" = "附加:"; "scam.description.donation.stub" = "此地址已被标记为可疑。我们强烈建议您不要向该账户发送%s。"; "scam.description.exchange.stub" = "这个地址被标记为交易所,请注意存款和提款地址可能不同。"; -"scam.description.sanctions.stub" = "由于与受制裁国家相关的实体,此地址已被标记。我们强烈建议您不要向此账户发送%s。"; -"scam.description.scam.stub" = "这个地址因涉嫌欺诈行为而被标记。我们强烈建议您不要向该账户转账%s。"; +"scam.description.sanctions.stub" = "由于与受制裁国家相关的实体,此地址已被标记。我们强烈建议您不要向此账户发送%s。"; +"scam.description.scam.stub" = "此地址已被标记存在诈骗嫌疑。我们强烈建议您不要将{asset}发送到此账户。"; "scam.name.stub" = "姓名:"; "scam.reason.stub" = "原因:"; "scam.warning.alert.subtitle" = "我们强烈建议您不要向此账户发送%s。"; @@ -704,7 +737,7 @@ "select.save.type" = "选择保存类型"; "select.suggested.validators.warning" = "算法验证人建议并不构成金融咨询或建议。质押是一项高风险活动,算法验证人的建议并不能完全减轻这种风险。由算法推荐的验证人仍然有可能离开候选池。由算法推荐的验证人也可以在被推荐和/或选择后随时更改其参数(例如佣金率等)。您可能因此或其他原因而失去奖励。只有在进行尽职调查并慎重考虑所涉及风险后,根据自己的判断决定质押代币和采用验证人建议。"; "select.validators.disclaimer" = "免责声明:算法验证人建议并不构成金融咨询或建议。质押是一项高风险活动,算法验证人的建议并不能完全消除这种风险。由算法提供的验证人仍然可能被惩罚。由算法提供的验证人也可以在被推荐和/或选择后随时更改其参数(例如佣金率等)。您可能因为这些原因或其他原因而失去代币或奖励。请在进行尽职调查并慎重考虑所涉及风险后,根据自己的判断谨慎质押代币和使用验证人建议。"; -"send.all.title" = "发送所有代币并收获账户"; +"send.all.title" = "发送所有代币并获取账户"; "send.confirm.amount.title" = "正在发送\n%@"; "send.fund.title" = "发送资金"; "settings.add.wallet" = "添加钱包"; @@ -835,8 +868,11 @@ "staking.pool.create.root" = "Root"; "staking.pool.create.stateToggler" = "状态切换器"; "staking.pool.create.title" = "创建一个池子"; +"staking.pool.create.title" = "创建一个池"; "staking.pool.info.title" = "池信息"; "staking.pool.management.select.validators.subtitle" = "要继续,您应该\n选择验证人"; +"staking.pool.management.select.validators.subtitle" = "为了继续,你应该\n选择验证人"; +"staking.pool.management.select.validators.title" = "选择池验证人"; "staking.pool.management.select.validators.title" = "选择池验证人"; "staking.pool.rewards.delay.text" = "随时质押。您将在%s之后开始赚取利息"; "staking.pool.select.validators.manual" = "手动选择"; @@ -895,6 +931,7 @@ "staking.round.title" = "轮次 # %@"; "staking.select.suggested" = "选择推荐的"; "staking.select.validators.confirm.title" = "选择验证人"; +"staking.select.validators.confirm.title" = "选择验证人"; "staking.select.validators.custom.button.title" = "自己选择"; "staking.select.validators.custom.desc" = "你应该相信你的提名人有能力和诚实地行事,仅仅基于他们当前的盈利能力来做决策可能会导致利润减少甚至资金损失。"; "staking.select.validators.custom.title" = "与您的验证人进行质押"; @@ -1025,9 +1062,9 @@ "transaction.details.from" = "从"; "transaction.details.hash.title" = "外部哈希"; "transaction.details.view.etherscan" = "在Etherscan上查看"; +"transaction.details.view.oklink" = "在OKX浏览器中查看"; "transaction.details.view.polkascan" = "在Polkascan中查看"; "transaction.details.view.reefscan" = "在Reefscan中查看"; -"transaction.details.view.oklink" = "View in OKX explorer"; "transaction.details.view.subscan" = "在Subscan中查看"; "transaction.list.header" = "所有交易"; "transaction.status.completed" = "已完成"; @@ -1043,6 +1080,8 @@ "username.setup.title" = "创建一个账户"; "username.setup.title.2.0" = "创建一个新的钱包"; "validator.info.comission.title" = "佣金"; +"validator.info.min.stake.alert.text" = "活跃提名人中的最低质押金额为%s。要获得奖励,您需要质押更多。"; +"validator.info.min.stake.among.active.nominators.text" = "活跃提名人的最低质押"; "validators.list.empty.message" = "未找到验证人"; "verify.phone.number.title" = "验证您的电话号码"; "vesting.claim.disclaimer.text" = "由于每个平行链的独特归属计划,我们的应用无法显示可认领的锁定代币数量。请注意,如果预计金额微不足道且与涉及的交易费用相当,发起认领可能是不切实际的。要全面了解您的锁定余额,请咨询Subscan区块浏览器。我们敦促您仔细评估信息,并根据自己的判断继续操作。"; @@ -1052,6 +1091,7 @@ "view.wallet" = "查看钱包"; "wallet.account.locks.democracy" = "民主"; "wallet.account.locks.vesting" = "归属"; +"wallet.all.assets.hidden" = "你已经隐藏所有资产"; "wallet.asset.buy" = "购买"; "wallet.asset.buy.with" = "购买%s用"; "wallet.asset.receive" = "接收"; @@ -1110,6 +1150,7 @@ "wallet.send.balance.total.after.transfer" = "转账后的总额"; "wallet.send.confirm.title" = "确认转账"; "wallet.send.dead.recipient.message" = "您的转账将失败,因为目标账户上的最终金额将低于最低余额。请尝试增加金额。"; +"wallet.send.dead.recipient.message.v2" = "您的转账将失败,因为目标账户的最终金额(%1$s)将低于最低余额(%2$s)。请增加%3$s的金额。"; "wallet.send.dead.recipient.title" = "金额太低"; "wallet.send.eth.dead.recipient.message" = "接收方账户中的以太币余额不足,导致无法完成ERC20代币转账。请确保接收方有足够的以太币来进行转账。"; "wallet.send.existential.warning" = "您的转账将从区块存储中移除该账户,因为这将使总余额低于最低余额要求。"; @@ -1130,9 +1171,11 @@ "what.accounts.for.export" = "您想导出钱包中的哪些账户?"; "xcm.cross.chain.button.title" = "跨链"; "xcm.cross.chain.invalid.address.message" = "根据提供的地址,您正在尝试在错误的网络上进行转账。"; +"xcm.cross.chain.invalid.address.message" = "根据提供的地址,您正在尝试在错误的网络上进行转账。"; "xcm.cross.chain.invalid.address.title" = "不是网络地址"; "xcm.destination.network.fee.title" = "目标网络费用"; "xcm.destination.network.title" = "目标网络"; +"xcm.low.amaunt.assetSymbol.alert" = "目前,为确保稳定性和安全性,需要最低金额%@作为跨链费用。我们感谢您的理解。"; "xcm.mywallets.button.title" = "我的钱包"; "xcm.origin.network.fee.title" = "原始网络费用"; "xcm.origin.network.title" = "原始网络"; @@ -1141,131 +1184,4 @@ "your.validators.stop.nominating.title" = "停止提名"; "your.validators.validator.total.stake" = "总质押:%@"; "сurrencies.stub.text" = "货币"; -"cant.fetch.refresh" = "无法获取数据,请点击刷新。"; -"card.attention.text" = "注意"; -"card.hub.manage.card" = "管理 SORA 卡"; -"card.hub.manage.card.alert.message" = "请安装官方的 SORA 卡应用程序来管理您的 SORA 卡。点击“确定”以前往 App Store。"; -"card.hub.manage.card.alert.title" = "下载 SORA 卡应用"; -"card.hub.settings.logout.button" = "退出登录"; -"card.hub.settings.logout.description" = "您即将退出 SORA 卡。您仍然可以使用 SORA 卡独立应用程序,但余额将不再在 SORA 钱包中可用。"; -"card.hub.settings.logout.title" = "退出 SORA 卡"; -"card.hub.settings.title" = "卡片设置"; -"card.hub.title" = "卡片详情"; -"card.hub.update.button" = "立即更新"; -"card.hub.update.description" = "您正在使用过时的应用版本,部分功能可能无法正常工作。"; -"card.hub.update.title" = "更新应用至最新版本"; -"card.issuance.screen.free.card.description" = "免费发行一张卡:\ -在您的SORA账户中持有、质押或提供至少价值%@欧元的XOR的流动性。"; -"card.issuance.screen.free.card.get.xor" = "获得%@个XOR。"; -"card.issuance.screen.paid.card.description" = "发卡一次性费用"; -"card.issuance.screen.paid.card.note" = "注:付费卡发行将在稍后阶段提供。"; -"card.issuance.screen.paid.card.pay.euro" = "支付%@欧元的发行费用"; -"card.issuance.screen.paid.card.title" = "%@欧元发行费"; -"card.issuance.screen.title" = "卡片发行"; -"card.or" = "或者"; -"card.update.button" = "更新应用程序"; -"card.update.title" = "请更新应用程序至最新版本以使用SORA卡"; -"cardhub.coming.soon" = "即将推出卡片管理功能"; -"cardhub.exchange" = "兑换"; -"cardhub.freeze" = "冻结"; -"cardhub.iban.title" = "IBAN账户详情"; -"cardhub.top.up" = "充值"; -"cardhub.transfer" = "转账"; -"change.email.title" = "更改您的电子邮件"; -"common.back" = "后退"; -"common.change.email" = "使用另一个电子邮件"; -"common.no.spam" = "不要垃圾邮件!只是为了保护您的账户"; -"common.resend.code" = "重新发送验证码"; -"common.resend.link" = "重新发送链接"; -"common.resend.timer" = "重新发送在%02d:% 02d"; -"common.send.code" = "发送短信代码"; -"common.send.link" = "发送链接"; -"common.support" = "支持"; -"common.try.again" = "请再试一次"; -"common.user.not.found" = "未找到手机号码,请重新检查并重试。"; -"details.already.have.card" = "我已经有一张卡了"; -"details.already.used.free.try" = "您已经使用了免费尝试"; -"details.annual.service.fee" = "年服务费€0"; -"details.description" = "在您的SORA钱包中获得一个欧元IBAN账户和可访问的借记卡"; -"details.enough.xor.desription" = "你有足够的XOR"; -"details.free.card.issuance" = "免费发卡"; -"details.free.card.issuance.conditions.euro" = "或%@欧元的申请费"; -"details.free.card.issuance.conditions.xor" = "如果您在SORA账户中持有、质押或提供至少价值%@欧元的XOR的流动性"; -"details.get.more.xor" = "获取更多的XOR"; -"details.issue.card" = "免费发卡"; -"details.need.xor.desription" = "您需要%@更多 XOR (€ %@ )"; -"details.title" = "使用法定货币或加密货币充值SORA卡,可在线支付、门店消费或从ATM机取款"; -"enter.email.description" = "魔法链接将发送\n到您的电子邮件"; -"enter.email.input.field.label" = "电子邮件"; -"enter.email.title" = "请输入您的电子邮件"; -"enter.phone.number.description" = "您将通过短信收到\n验证码"; -"enter.phone.number.phone.input.field.label" = "电话号码"; -"entry.card.info" = "卡片信息"; -"get.more.xor.dialog.buy.option" = "用卡购买XOR"; -"get.more.xor.dialog.deposit.option" = "接收 XOR"; -"get.more.xor.dialog.description" = "你可以使用 XOR 进行兑换或用欧元购买"; -"get.more.xor.dialog.swap.option" = "用加密货币兑换XOR"; -"get.prepared.alert" = "您只有%@免费尝试机会通过 KYC 流程。此后的所有其他尝试都将花费 € %@ 。下次更新应用程序后将提供付费尝试。"; -"get.prepared.need" = "完成KYC需要:"; -"get.prepared.ok.title" = "好的,我准备好了"; -"get.prepared.personal.info.description" = "请填写表格,包括您的姓名和地址。"; -"get.prepared.personal.info.title" = "提交个人信息"; -"get.prepared.proof.address.description" = "包含您的全名和地址且不超过 3 个月的正式文件(水电费账单、银行对账单、政府声明或信件)。"; -"get.prepared.proof.address.note" = "请注意:像Revolut和N26这样的新型银行的声明目前不被视为有效的地址证明。"; -"get.prepared.proof.address.title" = "提交地址证明照片"; -"get.prepared.submit.id.photo.description" = "护照、驾照、身份证或居住许可证"; -"get.prepared.submit.id.photo.title" = "提交您的身份证明照片"; -"get.prepared.take.selfie.description" = "在过程中将提供指导"; -"get.prepared.take.selfie.title" = "自拍"; -"get.prepared.title" = "做好准备"; -"iban.frozen.description" = "您的国际银行账号(IBAN)已被冻结。\ -欲了解更多信息,请联系%@。"; -"iban.pending.description" = "您的 IBAN 正在等待签发。\ -如果等待时间超过 72 小时,请通过%@联系我们"; -"iban.suspended.description" = "您的IBAN已被暂停使用。\ -如需了解更多信息,请联系%@。"; -"kyc.result.verification.in.progress" = "正在进行验证"; -"kyc.result.verification.in.progress.description" = "您已成功完成KYC申请。审核正在进行中,您可以很快收到决定。\n\n通常情况下,决定会在同一天做出,在某些情况下可能需要最多3天时间。"; -"log.out" = "退出登录"; -"login.title" = "登录或注册"; -"no.free.kyc.attempts.description" = "您已经使用了免费的尝试次数来完成KYC过程。\n\n我们恳请您等待下一次应用程序升级以进行付费尝试。"; -"no.free.kyc.attempts.title" = "没有更多的免费尝试"; -"otp.error.message.wrong.code" = "代码不正确。请再试一次。"; -"paid.attempts.available.later" = "付费尝试将在后期提供"; -"payment.widget.unavailable.confirm" = "是的,我理解。"; -"payment.widget.unavailable.description" = "对于不便我们深表歉意。\n我们正在努力解决此问题。\n请稍后再试。"; -"payment.widget.unavailable.message" = "支付小部件目前不可用。"; -"payment.widget.unavailable.title" = "小部件不可用"; -"select.country.title" = "选择您所在的国家/地区"; -"status.not.started" = "获取SORA卡"; -"terms.and.conditions.confirm.description" = "继续操作即表示您已阅读、理解并接受这些政策。"; -"terms.and.conditions.sora.community.alert" = "SORA社区不会收集任何个人数据,如果您想获得SORA卡和IBAN账户,您需要直接与发卡机构完成KYC流程。"; -"unsupported.countries.disclaimer" = "目前来自某些国家的居民无法申请SORA卡。"; -"unsupported.countries.link" = "查看列表"; -"user.registration.first.name.input.filed.label" = "名"; -"user.registration.last.name.input.filed.label" = "姓"; -"user.registration.title" = "介绍一下自己"; -"verification.failed.description" = "KYC已终止或未能通过验证。"; -"verification.failed.title" = "验证失败"; -"verification.rejected.description" = "您的申请已被拒绝。"; -"verification.rejected.screen.attempts.price.disclaimer" = "每次其他尝试都会花费你%@欧元。"; -"verification.rejected.screen.attempts.used" = "您已经使用完了免费的KYC尝试次数。"; -"verification.rejected.screen.try.again.for.euros" = "再试一次,%@欧元。"; -"verification.rejected.screen.try.again.for.free" = "免费重试"; -"verification.rejected.support" = "支持"; -"verification.rejected.title" = "验证被拒绝"; -"verification.successful.description" = "您的 KYC 验证成功,我们已经准备向您发送 SORA 卡!"; -"verification.successful.title" = "申请已批准"; -"verify.email.description" = "请点击已发送\n至%@\n的魔法链接进行跟踪。\ -邮件可能需要几分钟才能到达,请在5分钟后检查您的垃圾邮件文件夹,以防未收到该邮件。"; -"verify.email.resend" = "%@以后重新发送"; -"verify.email.title" = "验证您的电子邮件"; -"verify.phone.number.code.input.field.label" = "短信验证码"; -"verify.phone.number.description" = "输入已发送到\n%@的短信验证码"; -"verify.phone.number.send.code" = "发送验证码"; -"balance.locks.blocked.row.title" = "Blocked"; -"wallet.send.dead.recipient.message.v2" = "Your transfer will fail since the final amount (%s) on the destination account will be less than the minimal balance (%s). Please increase the amount by %s"; -"common.more" = "more"; -"nft.spam.warning" = "Beware of a scam/spam NFT collection – verify authenticity before engaging. Stay safe!"; -"wallet.all.assets.hidden" = "You have hidden all assets"; -"xcm.low.amaunt.assetSymbol.alert" = "Currently, there's a min. amount %@ for bridging to ensure the stability and security. We appreciate your understanding."; +"common.more" = "More"; From dd90d5ad208ab82a2d864f9930568bb29757f23f Mon Sep 17 00:00:00 2001 From: Alex Lebedko Date: Thu, 27 Jun 2024 16:49:03 +0700 Subject: [PATCH 056/115] packages update --- .../xcshareddata/swiftpm/Package.resolved | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index 590d0594c1..e35d64d6e7 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -126,6 +126,15 @@ "version" : "0.1.7" } }, + { + "identity" : "shared-features-spm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/soramitsu/shared-features-spm.git", + "state" : { + "branch" : "liquidity-pools-fw", + "revision" : "33c0c06715133b359efbc1a3e35a4952f88b9006" + } + }, { "identity" : "swift-atomics", "kind" : "remoteSourceControl", @@ -216,15 +225,6 @@ "version" : "1.3.0" } }, - { - "identity" : "swiftformat", - "kind" : "remoteSourceControl", - "location" : "https://github.com/nicklockwood/SwiftFormat", - "state" : { - "revision" : "dd989a46d0c6f15c016484bab8afe5e7a67a4022", - "version" : "0.54.0" - } - }, { "identity" : "swiftimagereadwrite", "kind" : "remoteSourceControl", From d0ed467dbaee410e0fe50b05fbb670299d6f768b Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Fri, 28 Jun 2024 11:05:30 +0500 Subject: [PATCH 057/115] [#FLW-4717] There is an error when we are trying ti sign transaction on Polkadot.js --- fearless.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 12 +++---- .../Model/WalletConnectExtrinsic.swift | 9 +++++ .../WalletConnectPolkadorSigner.swift | 5 +-- .../WalletConnectPolkadotParser.swift | 33 +++++-------------- .../ChainRegistry/ChainSyncService.swift | 6 ++-- .../ConnectionPool/ConnectionPool.swift | 1 - 7 files changed, 30 insertions(+), 38 deletions(-) diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index 24e7de5be5..236d1a47f2 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -19300,7 +19300,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/soramitsu/shared-features-spm.git"; requirement = { - branch = "socket-connection-refactoring"; + branch = "fearless-wallet"; kind = branch; }; }; diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index adaf4c127a..ec2082f6d1 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/google-api-objectivec-client-for-rest.git", "state" : { - "revision" : "d46fb27cf61e08285a727c18a2ae0dbc20d91b2f", - "version" : "3.5.4" + "revision" : "a8c1e0b1173659d0be452680582c28556372ef74", + "version" : "3.5.5" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/gtm-session-fetcher.git", "state" : { - "revision" : "0382ca27f22fb3494cf657d8dc356dc282cd1193", - "version" : "3.4.1" + "revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b", + "version" : "3.5.0" } }, { @@ -132,8 +132,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/soramitsu/shared-features-spm.git", "state" : { - "branch" : "socket-connection-refactoring", - "revision" : "1527989589adeff7821582ec80b8dcf7698f7bee" + "branch" : "fearless-wallet", + "revision" : "dd49df35001f0dd411e6f0e289311a10b19b86c2" } }, { diff --git a/fearless/ApplicationLayer/Services/WalletConnect/Model/WalletConnectExtrinsic.swift b/fearless/ApplicationLayer/Services/WalletConnect/Model/WalletConnectExtrinsic.swift index ee2414c74f..910eda662d 100644 --- a/fearless/ApplicationLayer/Services/WalletConnect/Model/WalletConnectExtrinsic.swift +++ b/fearless/ApplicationLayer/Services/WalletConnect/Model/WalletConnectExtrinsic.swift @@ -21,6 +21,15 @@ enum WalletConnectPolkadotCall: Codable { case raw(bytes: Data) case callable(value: RuntimeCall) + var payloadType: ExtrinsicBuilder.PayloadType { + switch self { + case .callable: + return .regular + case .raw: + return .rawData + } + } + func encode(to encoder: Encoder) throws { switch self { case let .raw(bytes): diff --git a/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectPolkadorSigner.swift b/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectPolkadorSigner.swift index 55156cb618..79e7edfa15 100644 --- a/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectPolkadorSigner.swift +++ b/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectPolkadorSigner.swift @@ -55,11 +55,11 @@ final class WalletConnectPolkadorSigner: WalletConnectPayloadSigner { let transaction = try params.get(TransactionPayload.self) let builder = try await createBuilder(for: transaction) let coderFactory = try await fetchCoderFactory() - let signature = try builder.buildSignature( + let signaturePayload = try builder.buildSignaturePayload( encodingBy: coderFactory.createEncoder(), metadata: coderFactory.metadata ) - let signedRawData = try transactionSigner.sign(signature).rawData() + let signedRawData = try transactionSigner.sign(signaturePayload).rawData() let encoded = try encode(rawData: signedRawData, encoder: coderFactory.createEncoder()) let result = WalletConnectPolkadotSignature( id: UInt.random(in: 0 ..< UInt.max), @@ -101,6 +101,7 @@ final class WalletConnectPolkadorSigner: WalletConnectPayloadSigner { .with(address: transaction.address) .with(nonce: UInt32(transaction.nonce)) .with(era: transaction.era, blockHash: transaction.blockHash) + .with(payloadType: transaction.method.payloadType) switch transaction.method { case let .callable(value): diff --git a/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectPolkadotParser.swift b/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectPolkadotParser.swift index 75fe971631..9bd1c6d6f0 100644 --- a/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectPolkadotParser.swift +++ b/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectPolkadotParser.swift @@ -15,10 +15,6 @@ final class WalletConnectPolkadorParserImpl: WalletConnectPolkadotParser { ChainRegistryFacade.sharedRegistry }() - private lazy var operationQueue = { - OperationQueue() - }() - func parse( transactionPayload: TransactionPayload, chain: ChainModel @@ -68,27 +64,16 @@ final class WalletConnectPolkadorParserImpl: WalletConnectPolkadotParser { guard let runtimeProvider = chainRegistry.getRuntimeProvider(for: chain.chainId) else { throw RuntimeProviderError.providerUnavailable } - let fetchCoderFactoryOperation = runtimeProvider.fetchCoderFactoryOperation() - operationQueue.addOperation(fetchCoderFactoryOperation) - - return try await withCheckedThrowingContinuation { continuation in - fetchCoderFactoryOperation.completionBlock = { - do { - let codingFactory = try fetchCoderFactoryOperation.extractNoCancellableResultData() - let methodData = try Data(hexStringSSF: transactionPayload.method) - let methodDecoder = try codingFactory.createDecoder(from: methodData) + let codingFactory = try await runtimeProvider.fetchCoderFactory() + let methodData = try Data(hexStringSSF: transactionPayload.method) + let methodDecoder = try codingFactory.createDecoder(from: methodData) - let call: WalletConnectPolkadotCall - if let callableMethod: RuntimeCall = try? methodDecoder.read(of: KnownType.call.rawValue) { - call = .callable(value: callableMethod) - } else { - call = .raw(bytes: methodData) - } - continuation.resume(returning: call) - } catch { - continuation.resume(throwing: error) - } - } + let call: WalletConnectPolkadotCall + if let callableMethod: RuntimeCall = try? methodDecoder.read(of: KnownType.call.rawValue) { + call = .callable(value: callableMethod) + } else { + call = .raw(bytes: methodData) } + return call } } diff --git a/fearless/Common/Services/ChainRegistry/ChainSyncService.swift b/fearless/Common/Services/ChainRegistry/ChainSyncService.swift index 1f241670da..4f974e47be 100644 --- a/fearless/Common/Services/ChainRegistry/ChainSyncService.swift +++ b/fearless/Common/Services/ChainRegistry/ChainSyncService.swift @@ -281,16 +281,14 @@ extension ChainSyncService: SchedulerDelegate { } extension ChainSyncService: CountdownTimerDelegate { - func didStart(with interval: TimeInterval) { + func didStart(with _: TimeInterval) { isSyncing = true - print("CountdownTimerDelegate didStart", interval) } func didCountdown(remainedInterval _: TimeInterval) {} - func didStop(with remainedInterval: TimeInterval) { + func didStop(with _: TimeInterval) { isSyncing = false - print("CountdownTimerDelegate didStop", remainedInterval) } } diff --git a/fearless/Common/Services/ChainRegistry/ConnectionPool/ConnectionPool.swift b/fearless/Common/Services/ChainRegistry/ConnectionPool/ConnectionPool.swift index 6c18c88932..5945159875 100644 --- a/fearless/Common/Services/ChainRegistry/ConnectionPool/ConnectionPool.swift +++ b/fearless/Common/Services/ChainRegistry/ConnectionPool/ConnectionPool.swift @@ -88,7 +88,6 @@ extension ConnectionPool: ConnectionPoolProtocol { extension ConnectionPool: WebSocketEngineDelegate { func webSocketDidChangeState( engine: WebSocketEngine, - from _: WebSocketEngine.State, to newState: WebSocketEngine.State ) { guard let chainId = engine.connectionName else { From 3c32bb108f19053699a1b4e7c730542d95423d40 Mon Sep 17 00:00:00 2001 From: Alex Lebedko Date: Fri, 28 Jun 2024 16:31:47 +0700 Subject: [PATCH 058/115] pull to refresh added --- .../LiquidityPoolDetailsAssembly.swift | 5 +-- .../LiquidityPoolDetailsPresenter.swift | 9 +++-- .../LiquidityPoolDetailsProtocols.swift | 17 +++++++-- .../LiquidityPoolDetailsRouter.swift | 8 ++--- ...LiquidityPoolRemoveLiquidityAssembly.swift | 9 +++-- ...iquidityPoolRemoveLiquidityPresenter.swift | 9 +++-- ...iquidityPoolRemoveLiquidityProtocols.swift | 1 + .../LiquidityPoolRemoveLiquidityRouter.swift | 11 +++--- ...ityPoolRemoveLiquidityViewController.swift | 2 +- ...tyPoolRemoveLiquidityConfirmAssembly.swift | 6 ++-- .../LiquidityPoolSupplyAssembly.swift | 5 +-- .../LiquidityPoolSupplyPresenter.swift | 6 +++- .../LiquidityPoolSupplyProtocols.swift | 1 + .../LiquidityPoolSupplyRouter.swift | 9 ++++- .../LiquidityPoolSupplyConfirmAssembly.swift | 6 ++-- .../LiquidityPoolSupplyConfirmPresenter.swift | 6 +++- ...AvailableLiquidityPoolsListPresenter.swift | 17 ++++++++- .../UserLiquidityPoolsListPresenter.swift | 17 ++++++++- ...erLiquidityPoolsListViewModelFactory.swift | 25 ++++++------- .../LiquidityPoolsListProtocols.swift | 4 +++ .../LiquidityPoolsListRouter.swift | 4 +-- .../LiquidityPoolsListViewController.swift | 11 +++++- .../LiquidityPoolsListViewLayout.swift | 2 +- .../LiquidityPoolsOverviewPresenter.swift | 14 ++++++++ .../LiquidityPoolsOverviewProtocols.swift | 1 + ...LiquidityPoolsOverviewViewController.swift | 9 +++++ .../LiquidityPoolsOverviewViewLayout.swift | 35 +++++++++++++------ 27 files changed, 189 insertions(+), 60 deletions(-) diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsAssembly.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsAssembly.swift index 5471a6988b..209546e3f4 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsAssembly.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsAssembly.swift @@ -4,7 +4,7 @@ import SSFPolkaswap import SSFModels final class LiquidityPoolDetailsAssembly { - static func configureModule(assetIdPair: AssetIdPair, chain: ChainModel, wallet: MetaAccountModel, input: LiquidityPoolDetailsInput) -> LiquidityPoolDetailsModuleCreationResult? { + static func configureModule(assetIdPair: AssetIdPair, chain: ChainModel, wallet: MetaAccountModel, input: LiquidityPoolDetailsInput, poolOperationFlowsClosure: @escaping () -> Void) -> LiquidityPoolDetailsModuleCreationResult? { let localizationManager = LocalizationManager.shared let chainRegistry = ChainRegistryFacade.sharedRegistry @@ -32,7 +32,8 @@ final class LiquidityPoolDetailsAssembly { viewModelFactory: viewModelFactory, chain: chain, wallet: wallet, - input: input + input: input, + poolOperationFlowsClosure: poolOperationFlowsClosure ) let view = LiquidityPoolDetailsViewController( diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift index ecbdc1f860..c5fcd519c8 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsPresenter.swift @@ -95,6 +95,7 @@ final class LiquidityPoolDetailsPresenter { private let chain: ChainModel private let wallet: MetaAccountModel private let input: LiquidityPoolDetailsInput + private var poolOperationFlowsClosure: () -> Void private var liquidityPair: LiquidityPair? private var accountPoolInfo: AccountPool? @@ -113,7 +114,8 @@ final class LiquidityPoolDetailsPresenter { viewModelFactory: LiquidityPoolDetailsViewModelFactory, chain: ChainModel, wallet: MetaAccountModel, - input: LiquidityPoolDetailsInput + input: LiquidityPoolDetailsInput, + poolOperationFlowsClosure: @escaping () -> Void ) { self.interactor = interactor self.router = router @@ -123,6 +125,7 @@ final class LiquidityPoolDetailsPresenter { self.chain = chain self.wallet = wallet self.input = input + self.poolOperationFlowsClosure = poolOperationFlowsClosure liquidityPair = input.liquidityPair @@ -174,7 +177,7 @@ extension LiquidityPoolDetailsPresenter: LiquidityPoolDetailsViewOutput { return } - router.showSupplyFlow(liquidityPair: liquidityPair, chain: chain, wallet: wallet, availablePairs: input.availablePairs, from: view) + router.showSupplyFlow(liquidityPair: liquidityPair, chain: chain, wallet: wallet, availablePairs: input.availablePairs, flowClosure: poolOperationFlowsClosure, from: view) } func removeButtonClicked() { @@ -182,7 +185,7 @@ extension LiquidityPoolDetailsPresenter: LiquidityPoolDetailsViewOutput { return } - router.showRemoveFlow(liquidityPair: liquidityPair, chain: chain, wallet: wallet, from: view) + router.showRemoveFlow(liquidityPair: liquidityPair, chain: chain, wallet: wallet, flowClosure: poolOperationFlowsClosure, from: view) } func didTapApyInfo() { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsProtocols.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsProtocols.swift index aacae2a6bb..eecd493e51 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsProtocols.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsProtocols.swift @@ -4,8 +4,21 @@ import SSFModels typealias LiquidityPoolDetailsModuleCreationResult = (view: LiquidityPoolDetailsViewInput, input: LiquidityPoolDetailsModuleInput) protocol LiquidityPoolDetailsRouterInput: AnyObject, AnyDismissable, SheetAlertPresentable { - func showSupplyFlow(liquidityPair: LiquidityPair, chain: ChainModel, wallet: MetaAccountModel, availablePairs: [LiquidityPair]?, from view: ControllerBackedProtocol?) - func showRemoveFlow(liquidityPair: LiquidityPair, chain: ChainModel, wallet: MetaAccountModel, from view: ControllerBackedProtocol?) + func showSupplyFlow( + liquidityPair: LiquidityPair, + chain: ChainModel, + wallet: MetaAccountModel, + availablePairs: [LiquidityPair]?, + flowClosure: @escaping () -> Void, + from view: ControllerBackedProtocol? + ) + func showRemoveFlow( + liquidityPair: LiquidityPair, + chain: ChainModel, + wallet: MetaAccountModel, + flowClosure: @escaping () -> Void, + from view: ControllerBackedProtocol? + ) } protocol LiquidityPoolDetailsModuleInput: AnyObject {} diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsRouter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsRouter.swift index 322e21b01f..4d635dd1c0 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsRouter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolDetails/LiquidityPoolDetailsRouter.swift @@ -3,16 +3,16 @@ import SSFModels import SSFPools final class LiquidityPoolDetailsRouter: LiquidityPoolDetailsRouterInput { - func showSupplyFlow(liquidityPair: LiquidityPair, chain: ChainModel, wallet: MetaAccountModel, availablePairs: [LiquidityPair]?, from view: ControllerBackedProtocol?) { - guard let controller = LiquidityPoolSupplyAssembly.configureModule(chain: chain, wallet: wallet, liquidityPair: liquidityPair, availablePairs: availablePairs)?.view.controller else { + func showSupplyFlow(liquidityPair: LiquidityPair, chain: ChainModel, wallet: MetaAccountModel, availablePairs: [LiquidityPair]?, flowClosure: @escaping () -> Void, from view: ControllerBackedProtocol?) { + guard let controller = LiquidityPoolSupplyAssembly.configureModule(chain: chain, wallet: wallet, liquidityPair: liquidityPair, availablePairs: availablePairs, flowClosure: flowClosure)?.view.controller else { return } view?.controller.navigationController?.pushViewController(controller, animated: true) } - func showRemoveFlow(liquidityPair: LiquidityPair, chain: ChainModel, wallet: MetaAccountModel, from view: ControllerBackedProtocol?) { - guard let controller = LiquidityPoolRemoveLiquidityAssembly.configureModule(wallet: wallet, chain: chain, liquidityPair: liquidityPair)?.view.controller else { + func showRemoveFlow(liquidityPair: LiquidityPair, chain: ChainModel, wallet: MetaAccountModel, flowClosure: @escaping () -> Void, from view: ControllerBackedProtocol?) { + guard let controller = LiquidityPoolRemoveLiquidityAssembly.configureModule(wallet: wallet, chain: chain, liquidityPair: liquidityPair, flowClosure: flowClosure)?.view.controller else { return } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityAssembly.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityAssembly.swift index 049b38dd7c..b8b744f4c5 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityAssembly.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityAssembly.swift @@ -6,7 +6,12 @@ import SSFPolkaswap import SoraKeystore final class LiquidityPoolRemoveLiquidityAssembly { - static func configureModule(wallet: MetaAccountModel, chain: ChainModel, liquidityPair: LiquidityPair) -> LiquidityPoolRemoveLiquidityModuleCreationResult? { + static func configureModule( + wallet: MetaAccountModel, + chain: ChainModel, + liquidityPair: LiquidityPair, + flowClosure: @escaping () -> Void + ) -> LiquidityPoolRemoveLiquidityModuleCreationResult? { guard let response = wallet.fetch(for: chain.accountRequest()) else { return nil } @@ -41,7 +46,7 @@ final class LiquidityPoolRemoveLiquidityAssembly { let interactor = LiquidityPoolRemoveLiquidityInteractor(lpOperationService: lpOperationService, lpDataService: lpDataService, liquidityPair: liquidityPair, priceLocalSubscriber: PriceLocalStorageSubscriberImpl.shared, chain: chain, accountInfoSubscriptionAdapter: accountInfoSubscriptionAdapter, wallet: wallet) let router = LiquidityPoolRemoveLiquidityRouter() let dataValidatingFactory = SendDataValidatingFactory(presentable: router) - let presenter = LiquidityPoolRemoveLiquidityPresenter(interactor: interactor, router: router, localizationManager: localizationManager, wallet: wallet, logger: Logger.shared, chain: chain, liquidityPair: liquidityPair, dataValidatingFactory: dataValidatingFactory, confirmViewModelFactory: nil, removeInfo: nil) + let presenter = LiquidityPoolRemoveLiquidityPresenter(interactor: interactor, router: router, localizationManager: localizationManager, wallet: wallet, logger: Logger.shared, chain: chain, liquidityPair: liquidityPair, dataValidatingFactory: dataValidatingFactory, confirmViewModelFactory: nil, removeInfo: nil, flowClosure: flowClosure) let view = LiquidityPoolRemoveLiquidityViewController( output: presenter, diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityPresenter.swift index 58280ca511..8f2a1dea48 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityPresenter.swift @@ -58,6 +58,7 @@ final class LiquidityPoolRemoveLiquidityPresenter { private let liquidityPair: LiquidityPair private let dataValidatingFactory: SendDataValidatingFactory private let confirmViewModelFactory: LiquidityPoolSupplyConfirmViewModelFactory? + private var flowClosure: () -> Void private var removeInfo: RemoveLiquidityInfo? private var reserves: BigUInt? @@ -111,7 +112,8 @@ final class LiquidityPoolRemoveLiquidityPresenter { liquidityPair: LiquidityPair, dataValidatingFactory: SendDataValidatingFactory, confirmViewModelFactory: LiquidityPoolSupplyConfirmViewModelFactory?, - removeInfo: RemoveLiquidityInfo? + removeInfo: RemoveLiquidityInfo?, + flowClosure: @escaping () -> Void ) { self.interactor = interactor self.router = router @@ -123,6 +125,7 @@ final class LiquidityPoolRemoveLiquidityPresenter { self.confirmViewModelFactory = confirmViewModelFactory self.removeInfo = removeInfo dexId = liquidityPair.dexId + self.flowClosure = flowClosure if let removeInfo = removeInfo, let utilityAsset = chain.utilityAssets().first { totalIssuance = removeInfo.totalIssuances.toSubstrateAmount(precision: Int16(utilityAsset.precision)) @@ -375,7 +378,7 @@ extension LiquidityPoolRemoveLiquidityPresenter: LiquidityPoolRemoveLiquidityCon guard let removeInfo else { return } - + runLoadingState() interactor.submit(removeLiquidityInfo: removeInfo) } @@ -491,6 +494,7 @@ extension LiquidityPoolRemoveLiquidityPresenter: LiquidityPoolRemoveLiquidityVie wallet: self.wallet, liquidityPair: self.liquidityPair, info: info, + flowClosure: self.flowClosure, from: self.setupView ) } @@ -575,6 +579,7 @@ extension LiquidityPoolRemoveLiquidityPresenter: LiquidityPoolRemoveLiquidityInt return } + flowClosure() resetLoadingState() router.complete(on: confirmView, title: hash, chainAsset: utilityChainAsset) diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityProtocols.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityProtocols.swift index 7d19f31839..7c5f9b537d 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityProtocols.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityProtocols.swift @@ -12,6 +12,7 @@ protocol LiquidityPoolRemoveLiquidityRouterInput: AnyObject, ErrorPresentable, S wallet: MetaAccountModel, liquidityPair: LiquidityPair, info: RemoveLiquidityInfo, + flowClosure: @escaping () -> Void, from view: ControllerBackedProtocol? ) diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityRouter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityRouter.swift index 8884f6a7f0..b19992900e 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityRouter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityRouter.swift @@ -9,9 +9,10 @@ final class LiquidityPoolRemoveLiquidityRouter: LiquidityPoolRemoveLiquidityRout wallet: MetaAccountModel, liquidityPair: LiquidityPair, info: RemoveLiquidityInfo, + flowClosure: @escaping () -> Void, from view: ControllerBackedProtocol? ) { - guard let module = LiquidityPoolRemoveLiquidityConfirmAssembly.configureModule(wallet: wallet, chain: chain, liquidityPair: liquidityPair, removeInfo: info) else { + guard let module = LiquidityPoolRemoveLiquidityConfirmAssembly.configureModule(wallet: wallet, chain: chain, liquidityPair: liquidityPair, removeInfo: info, flowClosure: flowClosure) else { return } @@ -33,11 +34,9 @@ final class LiquidityPoolRemoveLiquidityRouter: LiquidityPoolRemoveLiquidityRout ) controller?.modalTransitioningFactory = factory - view?.controller.navigationController?.dismiss(animated: true) { - if let presenter = presenter as? ControllerBackedProtocol, - let controller = controller { - presenter.controller.present(controller, animated: true) - } + view?.controller.navigationController?.popToRootViewController(animated: true) + if let controller = controller { + view?.controller.navigationController?.present(controller, animated: true) } } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityViewController.swift index 4eceeae0ef..5ac1be2cfc 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityViewController.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidity/LiquidityPoolRemoveLiquidityViewController.swift @@ -93,7 +93,7 @@ final class LiquidityPoolRemoveLiquidityViewController: UIViewController, ViewHo rootView.swapToInputView.textField.isUserInteractionEnabled = false let locale = localizationManager?.selectedLocale ?? Locale.current - let accessoryView = UIFactory.default.createAmountAccessoryView(for: self, locale: locale) + let accessoryView = UIFactory.default.createDoneAccessoryView(for: self, locale: locale) rootView.swapFromInputView.textField.inputAccessoryView = accessoryView rootView.swapToInputView.textField.inputAccessoryView = accessoryView diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmAssembly.swift b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmAssembly.swift index f8f884bd1b..db8cbda095 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmAssembly.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolRemoveLiquidityConfirm/LiquidityPoolRemoveLiquidityConfirmAssembly.swift @@ -10,7 +10,8 @@ final class LiquidityPoolRemoveLiquidityConfirmAssembly { wallet: MetaAccountModel, chain: ChainModel, liquidityPair: LiquidityPair, - removeInfo: RemoveLiquidityInfo + removeInfo: RemoveLiquidityInfo, + flowClosure: @escaping () -> Void ) -> LiquidityPoolRemoveLiquidityConfirmModuleCreationResult? { guard let response = wallet.fetch(for: chain.accountRequest()) else { return nil @@ -56,7 +57,8 @@ final class LiquidityPoolRemoveLiquidityConfirmAssembly { liquidityPair: liquidityPair, dataValidatingFactory: dataValidatingFactory, confirmViewModelFactory: LiquidityPoolSupplyConfirmViewModelFactoryDefault(), - removeInfo: removeInfo + removeInfo: removeInfo, + flowClosure: flowClosure ) let view = LiquidityPoolRemoveLiquidityConfirmViewController( diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyAssembly.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyAssembly.swift index a2b2686fe0..a83c1f10b6 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyAssembly.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyAssembly.swift @@ -6,7 +6,7 @@ import SSFModels import SoraKeystore final class LiquidityPoolSupplyAssembly { - static func configureModule(chain: ChainModel, wallet: MetaAccountModel, liquidityPair: LiquidityPair, availablePairs: [LiquidityPair]?) -> LiquidityPoolSupplyModuleCreationResult? { + static func configureModule(chain: ChainModel, wallet: MetaAccountModel, liquidityPair: LiquidityPair, availablePairs: [LiquidityPair]?, flowClosure: @escaping () -> Void) -> LiquidityPoolSupplyModuleCreationResult? { guard let response = wallet.fetch(for: chain.accountRequest()) else { return nil } @@ -51,7 +51,8 @@ final class LiquidityPoolSupplyAssembly { wallet: wallet, dataValidatingFactory: dataValidatingFactory, viewModelFactory: LiquidityPoolSupplyViewModelFactoryDefault(), - availablePairs: availablePairs + availablePairs: availablePairs, + flowClosure: flowClosure ) let view = LiquidityPoolSupplyViewController( diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift index 5274af5ec2..d0f252ad2e 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyPresenter.swift @@ -55,6 +55,7 @@ final class LiquidityPoolSupplyPresenter { private let liquidityPair: LiquidityPair private let chain: ChainModel private let viewModelFactory: LiquidityPoolSupplyViewModelFactory + private var flowClosure: () -> Void private let wallet: MetaAccountModel private var swapFromChainAsset: ChainAsset? @@ -110,7 +111,8 @@ final class LiquidityPoolSupplyPresenter { wallet: MetaAccountModel, dataValidatingFactory: SendDataValidatingFactory, viewModelFactory: LiquidityPoolSupplyViewModelFactory, - availablePairs: [LiquidityPair]? + availablePairs: [LiquidityPair]?, + flowClosure: @escaping () -> Void ) { self.interactor = interactor self.router = router @@ -122,6 +124,7 @@ final class LiquidityPoolSupplyPresenter { self.viewModelFactory = viewModelFactory pairs = availablePairs dexId = liquidityPair.dexId + self.flowClosure = flowClosure self.localizationManager = localizationManager } @@ -381,6 +384,7 @@ extension LiquidityPoolSupplyPresenter: LiquidityPoolSupplyViewOutput { wallet: self.wallet, liquidityPair: self.liquidityPair, inputData: inputData, + flowClosure: flowClosure, from: self.view ) } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyProtocols.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyProtocols.swift index c5e9bff54d..57f2005b40 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyProtocols.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyProtocols.swift @@ -21,6 +21,7 @@ protocol LiquidityPoolSupplyRouterInput: AnyObject, AnyDismissable, SheetAlertPr wallet: MetaAccountModel, liquidityPair: LiquidityPair, inputData: LiquidityPoolSupplyConfirmInputData, + flowClosure: @escaping () -> Void, from view: ControllerBackedProtocol? ) } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyRouter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyRouter.swift index 425ff95c68..7a7e92c469 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyRouter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupply/LiquidityPoolSupplyRouter.swift @@ -31,9 +31,16 @@ final class LiquidityPoolSupplyRouter: LiquidityPoolSupplyRouterInput { wallet: MetaAccountModel, liquidityPair: LiquidityPair, inputData: LiquidityPoolSupplyConfirmInputData, + flowClosure: @escaping () -> Void, from view: ControllerBackedProtocol? ) { - guard let module = LiquidityPoolSupplyConfirmAssembly.configureModule(chain: chain, wallet: wallet, liquidityPair: liquidityPair, inputData: inputData) else { + guard let module = LiquidityPoolSupplyConfirmAssembly.configureModule( + chain: chain, + wallet: wallet, + liquidityPair: liquidityPair, + inputData: inputData, + flowClosure: flowClosure + ) else { return } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmAssembly.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmAssembly.swift index c6bc18b9af..351c0fc175 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmAssembly.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmAssembly.swift @@ -17,7 +17,8 @@ enum LiquidityPoolSupplyConfirmAssembly { chain: ChainModel, wallet: MetaAccountModel, liquidityPair: LiquidityPair, - inputData: LiquidityPoolSupplyConfirmInputData + inputData: LiquidityPoolSupplyConfirmInputData, + flowClosure: @escaping () -> Void ) -> LiquidityPoolSupplyConfirmModuleCreationResult? { guard let response = wallet.fetch(for: chain.accountRequest()) else { return nil @@ -70,7 +71,8 @@ enum LiquidityPoolSupplyConfirmAssembly { chain: chain, inputData: inputData, wallet: wallet, - viewModelFactory: LiquidityPoolSupplyConfirmViewModelFactoryDefault() + viewModelFactory: LiquidityPoolSupplyConfirmViewModelFactoryDefault(), + flowClosure: flowClosure ) let view = LiquidityPoolSupplyConfirmViewController( diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmPresenter.swift index a1310746af..8d158cb73c 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolSupplyConfirm/LiquidityPoolSupplyConfirmPresenter.swift @@ -42,6 +42,7 @@ final class LiquidityPoolSupplyConfirmPresenter { private let chain: ChainModel private let inputData: LiquidityPoolSupplyConfirmInputData private let viewModelFactory: LiquidityPoolSupplyConfirmViewModelFactory + private var flowClosure: () -> Void private var apyInfo: PoolApyInfo? private let wallet: MetaAccountModel @@ -76,7 +77,8 @@ final class LiquidityPoolSupplyConfirmPresenter { chain: ChainModel, inputData: LiquidityPoolSupplyConfirmInputData, wallet: MetaAccountModel, - viewModelFactory: LiquidityPoolSupplyConfirmViewModelFactory + viewModelFactory: LiquidityPoolSupplyConfirmViewModelFactory, + flowClosure: @escaping () -> Void ) { self.interactor = interactor self.router = router @@ -89,6 +91,7 @@ final class LiquidityPoolSupplyConfirmPresenter { self.viewModelFactory = viewModelFactory dexId = liquidityPair.dexId availablePairs = inputData.availablePools + self.flowClosure = flowClosure self.localizationManager = localizationManager } @@ -376,6 +379,7 @@ extension LiquidityPoolSupplyConfirmPresenter: LiquidityPoolSupplyConfirmInterac } func didReceiveTransactionHash(_ hash: String) { + flowClosure() resetLoadingState() guard let utilityChainAsset = chain.utilityChainAssets().first else { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListPresenter.swift index 764e181c62..18573d47c8 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/AvailablePools/AvailableLiquidityPoolsListPresenter.swift @@ -92,7 +92,14 @@ extension AvailableLiquidityPoolsListPresenter: LiquidityPoolsListViewOutput { let assetIdPair = AssetIdPair(baseAssetIdCode: liquidityPair.baseAssetId, targetAssetIdCode: liquidityPair.targetAssetId) let input = LiquidityPoolDetailsInput.availablePool(liquidityPair: liquidityPair, reserves: reserves, apyInfo: apyInfo, availablePairs: pairs) - router.showPoolDetails(assetIdPair: assetIdPair, chain: chain, wallet: wallet, input: input, from: view) + router.showPoolDetails( + assetIdPair: assetIdPair, + chain: chain, + wallet: wallet, + input: input, + poolOperationFlowsClosure: { [weak self] in self?.moduleOutput?.didReceiveFlowClosureEvent() }, + from: view + ) } func didTapMoreButton() { @@ -113,6 +120,10 @@ extension AvailableLiquidityPoolsListPresenter: LiquidityPoolsListViewOutput { let viewModel = viewModelFactory.buildLoadingViewModel(type: type) view?.didReceive(viewModel: viewModel) } + + func handleRefreshControlEvent() { + interactor.fetchPools() + } } extension AvailableLiquidityPoolsListPresenter: AvailableLiquidityPoolsListInteractorOutput { @@ -158,6 +169,10 @@ extension AvailableLiquidityPoolsListPresenter: LiquidityPoolsListModuleInput { func resetTasks() { interactor.cancelTasks() } + + func refreshData() { + interactor.fetchPools() + } } extension AvailableLiquidityPoolsListPresenter: Localizable { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift index 17d46f17ef..1c01fa2f75 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListPresenter.swift @@ -98,7 +98,14 @@ extension UserLiquidityPoolsListPresenter: LiquidityPoolsListViewOutput { accountPool: accountPool, availablePairs: accountPools?.compactMap { $0.liquidityPair } ) - router.showPoolDetails(assetIdPair: assetIdPair, chain: chain, wallet: wallet, input: input, from: view) + router.showPoolDetails( + assetIdPair: assetIdPair, + chain: chain, + wallet: wallet, + input: input, + poolOperationFlowsClosure: { [weak self] in self?.moduleOutput?.didReceiveFlowClosureEvent() }, + from: view + ) } func didTapMoreButton() { @@ -114,6 +121,10 @@ extension UserLiquidityPoolsListPresenter: LiquidityPoolsListViewOutput { searchText = text provideViewModel() } + + func handleRefreshControlEvent() { + interactor.fetchPools() + } } extension UserLiquidityPoolsListPresenter: UserLiquidityPoolsListInteractorOutput { @@ -170,6 +181,10 @@ extension UserLiquidityPoolsListPresenter: LiquidityPoolsListModuleInput { func resetTasks() { interactor.cancelTasks() } + + func refreshData() { + interactor.fetchPools() + } } extension UserLiquidityPoolsListPresenter: Localizable { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListViewModelFactory.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListViewModelFactory.swift index d323f10b98..48b410144d 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListViewModelFactory.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/Flows/UserPools/UserLiquidityPoolsListViewModelFactory.swift @@ -76,19 +76,20 @@ final class UserLiquidityPoolsListViewModelFactoryDefault: UserLiquidityPoolsLis let reservesString = reservesValue.flatMap { fiatFormatter.stringFromDecimal($0) } let reservesLabelText: String? = reservesString.flatMap { "\($0) TVL" } - let baseAssetBalanceViewModelFactory = createBalanceViewModelFactory(for: ChainAsset(chain: chain, asset: baseAsset), wallet: wallet) - let targetAssetBalanceViewModelFactory = createBalanceViewModelFactory(for: ChainAsset(chain: chain, asset: targetAsset), wallet: wallet) - - let baseAssetViewModel = pair.baseAssetPooled.flatMap { - baseAssetBalanceViewModelFactory.balanceFromPrice($0, priceData: baseAssetPrice, usageCase: .listCryptoWith(minimumFractionDigits: 1, maximumFractionDigits: 3)) - } - - let targetAssetViewModel = pair.targetAssetPooled.flatMap { - targetAssetBalanceViewModelFactory.balanceFromPrice($0, priceData: targetAssetPrice, usageCase: .listCryptoWith(minimumFractionDigits: 1, maximumFractionDigits: 3)) - } + let numberFormatter = NumberFormatter.decimalFormatter(precision: 3, rounding: .floor) + let baseAssetPooledText = pair.baseAssetPooled + .flatMap { + numberFormatter.stringFromDecimal($0) + }.flatMap { + "\($0) \(targetAsset.symbol.uppercased())" + } - let baseAssetPooledText = baseAssetViewModel?.value(for: locale).amount - let targetAssetPooledText = targetAssetViewModel?.value(for: locale).amount + let targetAssetPooledText = pair.targetAssetPooled + .flatMap { + numberFormatter.stringFromDecimal($0) + }.flatMap { + "\($0) \(targetAsset.symbol.uppercased())" + } let stakingStatusLabelText: String? = baseAssetPooledText.flatMap { guard let targetAssetPooledText = targetAssetPooledText else { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListProtocols.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListProtocols.swift index 17076eac54..8c4f410947 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListProtocols.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListProtocols.swift @@ -15,6 +15,7 @@ protocol LiquidityPoolsListViewOutput: AnyObject { func didTapBackButton() func searchTextDidChanged(_ text: String?) func didAppearView() + func handleRefreshControlEvent() } protocol LiquidityPoolsListInteractorInput: AnyObject { @@ -28,12 +29,14 @@ protocol LiquidityPoolsListRouterInput: AnyObject, AnyDismissable { chain: ChainModel, wallet: MetaAccountModel, input: LiquidityPoolDetailsInput, + poolOperationFlowsClosure: @escaping () -> Void, from view: ControllerBackedProtocol? ) } protocol LiquidityPoolsListModuleInput: AnyObject { func resetTasks() + func refreshData() } protocol LiquidityPoolsListModuleOutput: AnyObject { @@ -41,4 +44,5 @@ protocol LiquidityPoolsListModuleOutput: AnyObject { func didTapMoreAvailablePools() func shouldShowUserPools(_ shouldShow: Bool) func didReceiveUserPoolCount(_ userPoolsCount: Int) + func didReceiveFlowClosureEvent() } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListRouter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListRouter.swift index 89d0005cf2..2a94b0f006 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListRouter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListRouter.swift @@ -3,8 +3,8 @@ import SSFModels import SSFPolkaswap final class LiquidityPoolsListRouter: LiquidityPoolsListRouterInput { - func showPoolDetails(assetIdPair: AssetIdPair, chain: ChainModel, wallet: MetaAccountModel, input: LiquidityPoolDetailsInput, from view: ControllerBackedProtocol?) { - let module = LiquidityPoolDetailsAssembly.configureModule(assetIdPair: assetIdPair, chain: chain, wallet: wallet, input: input) + func showPoolDetails(assetIdPair: AssetIdPair, chain: ChainModel, wallet: MetaAccountModel, input: LiquidityPoolDetailsInput, poolOperationFlowsClosure: @escaping () -> Void, from view: ControllerBackedProtocol?) { + let module = LiquidityPoolDetailsAssembly.configureModule(assetIdPair: assetIdPair, chain: chain, wallet: wallet, input: input, poolOperationFlowsClosure: poolOperationFlowsClosure) guard let viewController = module?.view.controller else { return diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift index d21b34b133..10d6cbd4b0 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewController.swift @@ -11,6 +11,7 @@ final class LiquidityPoolsListViewController: UIViewController, ViewHolder, Hidd private var cellModels: [LiquidityPoolListCellModel]? private let output: LiquidityPoolsListViewOutput + private var refreshControl = UIRefreshControl() private var viewLoadingFinished: Bool = false @@ -54,6 +55,9 @@ final class LiquidityPoolsListViewController: UIViewController, ViewHolder, Hidd bindSearchTextView() addEndEditingTapGesture(for: rootView) + + rootView.tableView.addSubview(refreshControl) + refreshControl.addTarget(self, action: #selector(handleRefreshControlEvent), for: .valueChanged) } override func viewWillAppear(_ animated: Bool) { @@ -84,6 +88,10 @@ final class LiquidityPoolsListViewController: UIViewController, ViewHolder, Hidd self?.output.searchTextDidChanged(text) } } + + @objc private func handleRefreshControlEvent() { + output.handleRefreshControlEvent() + } } extension LiquidityPoolsListViewController: KeyboardViewAdoptable { @@ -130,7 +138,8 @@ extension LiquidityPoolsListViewController: UITableViewDataSource, UITableViewDe extension LiquidityPoolsListViewController: LiquidityPoolsListViewInput { func didReceive(viewModel: LiquidityPoolListViewModel) { - print("didreceive viewmodels: ", viewModel.poolViewModels) + refreshControl.endRefreshing() + cellModels = viewModel.poolViewModels rootView.bind(viewModel: viewModel) diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift index f089a1176d..f446dafeac 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsList/LiquidityPoolsListViewLayout.swift @@ -169,7 +169,7 @@ final class LiquidityPoolsListViewLayout: UIView { backButton.snp.makeConstraints { make in make.size.equalTo(32) - make.leading.equalToSuperview().inset(16) + make.leading.equalToSuperview() make.centerY.equalToSuperview() } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewPresenter.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewPresenter.swift index 520b86ef84..c53f74bfa8 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewPresenter.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewPresenter.swift @@ -48,6 +48,11 @@ extension LiquidityPoolsOverviewPresenter: LiquidityPoolsOverviewViewOutput { availablePoolsInput?.resetTasks() userPoolsInput?.resetTasks() } + + func handleRefreshControlEvent() { + availablePoolsInput?.refreshData() + userPoolsInput?.refreshData() + } } // MARK: - LiquidityPoolsOverviewInteractorOutput @@ -78,4 +83,13 @@ extension LiquidityPoolsOverviewPresenter: LiquidityPoolsListModuleOutput { func didReceiveUserPoolCount(_ userPoolsCount: Int) { view?.didReceiveUserPoolsCount(count: userPoolsCount) } + + func didReceiveFlowClosureEvent() { + // Temporary until subscription will be implemented + let soraTargetBlockTime = 6.0 + DispatchQueue.global().asyncAfter(deadline: .now() + soraTargetBlockTime * 1.5) { [weak self] in + self?.availablePoolsInput?.refreshData() + self?.userPoolsInput?.refreshData() + } + } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewProtocols.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewProtocols.swift index 74e0767e5e..bf8544e5c0 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewProtocols.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewProtocols.swift @@ -10,6 +10,7 @@ protocol LiquidityPoolsOverviewViewInput: ControllerBackedProtocol { protocol LiquidityPoolsOverviewViewOutput: AnyObject { func didLoad(view: LiquidityPoolsOverviewViewInput) func backButtonClicked() + func handleRefreshControlEvent() } protocol LiquidityPoolsOverviewInteractorInput: AnyObject { diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewController.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewController.swift index 8ef11feac7..23dcf8b9d5 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewController.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewController.swift @@ -10,6 +10,7 @@ final class LiquidityPoolsOverviewViewController: UIViewController, ViewHolder, private let userPoolsViewController: UIViewController private let availablePoolsViewController: UIViewController + private var refreshControl = UIRefreshControl() // MARK: - Constructor @@ -23,6 +24,7 @@ final class LiquidityPoolsOverviewViewController: UIViewController, ViewHolder, self.userPoolsViewController = userPoolsViewController self.availablePoolsViewController = availablePoolsViewController super.init(nibName: nil, bundle: nil) + isModalInPresentation = true self.localizationManager = localizationManager } @@ -47,10 +49,16 @@ final class LiquidityPoolsOverviewViewController: UIViewController, ViewHolder, setupEmbededUserPoolsView() setupEmbededAvailablePoolsView() + refreshControl.addTarget(self, action: #selector(handleRefreshControlEvent), for: .valueChanged) + rootView.scrollView.refreshControl = refreshControl } // MARK: - Private methods + @objc private func handleRefreshControlEvent() { + output.handleRefreshControlEvent() + } + private func setupEmbededUserPoolsView() { addChild(userPoolsViewController) @@ -82,6 +90,7 @@ extension LiquidityPoolsOverviewViewController: LiquidityPoolsOverviewViewInput } func didReceiveUserPoolsCount(count: Int) { + refreshControl.endRefreshing() rootView.bind(userPoolsCount: count) } } diff --git a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewLayout.swift b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewLayout.swift index 5a3c0f0959..1a8430ff1d 100644 --- a/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewLayout.swift +++ b/fearless/Modules/LiquidityPools/LiquidityPoolsOverview/LiquidityPoolsOverviewViewLayout.swift @@ -96,14 +96,24 @@ final class LiquidityPoolsOverviewViewLayout: UIView { private func drawSubviews() { addSubview(backgroundImageView) addSubview(navigationBar) - addSubview(containerStackView) - containerStackView.addArrangedSubview(userPoolsContainerView) - containerStackView.addArrangedSubview(availablePoolsContainerView) + addSubview(scrollView) + scrollView.addSubview(userPoolsContainerView) + scrollView.addSubview(availablePoolsContainerView) + +// scrollView.addSubview(containerStackView) +// containerStackView.addArrangedSubview(userPoolsContainerView) +// containerStackView.addArrangedSubview(availablePoolsContainerView) navigationBar.setLeftViews([navigationBar.backButton, polkaswapImageView]) } private func setupConstraints() { + scrollView.snp.makeConstraints { make in + make.top.equalTo(navigationBar.snp.bottom) + make.bottom.equalToSuperview().inset(24) + make.width.equalTo(self) + make.centerX.equalToSuperview() + } backgroundImageView.snp.makeConstraints { make in make.edges.equalToSuperview() } @@ -112,21 +122,24 @@ final class LiquidityPoolsOverviewViewLayout: UIView { make.leading.trailing.top.equalToSuperview() } - containerStackView.snp.makeConstraints { make in - make.top.equalTo(navigationBar.snp.bottom) - make.bottom.lessThanOrEqualToSuperview() - make.width.equalTo(self) - make.centerX.equalToSuperview() - } +// containerStackView.snp.makeConstraints { make in +// make.top.equalTo(navigationBar.snp.bottom) +// make.bottom.lessThanOrEqualTo(self) +// make.width.equalTo(self) +// make.centerX.equalToSuperview() +// } userPoolsContainerView.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview().inset(16) + make.top.equalToSuperview() + make.leading.trailing.equalTo(self).inset(16) userPoolsHeightConstraint = make.height.equalTo(0).constraint } availablePoolsContainerView.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview().inset(16) + make.top.equalTo(userPoolsContainerView.snp.bottom).offset(16) + make.leading.trailing.equalTo(self).inset(16) availablePoolsHeightConstraint = make.height.equalTo(0).constraint + make.bottom.equalToSuperview() } updateConstraints(with: 0) From a9ba4709d4f16da03926b10ac4e24b198595241a Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Fri, 28 Jun 2024 15:08:43 +0500 Subject: [PATCH 059/115] [#FLW-4718] Reconnect socket after connection lost. --- fearless.xcodeproj/project.pbxproj | 2 +- fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index 236d1a47f2..82d3024d07 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -19300,7 +19300,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/soramitsu/shared-features-spm.git"; requirement = { - branch = "fearless-wallet"; + branch = webSocketEngine; kind = branch; }; }; diff --git a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved index ec2082f6d1..9d850ab614 100644 --- a/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/fearless.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -132,8 +132,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/soramitsu/shared-features-spm.git", "state" : { - "branch" : "fearless-wallet", - "revision" : "dd49df35001f0dd411e6f0e289311a10b19b86c2" + "branch" : "webSocketEngine", + "revision" : "68efafcb9ef72c9cc4cddde20897b5c85745e574" } }, { From b416009ed5ceb0eacf6fb9e3c38f06bd5048c233 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Fri, 28 Jun 2024 16:38:00 +0500 Subject: [PATCH 060/115] [#FLW-4686] Pool Staking, Wrong data --- .../ViewModel/StakingPoolStartViewModelFactory.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fearless/Modules/StakingPool/Join/StakingPoolStart/ViewModel/StakingPoolStartViewModelFactory.swift b/fearless/Modules/StakingPool/Join/StakingPoolStart/ViewModel/StakingPoolStartViewModelFactory.swift index 1ed39e2f3d..f398fa18af 100644 --- a/fearless/Modules/StakingPool/Join/StakingPoolStart/ViewModel/StakingPoolStartViewModelFactory.swift +++ b/fearless/Modules/StakingPool/Join/StakingPoolStart/ViewModel/StakingPoolStartViewModelFactory.swift @@ -86,7 +86,9 @@ final class StakingPoolStartViewModelFactory { return nil } - guard let percentString = NumberFormatter.percent.stringFromDecimal(apr) else { + let formatter = NumberFormatter.percent + formatter.locale = locale + guard let percentString = formatter.stringFromDecimal(apr) else { return nil } From 68b3b6c9390d059bddf4d81aa8592b305976203d Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Fri, 28 Jun 2024 17:37:28 +0500 Subject: [PATCH 061/115] [#FLW-4725] Liberland network. There is not possible to past "Send to" address --- fearless/Modules/CrossChain/CrossChainPresenter.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fearless/Modules/CrossChain/CrossChainPresenter.swift b/fearless/Modules/CrossChain/CrossChainPresenter.swift index d385ffc8fb..03d5acda06 100644 --- a/fearless/Modules/CrossChain/CrossChainPresenter.swift +++ b/fearless/Modules/CrossChain/CrossChainPresenter.swift @@ -804,17 +804,19 @@ extension CrossChainPresenter: ContactsModuleOutput { extension CrossChainPresenter: WalletsManagmentModuleOutput { func selectedWallet(_ wallet: MetaAccountModel, for _: Int) { + destWallet = wallet guard let chain = selectedDestChainModel, let accountId = wallet.fetch(for: chain.accountRequest())?.accountId, let address = try? AddressFactory.address(for: accountId, chain: chain) else { + let viewModel = viewModelFactory.buildRecipientViewModel(address: wallet.name) + view?.didReceive(recipientViewModel: viewModel) return } let viewModel = viewModelFactory.buildRecipientViewModel(address: address) view?.didReceive(recipientViewModel: viewModel) - destWallet = wallet recipientAddress = address loadingCollector.addressExists = true checkLoadingState() From 2e89fda4cf0662569dc0f4d0770c1e62b2bda5fd Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Mon, 1 Jul 2024 10:15:00 +0500 Subject: [PATCH 062/115] [#FLW-4716] Klaytn chain issue: fees aren't calculated --- .../Tokens/EthereumTransferService.swift | 9 +++++++- .../Common/Helpers/EthereumNodeFetching.swift | 12 +++++------ fearless/Modules/Send/SendInteractor.swift | 21 ------------------- 3 files changed, 13 insertions(+), 29 deletions(-) diff --git a/fearless/ApplicationLayer/Services/Transfer/Tokens/EthereumTransferService.swift b/fearless/ApplicationLayer/Services/Transfer/Tokens/EthereumTransferService.swift index 1eaf14dd2c..b3df8682ab 100644 --- a/fearless/ApplicationLayer/Services/Transfer/Tokens/EthereumTransferService.swift +++ b/fearless/ApplicationLayer/Services/Transfer/Tokens/EthereumTransferService.swift @@ -107,7 +107,14 @@ final class EthereumTransferService: BaseEthereumService, TransferServiceProtoco try subscribe() } } catch { - listener.didReceiveFeeError(feeError: error) + Task { + do { + let fee = try await estimateFee(for: transfer) + listener.didReceiveFee(fee: fee) + } catch { + listener.didReceiveFeeError(feeError: error) + } + } } } diff --git a/fearless/Common/Helpers/EthereumNodeFetching.swift b/fearless/Common/Helpers/EthereumNodeFetching.swift index ca890796cd..c6c6499cc7 100644 --- a/fearless/Common/Helpers/EthereumNodeFetching.swift +++ b/fearless/Common/Helpers/EthereumNodeFetching.swift @@ -99,21 +99,19 @@ enum EthereumChain: String { final class EthereumNodeFetching { func getNode(for chain: ChainModel) throws -> Web3.Eth { - guard let ethereumChain = EthereumChain(rawValue: chain.chainId) else { - return try getHttps(for: chain) - } - let randomWssNode = chain.nodes.filter { $0.url.absoluteString.contains("wss") }.randomElement() let hasSelectedWssNode = chain.selectedNode?.url.absoluteString.contains("wss") == true let node = hasSelectedWssNode ? chain.selectedNode : randomWssNode - guard let wssURL = node?.url else { + guard var wssURL = node?.url else { return try getHttps(for: chain) } - let finalURL = ethereumChain.apiKeyInjectedURL(baseURL: wssURL) + if let ethereumChain = EthereumChain(rawValue: chain.chainId) { + wssURL = ethereumChain.apiKeyInjectedURL(baseURL: wssURL) + } - let provider = try Web3WebSocketProvider(wsUrl: finalURL.absoluteString, timeout: .seconds(10)) + let provider = try Web3WebSocketProvider(wsUrl: wssURL.absoluteString, timeout: .seconds(10)) let web3 = Web3(provider: provider, rpcId: Int(chain.chainId) ?? 1) return web3.eth } diff --git a/fearless/Modules/Send/SendInteractor.swift b/fearless/Modules/Send/SendInteractor.swift index 36ffc5bb54..e67d72a75c 100644 --- a/fearless/Modules/Send/SendInteractor.swift +++ b/fearless/Modules/Send/SendInteractor.swift @@ -25,7 +25,6 @@ final class SendInteractor: RuntimeConstantFetching { private var subscriptionId: UInt16? private var dependencies: SendDependencies? - private var runtimeItemByChainId: [ChainModel.Id: RuntimeMetadataItem] = [:] init( accountInfoSubscriptionAdapter: AccountInfoSubscriptionAdapterProtocol, @@ -77,26 +76,6 @@ final class SendInteractor: RuntimeConstantFetching { } } - private func fetchCurrentRuntimeItem(currentChainAsset: ChainAsset) async throws -> RuntimeMetadataItem? { - if let item = runtimeItemByChainId[currentChainAsset.chain.chainId] { - return item - } - - let currentChainId = currentChainAsset.chain.chainId - let items = try await runtimeItemRepository.fetchAll() - cache(runtimeItems: items) - let currentRuntimeItem = items.first(where: { $0.chain == currentChainId }) - return currentRuntimeItem - } - - private func cache(runtimeItems: [RuntimeMetadataItem]) { - runtimeItemByChainId = runtimeItems.reduce([ChainModel.Id: RuntimeMetadataItem]()) { partialResult, currentItem in - var result = partialResult - result[currentItem.chain] = currentItem - return result - } - } - private func updateDependencies(for chainAsset: ChainAsset) { Task { let dependencies = try await dependencyContainer.prepareDepencies(chainAsset: chainAsset) From 9fc7337cf9f78c81762be1865bc82537a26ea379 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Mon, 1 Jul 2024 10:57:48 +0500 Subject: [PATCH 063/115] [#FLW-4586] On the main screen we show wrong balance --- .../WalletBalanceBuilder.swift | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/fearless/ApplicationLayer/Services/Balance/WalletBalanceSubscription/WalletBalanceBuilder.swift b/fearless/ApplicationLayer/Services/Balance/WalletBalanceSubscription/WalletBalanceBuilder.swift index 60d20b37dd..e733791f46 100644 --- a/fearless/ApplicationLayer/Services/Balance/WalletBalanceSubscription/WalletBalanceBuilder.swift +++ b/fearless/ApplicationLayer/Services/Balance/WalletBalanceSubscription/WalletBalanceBuilder.swift @@ -10,7 +10,7 @@ protocol WalletBalanceBuilderProtocol { ) -> [MetaAccountId: WalletBalanceInfo]? } -final class WalletBalanceBuilder: WalletBalanceBuilderProtocol { +final class WalletBalanceBuilder: WalletBalanceBuilderProtocol, ChainAssetListBuilder { func buildBalance( for accountInfos: [ChainAssetKey: AccountInfo?], _ metaAccounts: [MetaAccountModel], @@ -71,7 +71,14 @@ final class WalletBalanceBuilder: WalletBalanceBuilderProtocol { var totalDayChange: Decimal = .zero var enabledAccountInfos: [ChainAssetKey: AccountInfo?] = [:] - chainAssets.forEach { chainAsset in + let selectedChainAssets = filterChainAssets( + with: NetworkManagmentFilter(identifier: metaAccount.networkManagmentFilter), + chainAssets: chainAssets, + wallet: metaAccount, + search: nil + ) + + selectedChainAssets.forEach { chainAsset in let accountRequest = chainAsset.chain.accountRequest() guard let accountId = metaAccount.fetch(for: accountRequest)?.accountId else { accountInfosCount += 1 @@ -96,7 +103,7 @@ final class WalletBalanceBuilder: WalletBalanceBuilderProtocol { totalDayChange += balance.dayChange } - let isLoaded = accountInfosCount == chainAssets.count + let isLoaded = accountInfosCount == selectedChainAssets.count return CountBalanceInfo( totalBalance: totalBalance, totalDayChange: totalDayChange, From 3b9d82d7190883d2fb4bf7a86ce3c575e9736852 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Mon, 1 Jul 2024 11:03:39 +0500 Subject: [PATCH 064/115] [#FLW-4694] XCM. There is loader on the button after entering the amount --- fearless/Modules/CrossChain/CrossChainPresenter.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/fearless/Modules/CrossChain/CrossChainPresenter.swift b/fearless/Modules/CrossChain/CrossChainPresenter.swift index 03d5acda06..36e0d788d7 100644 --- a/fearless/Modules/CrossChain/CrossChainPresenter.swift +++ b/fearless/Modules/CrossChain/CrossChainPresenter.swift @@ -488,7 +488,6 @@ final class CrossChainPresenter { extension CrossChainPresenter: CrossChainViewOutput { func selectAmountPercentage(_ percentage: Float) { loadingCollector.originFeeReady = false - view?.setButtonLoadingState(isLoading: true) amountInputResult = .rate(Decimal(Double(percentage))) provideAssetViewModel() provideInputViewModel() @@ -497,7 +496,6 @@ extension CrossChainPresenter: CrossChainViewOutput { func updateAmount(_ newValue: Decimal) { loadingCollector.originFeeReady = false - view?.setButtonLoadingState(isLoading: true) amountInputResult = .absolute(newValue) provideAssetViewModel() estimateFee() From 2ba5c6f5690525336af1e0b950a375818a13cf63 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Mon, 1 Jul 2024 11:14:00 +0500 Subject: [PATCH 065/115] [#FLW-4672] Subscan and Share buttons should be upper --- .../SwapTransactionDetailViewLayout.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fearless/Modules/SwapTransactionDetail/SwapTransactionDetailViewLayout.swift b/fearless/Modules/SwapTransactionDetail/SwapTransactionDetailViewLayout.swift index 134dfb65fd..abe79778b1 100644 --- a/fearless/Modules/SwapTransactionDetail/SwapTransactionDetailViewLayout.swift +++ b/fearless/Modules/SwapTransactionDetail/SwapTransactionDetailViewLayout.swift @@ -242,13 +242,13 @@ final class SwapTransactionDetailViewLayout: UIView { subscanButton.snp.makeConstraints { make in make.leading.equalToSuperview().inset(UIConstants.bigOffset) - make.bottom.equalToSuperview().inset(UIConstants.bigOffset) + make.bottom.equalTo(safeAreaLayoutGuide) make.height.equalTo(UIConstants.actionHeight) } shareButton.snp.makeConstraints { make in make.trailing.equalToSuperview().inset(UIConstants.bigOffset) - make.bottom.equalToSuperview().inset(UIConstants.bigOffset) + make.bottom.equalTo(safeAreaLayoutGuide) make.height.equalTo(UIConstants.actionHeight) make.width.equalTo(subscanButton.snp.width) make.leading.equalTo(subscanButton.snp.trailing).offset(UIConstants.bigOffset) From aa62c36bb46c50eff470350a56b1cf56779cd2c9 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Mon, 1 Jul 2024 11:49:15 +0500 Subject: [PATCH 066/115] [#FLW-4671] Wallet backup banner does not disappear --- .../Modules/Banners/BannersAssembly.swift | 6 +--- .../Modules/Banners/BannersInteractor.swift | 35 +++++++------------ 2 files changed, 13 insertions(+), 28 deletions(-) diff --git a/fearless/Modules/Banners/BannersAssembly.swift b/fearless/Modules/Banners/BannersAssembly.swift index 4cda660b7f..459c2e738a 100644 --- a/fearless/Modules/Banners/BannersAssembly.swift +++ b/fearless/Modules/Banners/BannersAssembly.swift @@ -13,13 +13,9 @@ final class BannersAssembly { mapper: AnyCoreDataMapper(ManagedMetaAccountMapper()) ) - let accountRepositoryFactory = AccountRepositoryFactory(storageFacade: UserDataStorageFacade.shared) - let accountRepository = accountRepositoryFactory.createMetaAccountRepository(for: nil, sortDescriptors: []) - let interactor = BannersInteractor( walletProvider: walletProvider, - repository: accountRepository, - operationQueue: OperationQueue() + eventCenter: EventCenter.shared ) let router = BannersRouter() diff --git a/fearless/Modules/Banners/BannersInteractor.swift b/fearless/Modules/Banners/BannersInteractor.swift index 5355fd7a85..d981b9121e 100644 --- a/fearless/Modules/Banners/BannersInteractor.swift +++ b/fearless/Modules/Banners/BannersInteractor.swift @@ -12,17 +12,14 @@ final class BannersInteractor { private weak var output: BannersInteractorOutput? private let walletProvider: StreamableProvider - private let repository: AnyDataProviderRepository - private let operationQueue: OperationQueue + private let eventCenter: EventCenterProtocol init( walletProvider: StreamableProvider, - repository: AnyDataProviderRepository, - operationQueue: OperationQueue + eventCenter: EventCenterProtocol ) { self.walletProvider = walletProvider - self.repository = repository - self.operationQueue = operationQueue + self.eventCenter = eventCenter } // MARK: - Private methods @@ -64,25 +61,17 @@ extension BannersInteractor: BannersInteractorInput { func markWalletAsBackedUp(_ wallet: MetaAccountModel) { let updatedWallet = wallet.replacingIsBackuped(true) - let operation = repository.saveOperation { - [updatedWallet] - } _: { - [] - } - - operation.completionBlock = { - SelectedWalletSettings.shared.performSave(value: updatedWallet) { [weak self] result in - DispatchQueue.main.async { - switch result { - case let .success(account): - self?.output?.didReceive(wallet: account) - case let .failure(error): - self?.output?.didReceive(error: error) - } + SelectedWalletSettings.shared.performSave(value: updatedWallet) { [weak self] result in + DispatchQueue.main.async { + switch result { + case let .success(account): + self?.output?.didReceive(wallet: account) + let event = MetaAccountModelChangedEvent(account: account) + self?.eventCenter.notify(with: event) + case let .failure(error): + self?.output?.didReceive(error: error) } } } - - operationQueue.addOperation(operation) } } From aef9b1757f406c4218d3d06854ea91bb3c319b4c Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Mon, 1 Jul 2024 12:59:41 +0500 Subject: [PATCH 067/115] [#FLW-4586] On the main screen we show wrong balance --- .../WalletBalanceBuilder.swift | 13 ++---- .../WalletBalanceSubscriptionAdapter.swift | 44 ++++++++++++++++++- .../BalanceInfo/BalanceInfoAssembly.swift | 3 ++ .../BalanceInfo/BalanceInfoInteractor.swift | 7 +++ .../BalanceInfo/BalanceInfoPresenter.swift | 4 ++ .../BalanceInfoViewModelFactory.swift | 11 +++++ .../WalletMainContainerAssembly.swift | 2 +- .../WalletMainContainerPresenter.swift | 2 +- 8 files changed, 73 insertions(+), 13 deletions(-) diff --git a/fearless/ApplicationLayer/Services/Balance/WalletBalanceSubscription/WalletBalanceBuilder.swift b/fearless/ApplicationLayer/Services/Balance/WalletBalanceSubscription/WalletBalanceBuilder.swift index e733791f46..60d20b37dd 100644 --- a/fearless/ApplicationLayer/Services/Balance/WalletBalanceSubscription/WalletBalanceBuilder.swift +++ b/fearless/ApplicationLayer/Services/Balance/WalletBalanceSubscription/WalletBalanceBuilder.swift @@ -10,7 +10,7 @@ protocol WalletBalanceBuilderProtocol { ) -> [MetaAccountId: WalletBalanceInfo]? } -final class WalletBalanceBuilder: WalletBalanceBuilderProtocol, ChainAssetListBuilder { +final class WalletBalanceBuilder: WalletBalanceBuilderProtocol { func buildBalance( for accountInfos: [ChainAssetKey: AccountInfo?], _ metaAccounts: [MetaAccountModel], @@ -71,14 +71,7 @@ final class WalletBalanceBuilder: WalletBalanceBuilderProtocol, ChainAssetListBu var totalDayChange: Decimal = .zero var enabledAccountInfos: [ChainAssetKey: AccountInfo?] = [:] - let selectedChainAssets = filterChainAssets( - with: NetworkManagmentFilter(identifier: metaAccount.networkManagmentFilter), - chainAssets: chainAssets, - wallet: metaAccount, - search: nil - ) - - selectedChainAssets.forEach { chainAsset in + chainAssets.forEach { chainAsset in let accountRequest = chainAsset.chain.accountRequest() guard let accountId = metaAccount.fetch(for: accountRequest)?.accountId else { accountInfosCount += 1 @@ -103,7 +96,7 @@ final class WalletBalanceBuilder: WalletBalanceBuilderProtocol, ChainAssetListBu totalDayChange += balance.dayChange } - let isLoaded = accountInfosCount == selectedChainAssets.count + let isLoaded = accountInfosCount == chainAssets.count return CountBalanceInfo( totalBalance: totalBalance, totalDayChange: totalDayChange, diff --git a/fearless/ApplicationLayer/Services/Balance/WalletBalanceSubscription/WalletBalanceSubscriptionAdapter.swift b/fearless/ApplicationLayer/Services/Balance/WalletBalanceSubscription/WalletBalanceSubscriptionAdapter.swift index 2754671f37..2554b2f682 100644 --- a/fearless/ApplicationLayer/Services/Balance/WalletBalanceSubscription/WalletBalanceSubscriptionAdapter.swift +++ b/fearless/ApplicationLayer/Services/Balance/WalletBalanceSubscription/WalletBalanceSubscriptionAdapter.swift @@ -43,6 +43,11 @@ protocol WalletBalanceSubscriptionAdapterProtocol { listener: WalletBalanceSubscriptionListener ) + func subscribeNetworkManagementBalance( + wallet: MetaAccountModel, + listener: WalletBalanceSubscriptionListener + ) + func unsubscribe(listener: WalletBalanceSubscriptionListener) } @@ -51,9 +56,10 @@ enum WalletBalanceListenerType { case wallet(wallet: MetaAccountModel) case chainAsset(wallet: MetaAccountModel, chainAsset: ChainAsset) case chainAssets(chainAssets: [ChainAsset], wallet: MetaAccountModel) + case networkManagement(wallet: MetaAccountModel) } -final class WalletBalanceSubscriptionAdapter: WalletBalanceSubscriptionAdapterProtocol { +final class WalletBalanceSubscriptionAdapter: WalletBalanceSubscriptionAdapterProtocol, ChainAssetListBuilder { static let shared = createWalletBalanceAdapter() // MARK: - PriceLocalStorageSubscriber @@ -164,6 +170,27 @@ final class WalletBalanceSubscriptionAdapter: WalletBalanceSubscriptionAdapterPr } } + func subscribeNetworkManagementBalance( + wallet: MetaAccountModel, + listener: WalletBalanceSubscriptionListener + ) { + let weakListener = WeakWrapper(target: listener) + listenersLock.exclusivelyWrite { [weak self] in + self?.listeners.append(weakListener) + } + updateWalletsIfNeeded(with: wallet) + let selectedChainAssets = filterChainAssets( + with: NetworkManagmentFilter(identifier: wallet.networkManagmentFilter), + chainAssets: chainAssets, + wallet: wallet, + search: nil + ) + + if let balances = buildBalance(for: [wallet], chainAssets: selectedChainAssets) { + notify(listener: listener, result: .success(balances)) + } + } + func unsubscribe(listener: WalletBalanceSubscriptionListener) { listenersLock.exclusivelyWrite { [weak self] in guard let strongSelf = self else { @@ -319,6 +346,17 @@ final class WalletBalanceSubscriptionAdapter: WalletBalanceSubscriptionAdapterPr let balances = buildBalance(for: [wallet], chainAssets: chainAssets) { notify(listener: listener, result: .success(balances)) } + case let .networkManagement(wallet): + let selectedChainAssets = filterChainAssets( + with: NetworkManagmentFilter(identifier: wallet.networkManagmentFilter), + chainAssets: chainAssets, + wallet: wallet, + search: nil + ) + if updatedWalletsIds.contains(wallet.metaId), + let balances = buildBalance(for: [wallet], chainAssets: selectedChainAssets) { + notify(listener: listener, result: .success(balances)) + } } } } @@ -359,6 +397,10 @@ extension WalletBalanceSubscriptionAdapter: EventVisitorProtocol { let currencies = wallets.map { $0.selectedCurrency } subscribeToPrices(for: chainAssets, currencies: currencies) } + if wallet.networkManagmentFilter != event.account.networkManagmentFilter { + wallets[index] = event.account + buildAndNotifyIfNeeded(with: [wallet.metaId], updatedChainAssets: chainAssets) + } wallets[index] = event.account } } diff --git a/fearless/Modules/BalanceInfo/BalanceInfoAssembly.swift b/fearless/Modules/BalanceInfo/BalanceInfoAssembly.swift index f903a69b41..6763943e03 100644 --- a/fearless/Modules/BalanceInfo/BalanceInfoAssembly.swift +++ b/fearless/Modules/BalanceInfo/BalanceInfoAssembly.swift @@ -8,6 +8,7 @@ enum BalanceInfoType { case wallet(wallet: MetaAccountModel) case chainAsset(wallet: MetaAccountModel, chainAsset: ChainAsset) case chainAssets(chainAssets: [ChainAsset], wallet: MetaAccountModel) + case networkManagement(wallet: MetaAccountModel) var wallet: MetaAccountModel { switch self { @@ -17,6 +18,8 @@ enum BalanceInfoType { return wallet case let .chainAssets(_, wallet): return wallet + case let .networkManagement(wallet): + return wallet } } } diff --git a/fearless/Modules/BalanceInfo/BalanceInfoInteractor.swift b/fearless/Modules/BalanceInfo/BalanceInfoInteractor.swift index c70227beaa..7e35d1cfd8 100644 --- a/fearless/Modules/BalanceInfo/BalanceInfoInteractor.swift +++ b/fearless/Modules/BalanceInfo/BalanceInfoInteractor.swift @@ -80,6 +80,11 @@ private extension BalanceInfoInteractor { wallet: wallet, listener: self ) + case let .networkManagement(wallet): + walletBalanceSubscriptionAdapter.subscribeNetworkManagementBalance( + wallet: wallet, + listener: self + ) } } @@ -162,6 +167,8 @@ extension BalanceInfoInteractor: WalletBalanceSubscriptionListener { return .chainAsset(wallet: wallet, chainAsset: chainAsset) case let .chainAssets(chainAssets, wallet): return .chainAssets(chainAssets: chainAssets, wallet: wallet) + case let .networkManagement(wallet): + return .networkManagement(wallet: wallet) } } diff --git a/fearless/Modules/BalanceInfo/BalanceInfoPresenter.swift b/fearless/Modules/BalanceInfo/BalanceInfoPresenter.swift index 8860a73e42..472d9843d0 100644 --- a/fearless/Modules/BalanceInfo/BalanceInfoPresenter.swift +++ b/fearless/Modules/BalanceInfo/BalanceInfoPresenter.swift @@ -156,6 +156,10 @@ extension BalanceInfoPresenter: EventVisitorProtocol { let newType = BalanceInfoType.chainAssets(chainAssets: chainAssets, wallet: event.account) interactor.balanceInfoType = newType interactor.fetchBalanceInfo() + case .networkManagement: + let newType = BalanceInfoType.networkManagement(wallet: event.account) + interactor.balanceInfoType = newType + interactor.fetchBalanceInfo() } } } diff --git a/fearless/Modules/BalanceInfo/BalanceInfoViewModelFactory.swift b/fearless/Modules/BalanceInfo/BalanceInfoViewModelFactory.swift index b308683b7e..af149c4501 100644 --- a/fearless/Modules/BalanceInfo/BalanceInfoViewModelFactory.swift +++ b/fearless/Modules/BalanceInfo/BalanceInfoViewModelFactory.swift @@ -68,6 +68,17 @@ final class BalanceInfoViewModelFactory: BalanceInfoViewModelFactoryProtocol { infoButtonEnabled: infoButtonEnabled, locale: locale ) + case let .networkManagement(wallet): + guard let info = balances[wallet.metaId] else { + return zeroBalanceViewModel( + balanceString: wallet.selectedCurrency.symbol + "0", + infoButtonEnabled: false + ) + } + balanceInfoViewModel = buildWalletBalance( + with: info, + locale: locale + ) } return balanceInfoViewModel diff --git a/fearless/Modules/WalletMainContainer/WalletMainContainerAssembly.swift b/fearless/Modules/WalletMainContainer/WalletMainContainerAssembly.swift index 81222c2354..e6e0ee09f8 100644 --- a/fearless/Modules/WalletMainContainer/WalletMainContainerAssembly.swift +++ b/fearless/Modules/WalletMainContainer/WalletMainContainerAssembly.swift @@ -97,7 +97,7 @@ final class WalletMainContainerAssembly { private static func configureBalanceInfoModule( wallet: MetaAccountModel ) -> BalanceInfoModuleCreationResult? { - BalanceInfoAssembly.configureModule(with: .wallet(wallet: wallet)) + BalanceInfoAssembly.configureModule(with: .networkManagement(wallet: wallet)) } private static func configureAssetListModule( diff --git a/fearless/Modules/WalletMainContainer/WalletMainContainerPresenter.swift b/fearless/Modules/WalletMainContainer/WalletMainContainerPresenter.swift index 179d90e075..16829735af 100644 --- a/fearless/Modules/WalletMainContainer/WalletMainContainerPresenter.swift +++ b/fearless/Modules/WalletMainContainer/WalletMainContainerPresenter.swift @@ -162,7 +162,7 @@ extension WalletMainContainerPresenter: WalletMainContainerInteractorOutput { wallet = account provideViewModel() - balanceInfoModuleInput?.replace(infoType: .wallet(wallet: account)) + balanceInfoModuleInput?.replace(infoType: .networkManagement(wallet: account)) } func didReceiveControllerAccountIssue(issue: ControllerAccountIssue, hasStashItem: Bool) { From d278e24efb72bd21a91c5cbd6d293d0c03876b49 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Mon, 1 Jul 2024 14:18:53 +0500 Subject: [PATCH 068/115] [#FLW-4662] NFT. Send. There should be paste but not copy button --- fearless/Modules/NFT/NftSend/NftSendViewLayout.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/fearless/Modules/NFT/NftSend/NftSendViewLayout.swift b/fearless/Modules/NFT/NftSend/NftSendViewLayout.swift index cb0de99035..a6f8695629 100644 --- a/fearless/Modules/NFT/NftSend/NftSendViewLayout.swift +++ b/fearless/Modules/NFT/NftSend/NftSendViewLayout.swift @@ -88,6 +88,7 @@ final class NftSendViewLayout: UIView { var locale = Locale.current { didSet { + searchView.locale = locale if locale != oldValue { applyLocalization() } From 1429d887d6c375547d922637c07fa8279f38299e Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Mon, 1 Jul 2024 14:23:51 +0500 Subject: [PATCH 069/115] [#FLW-4651] We can see Equilibrium network --- fearless/Modules/WalletDetails/WalletDetailsViewFactory.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/fearless/Modules/WalletDetails/WalletDetailsViewFactory.swift b/fearless/Modules/WalletDetails/WalletDetailsViewFactory.swift index 246108d628..18ef260319 100644 --- a/fearless/Modules/WalletDetails/WalletDetailsViewFactory.swift +++ b/fearless/Modules/WalletDetails/WalletDetailsViewFactory.swift @@ -7,6 +7,7 @@ final class WalletDetailsViewFactory { flow: WalletDetailsFlow ) -> WalletDetailsViewProtocol { let chainsRepository = ChainRepositoryFactory().createRepository( + for: NSPredicate.enabledCHain(), sortDescriptors: [NSSortDescriptor.chainsByAddressPrefix] ) From f8387556d029d8266aee4a9caa00e8fc1ef0afa3 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Mon, 1 Jul 2024 14:37:19 +0500 Subject: [PATCH 070/115] [#FLW-4627] Wallet details. Search. There is no sumimasen text --- .../WalletDetailsViewController.swift | 41 +++++++++++++++++++ .../WalletDetailsViewLayout.swift | 10 ++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/fearless/Modules/WalletDetails/WalletDetailsViewController.swift b/fearless/Modules/WalletDetails/WalletDetailsViewController.swift index 1c4ee32af2..ade40f367b 100644 --- a/fearless/Modules/WalletDetails/WalletDetailsViewController.swift +++ b/fearless/Modules/WalletDetails/WalletDetailsViewController.swift @@ -85,6 +85,7 @@ extension WalletDetailsViewController: WalletDetailsViewProtocol { func didReceive(state: WalletDetailsViewState) { self.state = state applyState() + reloadEmptyState(animated: true) } func didReceive(locale: Locale) { @@ -217,3 +218,43 @@ extension WalletDetailsViewController: KeyboardViewAdoptable { func offsetFromKeyboardWithInset(_: CGFloat) -> CGFloat { 0 } func updateWhileKeyboardFrameChanging(_: CGRect) {} } + +// MARK: - EmptyStateViewOwnerProtocol + +extension WalletDetailsViewController: EmptyStateViewOwnerProtocol { + var emptyStateDelegate: EmptyStateDelegate { self } + var emptyStateDataSource: EmptyStateDataSource { self } +} + +// MARK: - EmptyStateDataSource + +extension WalletDetailsViewController: EmptyStateDataSource { + var viewForEmptyState: UIView? { + let emptyView = EmptyView() + emptyView.image = R.image.iconWarningGray() + emptyView.title = R.string.localizable + .emptyViewTitle(preferredLanguages: rootView.locale?.rLanguages) + emptyView.text = R.string.localizable.emptyViewDescription(preferredLanguages: rootView.locale?.rLanguages) + emptyView.iconMode = .smallFilled + return emptyView + } + + var contentViewForEmptyState: UIView { + rootView.container + } +} + +// MARK: - EmptyStateDelegate + +extension WalletDetailsViewController: EmptyStateDelegate { + var shouldDisplayEmptyState: Bool { + switch state { + case let .normal(viewModel): + return viewModel.sections.isEmpty + case let .export(viewModel): + return viewModel.sections.isEmpty + case .none: + return false + } + } +} diff --git a/fearless/Modules/WalletDetails/WalletDetailsViewLayout.swift b/fearless/Modules/WalletDetails/WalletDetailsViewLayout.swift index 0b42ddc9c8..2f54e00b58 100644 --- a/fearless/Modules/WalletDetails/WalletDetailsViewLayout.swift +++ b/fearless/Modules/WalletDetails/WalletDetailsViewLayout.swift @@ -26,6 +26,7 @@ final class WalletDetailsViewLayout: UIView { return searchTextField }() + let container = UIView() let tableView: UITableView = { let view = UITableView(frame: .zero, style: .grouped) view.backgroundColor = R.color.colorBlack19() @@ -136,13 +137,18 @@ private extension WalletDetailsViewLayout { make.height.equalTo(48) } - addSubview(tableView) - tableView.snp.makeConstraints { make in + addSubview(container) + container.snp.makeConstraints { make in make.leading.trailing.equalToSuperview() make.top.equalTo(searchTextField.snp.bottom).offset(UIConstants.bigOffset) keyboardAdoptableConstraint = make.bottom.equalTo(safeAreaLayoutGuide).constraint } + container.addSubview(tableView) + tableView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + addSubview(exportButton) exportButton.snp.makeConstraints { make in make.bottom.equalTo(safeAreaLayoutGuide).inset(UIConstants.bigOffset) From dd8e267219151730b004a5f39a83aa3b6d3c5e29 Mon Sep 17 00:00:00 2001 From: Radmir Dzhurabaev Date: Mon, 1 Jul 2024 15:06:41 +0500 Subject: [PATCH 071/115] [#FLW-4627] Wallet details. Search. There is no sumimasen text --- .../AccountExportPasswordViewController.xib | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/fearless/Modules/Export/AccountExportPassword/AccountExportPasswordViewController.xib b/fearless/Modules/Export/AccountExportPassword/AccountExportPasswordViewController.xib index c5ee3eeb5d..f4aa026a66 100644 --- a/fearless/Modules/Export/AccountExportPassword/AccountExportPasswordViewController.xib +++ b/fearless/Modules/Export/AccountExportPassword/AccountExportPasswordViewController.xib @@ -1,9 +1,9 @@ - + - + @@ -66,7 +66,7 @@ - + @@ -119,6 +119,10 @@ + + + +