diff --git a/Podfile b/Podfile index de248ae79d..18305633da 100644 --- a/Podfile +++ b/Podfile @@ -5,12 +5,12 @@ abstract_target 'novawalletAll' do use_frameworks! pod 'DSF_QRCode', '~> 18.0.0' - pod 'SubstrateSdk', :git => 'https://github.com/nova-wallet/substrate-sdk-ios.git', :tag => '3.4.0' + pod 'SubstrateSdk', :git => 'https://github.com/nova-wallet/substrate-sdk-ios.git', :tag => '3.5.0' pod 'SwiftLint' pod 'R.swift', :inhibit_warnings => true pod 'SoraKeystore', '~> 1.0.0' pod 'SoraUI', :git => 'https://github.com/ERussel/UIkit-iOS.git', :tag => '1.13.0' - pod 'Operation-iOS', :git => 'https://github.com/novasamatech/Operation-iOS', :tag => '2.0.1' + pod 'Operation-iOS', :git => 'https://github.com/novasamatech/Operation-iOS', :tag => '2.1.0' pod 'SoraFoundation', :git => 'https://github.com/ERussel/Foundation-iOS.git', :tag => '1.1.0' pod 'SwiftyBeaver' pod 'ReachabilitySwift', '~> 5.2.4' @@ -40,12 +40,12 @@ abstract_target 'novawalletAll' do inherit! :search_paths pod 'Cuckoo' - pod 'SubstrateSdk', :git => 'https://github.com/nova-wallet/substrate-sdk-ios.git', :tag => '3.4.0' + pod 'SubstrateSdk', :git => 'https://github.com/nova-wallet/substrate-sdk-ios.git', :tag => '3.5.0' pod 'SoraFoundation', :git => 'https://github.com/ERussel/Foundation-iOS.git', :tag => '1.1.0' pod 'R.swift', :inhibit_warnings => true pod 'FireMock', :inhibit_warnings => true pod 'SoraKeystore', '~> 1.0.0' - pod 'Operation-iOS', :git => 'https://github.com/novasamatech/Operation-iOS', :tag => '2.0.1' + pod 'Operation-iOS', :git => 'https://github.com/novasamatech/Operation-iOS', :tag => '2.1.0' pod 'Sourcery', '~> 1.4' pod 'Starscream', :git => 'https://github.com/novasamatech/Starscream.git', :tag => '4.0.12' pod 'HydraMath', :git => 'https://github.com/novasamatech/hydra-math-swift.git', :tag => '0.2' @@ -65,9 +65,9 @@ abstract_target 'novawalletAll' do pod 'R.swift', :inhibit_warnings => true pod 'SoraFoundation', :git => 'https://github.com/ERussel/Foundation-iOS.git', :tag => '1.1.0' pod 'SoraKeystore', '~> 1.0.0' - pod 'Operation-iOS', :git => 'https://github.com/novasamatech/Operation-iOS', :tag => '2.0.1' + pod 'Operation-iOS', :git => 'https://github.com/novasamatech/Operation-iOS', :tag => '2.1.0' pod 'Sourcery', '~> 1.4' - pod 'SubstrateSdk', :git => 'https://github.com/nova-wallet/substrate-sdk-ios.git', :tag => '3.4.0' + pod 'SubstrateSdk', :git => 'https://github.com/nova-wallet/substrate-sdk-ios.git', :tag => '3.5.0' pod 'SwiftyBeaver' pod 'IrohaCrypto', :git => 'https://github.com/novasamatech/IrohaCrypto', :tag => '0.9.1' pod 'secp256k1.c', :git => 'https://github.com/novasamatech/secp256k1.c', :tag => '0.1.3' diff --git a/Podfile.lock b/Podfile.lock index 96840216a4..da8f912933 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1387,7 +1387,7 @@ PODS: - nanopb/encode (= 3.30910.0) - nanopb/decode (3.30910.0) - nanopb/encode (3.30910.0) - - Operation-iOS (2.0.1) + - Operation-iOS (2.1.0) - PromisesObjC (2.4.0) - R.swift (5.4.0): - R.swift.Library (~> 5.3.0) @@ -1450,7 +1450,7 @@ PODS: - SoraUI/Skrull (1.13.0) - Sourcery (1.4.1) - Starscream (4.0.12) - - SubstrateSdk (3.4.0): + - SubstrateSdk (3.5.0): - BigInt (~> 5.0) - IrohaCrypto/ed25519 (~> 0.9.0) - IrohaCrypto/Scrypt (~> 0.9.0) @@ -1458,7 +1458,7 @@ PODS: - IrohaCrypto/sr25519 (~> 0.9.0) - IrohaCrypto/ss58 (~> 0.9.0) - keccak.c (~> 0.1.0) - - Operation-iOS (~> 2.0.1) + - Operation-iOS (~> 2.1.0) - ReachabilitySwift (~> 5.2.4) - Starscream - TweetNacl (~> 1.0.0) @@ -1544,7 +1544,7 @@ DEPENDENCIES: - IrohaCrypto (from `https://github.com/novasamatech/IrohaCrypto`, tag `0.9.1`) - Kingfisher - MetadataShortenerApi (from `https://github.com/novasamatech/metadata-shortener-ios.git`, tag `0.1.0`) - - Operation-iOS (from `https://github.com/novasamatech/Operation-iOS`, tag `2.0.1`) + - Operation-iOS (from `https://github.com/novasamatech/Operation-iOS`, tag `2.1.0`) - R.swift - ReachabilitySwift (~> 5.2.4) - secp256k1.c (from `https://github.com/novasamatech/secp256k1.c`, tag `0.1.3`) @@ -1554,7 +1554,7 @@ DEPENDENCIES: - SoraUI (from `https://github.com/ERussel/UIkit-iOS.git`, tag `1.13.0`) - Sourcery (~> 1.4) - Starscream (from `https://github.com/novasamatech/Starscream.git`, tag `4.0.12`) - - SubstrateSdk (from `https://github.com/nova-wallet/substrate-sdk-ios.git`, tag `3.4.0`) + - SubstrateSdk (from `https://github.com/nova-wallet/substrate-sdk-ios.git`, tag `3.5.0`) - SwiftAlgorithms (~> 1.0.0) - SwiftDraw (~> 0.18.0) - SwiftFormat/CLI (~> 0.47.13) @@ -1636,7 +1636,7 @@ EXTERNAL SOURCES: :tag: 0.1.0 Operation-iOS: :git: https://github.com/novasamatech/Operation-iOS - :tag: 2.0.1 + :tag: 2.1.0 secp256k1.c: :git: https://github.com/novasamatech/secp256k1.c :tag: 0.1.3 @@ -1651,7 +1651,7 @@ EXTERNAL SOURCES: :tag: 4.0.12 SubstrateSdk: :git: https://github.com/nova-wallet/substrate-sdk-ios.git - :tag: 3.4.0 + :tag: 3.5.0 SwiftRLP: :git: https://github.com/ERussel/SwiftRLP.git WalletConnectSwiftV2: @@ -1679,7 +1679,7 @@ CHECKOUT OPTIONS: :tag: 0.1.0 Operation-iOS: :git: https://github.com/novasamatech/Operation-iOS - :tag: 2.0.1 + :tag: 2.1.0 secp256k1.c: :git: https://github.com/novasamatech/secp256k1.c :tag: 0.1.3 @@ -1694,7 +1694,7 @@ CHECKOUT OPTIONS: :tag: 4.0.12 SubstrateSdk: :git: https://github.com/nova-wallet/substrate-sdk-ios.git - :tag: 3.4.0 + :tag: 3.5.0 SwiftRLP: :commit: 809e68a002d19ee3d8bbeb72577224b7513e7e8e :git: https://github.com/ERussel/SwiftRLP.git @@ -1740,7 +1740,7 @@ SPEC CHECKSUMS: leveldb-library: e8eadf9008a61f9e1dde3978c086d2b6d9b9dc28 MetadataShortenerApi: b2d5d3ddb2e75e34688cc5f162881a943939ea9a nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - Operation-iOS: 5f1c2cb82bf10f46b92faefc92d5821550b14eb5 + Operation-iOS: 29111118f2bc5049f7bdc4e27c3967f5acc61802 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 R.swift: c533450b0f7dc494e0993f5f1a1db925d84c3006 R.swift.Library: 0fc583cb55a99e28901299cc451614cad1161962 @@ -1754,7 +1754,7 @@ SPEC CHECKSUMS: SoraUI: 1d1a25881d1d597f19bc55f82c99ee236cc1ab11 Sourcery: db66600e8b285c427701821598d07cf3c7e6c476 Starscream: 9265e4163aeea93d4a74c49bc8cb4c0b9c6ff350 - SubstrateSdk: ee92f2ede60ffc3a80c73d57870c2cea05741ab4 + SubstrateSdk: e5aea1104b89dab2e2989fa9979854e1ca238f46 SwiftAlgorithms: 38dda4731d19027fdeee1125f973111bf3386b53 SwiftDraw: f63484562ddd30d9682b5576acc1d98acc2bec8f SwiftFormat: 73573b89257437c550b03d934889725fbf8f75e5 @@ -1771,6 +1771,6 @@ SPEC CHECKSUMS: ZMarkupParser: a92d31ba40695b790f1da5fec98c3d4505341aff ZNSTextAttachment: 1ddd53660a8d3c42dbb716bf6866ffce22c44181 -PODFILE CHECKSUM: f485e2dc6e10abfdd79cae68f47ca4679c0cd84f +PODFILE CHECKSUM: 9ee1d20436848ff58d57387fbd296c1b2d5c555d COCOAPODS: 1.15.2 diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index d928ba8f88..cad8bd1b2c 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 044BCF706211CF2AA2D28CAE /* CloudBackupReviewChangesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1AE1C4FF054023B98969F0 /* CloudBackupReviewChangesViewController.swift */; }; 049DA9A36A72CB6F8401769C /* WalletsListWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F404EE82BC45BFE0F42E0A4 /* WalletsListWireframe.swift */; }; 04B85867D67D56994D99FF14 /* NftListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89CFED2E01AB638656E251AF /* NftListProtocols.swift */; }; + 04D6122369889C927BB3D13F /* SwapRouteDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 461EFCB1DAACD7D5DBBB713C /* SwapRouteDetailsViewController.swift */; }; 04D86D5341406305E60F6D18 /* ReferendumVoteSetupInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 024B7E67C0603C53981EC394 /* ReferendumVoteSetupInteractor.swift */; }; 0503753EC5E2136973D13E52 /* PayCardProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0325A4A30102D3583ED09CEF /* PayCardProtocols.swift */; }; 054C4BCDEC29ED5F74A36E8B /* ExportMnemonicPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61EBE466BDCF77E65FDCDF81 /* ExportMnemonicPresenter.swift */; }; @@ -35,6 +36,7 @@ 069511799D6D0C3CB0BD4E3C /* AssetOperationNetworkListViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E646AC94F6AD246B0FD17EB /* AssetOperationNetworkListViewFactory.swift */; }; 06FD6F5999D57B27B29C8738 /* ParaStkStakeConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037D3CE23FFD176F4F7DABC0 /* ParaStkStakeConfirmViewFactory.swift */; }; 0754911527A21957BD25A1DA /* CommonDelegationTracksViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CD1EE26234E390E938D9311 /* CommonDelegationTracksViewFactory.swift */; }; + 076E4DC5984B431018CB9F65 /* SwapExecutionProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2F8A21F1E63BF16DCCE89FE /* SwapExecutionProtocols.swift */; }; 07AB0BC861AA5F134DB9AC26 /* StartStakingInfoProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = C92B3D5B314FB3EAE65FA471 /* StartStakingInfoProtocols.swift */; }; 07D1F0F4FAE24BED8A1CF257 /* SwapSlippagePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0816F2A4A5CC1F111E626188 /* SwapSlippagePresenter.swift */; }; 082CFE82CCE17D81CFD5EB25 /* ExportProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15C1E5B04B82499FEE87B0D6 /* ExportProtocols.swift */; }; @@ -52,9 +54,21 @@ 0B48B02E973CB304B765BBC9 /* ReferendumDetailsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3ABAD23C0039AFA8351C650 /* ReferendumDetailsProtocols.swift */; }; 0B65DAE0327678679CACE0B1 /* GovernanceDelegateInfoViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E894D4633D04AD4415CE1F2 /* GovernanceDelegateInfoViewFactory.swift */; }; 0BB2E3FF30B1700D321C526A /* TransferNetworkSelectionViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA463769D0F411429780D7D /* TransferNetworkSelectionViewFactory.swift */; }; + 0C0387512D066474000A2F24 /* AssetExchageUsdtConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0387502D066474000A2F24 /* AssetExchageUsdtConverter.swift */; }; + 0C03877C2D09A1AA000A2F24 /* MockAssetExchangePathCostEstimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C03877B2D09A1AA000A2F24 /* MockAssetExchangePathCostEstimator.swift */; }; + 0C03877F2D0A1043000A2F24 /* SwapRouteDetailsItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C03877E2D0A1043000A2F24 /* SwapRouteDetailsItemView.swift */; }; + 0C0387812D0A1283000A2F24 /* AssetAmountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0387802D0A1283000A2F24 /* AssetAmountView.swift */; }; + 0C0387832D0A1878000A2F24 /* AssetAmountRouteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0387822D0A1878000A2F24 /* AssetAmountRouteItemView.swift */; }; + 0C0387852D0A1C0A000A2F24 /* LabelRouteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0387842D0A1C0A000A2F24 /* LabelRouteItemView.swift */; }; + 0C0387872D0A24E6000A2F24 /* SwapRouteDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0387862D0A24E6000A2F24 /* SwapRouteDetailsView.swift */; }; + 0C03878A2D0A2785000A2F24 /* SwapRouteDetailsViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0387892D0A2785000A2F24 /* SwapRouteDetailsViewModelFactory.swift */; }; + 0C03878C2D0B2686000A2F24 /* LinePatternView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C03878B2D0B2686000A2F24 /* LinePatternView.swift */; }; + 0C0387952D0CD120000A2F24 /* SwapOperationFeeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0387942D0CD120000A2F24 /* SwapOperationFeeView.swift */; }; + 0C0387982D0CE605000A2F24 /* SwapFeeDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0387972D0CE605000A2F24 /* SwapFeeDetailsViewModel.swift */; }; + 0C03879A2D0CEDC9000A2F24 /* SwapFeeDetailsViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0387992D0CEDC9000A2F24 /* SwapFeeDetailsViewModelFactory.swift */; }; + 0C03879C2D0F64F9000A2F24 /* HydraSwapParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C03879B2D0F64F9000A2F24 /* HydraSwapParams.swift */; }; 0C053ECB2BA2FA5C003063A0 /* StorageLocationMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C053ECA2BA2FA5C003063A0 /* StorageLocationMigrationTests.swift */; }; 0C0CB37F2AC540B200EAC516 /* AssetConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB37E2AC540B200EAC516 /* AssetConversion.swift */; }; - 0C0CB3822AC545A800EAC516 /* AssetConversionExtrinsicService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB3812AC545A800EAC516 /* AssetConversionExtrinsicService.swift */; }; 0C0CB3852AC561FB00EAC516 /* AssetHubSwapOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB3842AC561FB00EAC516 /* AssetHubSwapOperationFactory.swift */; }; 0C0CB3882AC5688100EAC516 /* AssetConversionPallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB3872AC5688100EAC516 /* AssetConversionPallet.swift */; }; 0C0CB38A2AC56A1600EAC516 /* AssetConversionPallet+Path.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB3892AC56A1600EAC516 /* AssetConversionPallet+Path.swift */; }; @@ -63,8 +77,21 @@ 0C0E0A9D2B3EFD2500865F10 /* ProxyCallFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0E0A9C2B3EFD2500865F10 /* ProxyCallFilter.swift */; }; 0C0E0AA02B3F013800865F10 /* BalancesPallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0E0A9F2B3F013800865F10 /* BalancesPallet.swift */; }; 0C0E0AA32B3F01FD00865F10 /* UniquesPallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0E0AA22B3F01FD00865F10 /* UniquesPallet.swift */; }; + 0C11D8522CC9F5F2003EC46D /* HydraOmnipoolExchangeEdge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C11D8512CC9F5F2003EC46D /* HydraOmnipoolExchangeEdge.swift */; }; + 0C11D8542CC9F82C003EC46D /* HydraStableswapExchangeEdge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C11D8532CC9F82C003EC46D /* HydraStableswapExchangeEdge.swift */; }; + 0C11D8562CC9F9E6003EC46D /* HydraXYKExchangeEdge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C11D8552CC9F9E6003EC46D /* HydraXYKExchangeEdge.swift */; }; + 0C11D8582CC9FED7003EC46D /* AssetsHydraStableswapExchange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C11D8572CC9FED7003EC46D /* AssetsHydraStableswapExchange.swift */; }; + 0C11D85A2CCA3377003EC46D /* AssetsHydraOmnipoolExchange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C11D8592CCA3377003EC46D /* AssetsHydraOmnipoolExchange.swift */; }; + 0C11D85C2CCA3CCC003EC46D /* AssetsHydraXYKExchange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C11D85B2CCA3CCC003EC46D /* AssetsHydraXYKExchange.swift */; }; + 0C11D85E2CCA470D003EC46D /* AssetsExchangeRouteManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C11D85D2CCA470D003EC46D /* AssetsExchangeRouteManager.swift */; }; + 0C11D8602CCA486B003EC46D /* AssetExchangeRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C11D85F2CCA486B003EC46D /* AssetExchangeRoute.swift */; }; + 0C11F0282CEFD99C008D19D2 /* CountdownLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C11F0272CEFD99C008D19D2 /* CountdownLoadingView.swift */; }; 0C12A2472AA1D96000C7FA49 /* SubqueryMultistakingTypeFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12A2462AA1D96000C7FA49 /* SubqueryMultistakingTypeFactory.swift */; }; 0C12A2492AA35AE300C7FA49 /* RelaychainStakingValidatorFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12A2482AA35AE300C7FA49 /* RelaychainStakingValidatorFacade.swift */; }; + 0C12BC352CE952EF00AB919D /* SwapRouteViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12BC342CE952EF00AB919D /* SwapRouteViewCell.swift */; }; + 0C12BC372CEA7DCB00AB919D /* AssetExchangePrice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12BC362CEA7DCB00AB919D /* AssetExchangePrice.swift */; }; + 0C12BC3A2CEA7ECB00AB919D /* AssetExchangePriceStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12BC392CEA7ECB00AB919D /* AssetExchangePriceStore.swift */; }; + 0C12BC3E2CEA9DF800AB919D /* AssetExchangeTimeEstimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12BC3D2CEA9DF800AB919D /* AssetExchangeTimeEstimation.swift */; }; 0C12BC482CEB996700AB919D /* XcmV4Multilocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12BC472CEB996700AB919D /* XcmV4Multilocation.swift */; }; 0C12BC492CEB996700AB919D /* XcmJunctionV4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12BC462CEB996700AB919D /* XcmJunctionV4.swift */; }; 0C1338102AB832B30036BCD6 /* QRImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13380F2AB832B30036BCD6 /* QRImageViewModel.swift */; }; @@ -113,6 +140,9 @@ 0C1998EA2C4A22FD000EBFB8 /* BalancesRemoteSubscriptionService+Protocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1998E92C4A22FD000EBFB8 /* BalancesRemoteSubscriptionService+Protocol.swift */; }; 0C1998ED2C4CB24C000EBFB8 /* AssetHold+Display.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1998EC2C4CB24C000EBFB8 /* AssetHold+Display.swift */; }; 0C1998EF2C4CC51D000EBFB8 /* SCLoadableControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1998EE2C4CC51D000EBFB8 /* SCLoadableControllerProtocol.swift */; }; + 0C19CC9F2CC6EBBC007F8ED8 /* CrosschainAssetsExchange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C19CC9E2CC6EBBC007F8ED8 /* CrosschainAssetsExchange.swift */; }; + 0C19CCA22CC6EC75007F8ED8 /* AssetsHubExchangeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C19CCA12CC6EC75007F8ED8 /* AssetsHubExchangeProvider.swift */; }; + 0C19CCA52CC6EDAE007F8ED8 /* AssetsExchangeBaseProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C19CCA42CC6EDAE007F8ED8 /* AssetsExchangeBaseProvider.swift */; }; 0C1BE19E2A46EDB40010933C /* String+ScientificInt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1BE19D2A46EDB40010933C /* String+ScientificInt.swift */; }; 0C1BE1A02A46F1F00010933C /* ScientificStringParsing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1BE19F2A46F1F00010933C /* ScientificStringParsing.swift */; }; 0C1BE1A22A46F93B0010933C /* BigUInt+Scientific.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1BE1A12A46F93B0010933C /* BigUInt+Scientific.swift */; }; @@ -138,11 +168,26 @@ 0C259EA42B46721B00CB86E4 /* ProxyMessageSheetViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C259EA32B46721B00CB86E4 /* ProxyMessageSheetViewFactory.swift */; }; 0C259EA82B46C55C00CB86E4 /* ExtrinsicSigningErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C259EA72B46C55C00CB86E4 /* ExtrinsicSigningErrorHandling.swift */; }; 0C29B5382A4C68A500E35C6D /* AnimationUpdatibleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C29B5372A4C68A500E35C6D /* AnimationUpdatibleView.swift */; }; + 0C2A3C932CDC813B00A0E2B3 /* AssetsExchangeOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2A3C922CDC813B00A0E2B3 /* AssetsExchangeOperationFactory.swift */; }; + 0C2A3C952CDC8ADB00A0E2B3 /* SwapAssetSelectionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2A3C942CDC8ADB00A0E2B3 /* SwapAssetSelectionModel.swift */; }; + 0C2A3C982CDCCECC00A0E2B3 /* SwapTokensFlowState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2A3C972CDCCECC00A0E2B3 /* SwapTokensFlowState.swift */; }; + 0C2A3C9A2CDCEB8A00A0E2B3 /* SwapAssetSelectionClosure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2A3C992CDCEB8A00A0E2B3 /* SwapAssetSelectionClosure.swift */; }; 0C2AA829B5CB89B39E0FA95E /* CrowdloanContributionConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF01941105BCD02536538362 /* CrowdloanContributionConfirmProtocols.swift */; }; 0C2B18AE2BFE378B00206EDE /* CloudBackupSyncService+Create.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2B18AD2BFE378B00206EDE /* CloudBackupSyncService+Create.swift */; }; 0C2B18B02BFE3FC600206EDE /* CloudBackupSyncResultChanges+Critical.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2B18AF2BFE3FC600206EDE /* CloudBackupSyncResultChanges+Critical.swift */; }; 0C2B18B22BFE48BD00206EDE /* CloudBackupUpdateApplicationFactory+Create.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2B18B12BFE48BD00206EDE /* CloudBackupUpdateApplicationFactory+Create.swift */; }; 0C2B18B42BFEFD2A00206EDE /* CloudBackupConflictsResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2B18B32BFEFD2A00206EDE /* CloudBackupConflictsResolver.swift */; }; + 0C2DA89A2CC21419001F79C8 /* GraphQuotableEdge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2DA8992CC21419001F79C8 /* GraphQuotableEdge.swift */; }; + 0C2DA89C2CC215D0001F79C8 /* AssetExchangeGraphEdge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2DA89B2CC215D0001F79C8 /* AssetExchangeGraphEdge.swift */; }; + 0C2DA89E2CC21853001F79C8 /* CrosschainExchangeEdge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2DA89D2CC21853001F79C8 /* CrosschainExchangeEdge.swift */; }; + 0C2DA8A02CC21B09001F79C8 /* IndexedChainModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2DA89F2CC21B09001F79C8 /* IndexedChainModels.swift */; }; + 0C2DA8A22CC21E5F001F79C8 /* AssetHubExchangeEdge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2DA8A12CC21E5F001F79C8 /* AssetHubExchangeEdge.swift */; }; + 0C2DA8A42CC223D9001F79C8 /* AssetsHydraExchangeEdge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2DA8A32CC223D9001F79C8 /* AssetsHydraExchangeEdge.swift */; }; + 0C2DA8A62CC265F0001F79C8 /* AssetsExchangeGraphProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2DA8A52CC265F0001F79C8 /* AssetsExchangeGraphProvider.swift */; }; + 0C2DA8A82CC2679E001F79C8 /* AssetsExchangeGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2DA8A72CC2679E001F79C8 /* AssetsExchangeGraph.swift */; }; + 0C2DA8AA2CC2A2E2001F79C8 /* AnyAssetExchangeEdge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2DA8A92CC2A2E2001F79C8 /* AnyAssetExchangeEdge.swift */; }; + 0C2DA8AD2CC2B156001F79C8 /* AssetsExchangeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2DA8AC2CC2B156001F79C8 /* AssetsExchangeTests.swift */; }; + 0C2DA8AF2CC2F928001F79C8 /* AssetsExchangeGraphDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2DA8AE2CC2F928001F79C8 /* AssetsExchangeGraphDescription.swift */; }; 0C2F86802A7119D400593C01 /* AuraSessionLengthOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2F867F2A7119D400593C01 /* AuraSessionLengthOperationFactory.swift */; }; 0C2F86822A7233DC00593C01 /* EraNominationPoolsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2F86812A7233DC00593C01 /* EraNominationPoolsService.swift */; }; 0C2F86842A72343800593C01 /* EraNominationPoolsServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2F86832A72343800593C01 /* EraNominationPoolsServiceProtocol.swift */; }; @@ -154,7 +199,6 @@ 0C2F86962A72807E00593C01 /* NominationPoolsRewardEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2F86952A72807E00593C01 /* NominationPoolsRewardEngine.swift */; }; 0C2F86982A728EE900593C01 /* NPoolsRewardEngineFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2F86972A728EE900593C01 /* NPoolsRewardEngineFactory.swift */; }; 0C2F869A2A72948100593C01 /* NominationPoolsApyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2F86992A72948100593C01 /* NominationPoolsApyTests.swift */; }; - 0C2FDF192AEB82AA006A6C59 /* AssetHubFeeModelBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2FDF182AEB82AA006A6C59 /* AssetHubFeeModelBuilder.swift */; }; 0C3205BB2A8679F0002EB914 /* EvmGasPriceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205BA2A8679F0002EB914 /* EvmGasPriceProvider.swift */; }; 0C3205BE2A867A9C002EB914 /* EvmLegacyGasPriceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205BD2A867A9C002EB914 /* EvmLegacyGasPriceProvider.swift */; }; 0C3205C02A867DD6002EB914 /* EvmMaxPriorityGasPriceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205BF2A867DD6002EB914 /* EvmMaxPriorityGasPriceProvider.swift */; }; @@ -178,11 +222,21 @@ 0C3205E82A898195002EB914 /* EvmValidationErrorPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205E72A898195002EB914 /* EvmValidationErrorPresentable.swift */; }; 0C3205EA2A8A0539002EB914 /* EvmValidationProviderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205E92A8A0539002EB914 /* EvmValidationProviderFactory.swift */; }; 0C3205EC2A8A122D002EB914 /* FeeOutputModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205EB2A8A122D002EB914 /* FeeOutputModel.swift */; }; - 0C363E962B6B591C0065AFA6 /* HydraExtrinsicOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C363E952B6B591C0065AFA6 /* HydraExtrinsicOperationFactory.swift */; }; + 0C3272892CD1F83D00FC1B42 /* AssetExchangeAtomicOperationArgs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3272882CD1F83D00FC1B42 /* AssetExchangeAtomicOperationArgs.swift */; }; + 0C32728D2CD1F90100FC1B42 /* AssetExchangeSwapLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32728C2CD1F90100FC1B42 /* AssetExchangeSwapLimit.swift */; }; + 0C32728F2CD203F000FC1B42 /* XcmTotalFeeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32728E2CD203F000FC1B42 /* XcmTotalFeeModel.swift */; }; + 0C3272912CD2409B00FC1B42 /* AssetExchangeOperationFee.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3272902CD2409B00FC1B42 /* AssetExchangeOperationFee.swift */; }; + 0C3272932CD246DD00FC1B42 /* AssetExchangeOperationFee+Crosschain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3272922CD246DD00FC1B42 /* AssetExchangeOperationFee+Crosschain.swift */; }; + 0C3272952CD24F9600FC1B42 /* AssetHubExchangeAtomicOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3272942CD24F9600FC1B42 /* AssetHubExchangeAtomicOperation.swift */; }; + 0C3272972CD2821500FC1B42 /* ChainRegistry+Get.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3272962CD2821500FC1B42 /* ChainRegistry+Get.swift */; }; + 0C32729C2CD28C4000FC1B42 /* AssetExchangeOperationFee+AssetHub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32729B2CD28C4000FC1B42 /* AssetExchangeOperationFee+AssetHub.swift */; }; + 0C32729E2CD2975400FC1B42 /* HydraExchangeAtomicOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32729D2CD2975400FC1B42 /* HydraExchangeAtomicOperation.swift */; }; + 0C3272A02CD34CB200FC1B42 /* HydraExchangeExtrinsicParamsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32729F2CD34CB200FC1B42 /* HydraExchangeExtrinsicParamsFactory.swift */; }; + 0C3272A22CD34F7700FC1B42 /* HydraExchangeExtrinsicConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3272A12CD34F7700FC1B42 /* HydraExchangeExtrinsicConverter.swift */; }; + 0C33E8B62D0069EC0090096A /* AssetsExchangeFeeSupportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C33E8B52D0069EC0090096A /* AssetsExchangeFeeSupportProvider.swift */; }; + 0C33E8BA2D011D2E0090096A /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C33E8B92D011D2E0090096A /* Debouncer.swift */; }; 0C363E982B6B63280065AFA6 /* HydraConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C363E972B6B63280065AFA6 /* HydraConstants.swift */; }; - 0C363E9A2B6B69D60065AFA6 /* HydraFeeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C363E992B6B69D60065AFA6 /* HydraFeeService.swift */; }; 0C363E9C2B6B8C400065AFA6 /* HydraExtrinsicConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C363E9B2B6B8C400065AFA6 /* HydraExtrinsicConverter.swift */; }; - 0C363E9E2B6BB2E20065AFA6 /* HydraSwapsFeeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C363E9D2B6BB2E20065AFA6 /* HydraSwapsFeeTests.swift */; }; 0C37AFB72B555F1400009ECA /* StakingValidatorExposureFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C37AFB62B555F1400009ECA /* StakingValidatorExposureFacade.swift */; }; 0C37AFB92B5562B500009ECA /* StakingEraStakersExposureFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C37AFB82B5562B500009ECA /* StakingEraStakersExposureFactory.swift */; }; 0C37AFBB2B556A9300009ECA /* StakingPagedExposureFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C37AFBA2B556A9300009ECA /* StakingPagedExposureFactory.swift */; }; @@ -239,6 +293,8 @@ 0C41D21E2BFB0719000950EE /* WalletsFetchHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41D21D2BFB0719000950EE /* WalletsFetchHelper.swift */; }; 0C41D2202BFB07B4000950EE /* CloudBackupFetchHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41D21F2BFB07B4000950EE /* CloudBackupFetchHelper.swift */; }; 0C41D2222BFB0BAC000950EE /* KeystoreValidationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41D2212BFB0BAC000950EE /* KeystoreValidationHelper.swift */; }; + 0C423ABB2CDBAE9B00A64941 /* AssetExchangeFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C423ABA2CDBAE9B00A64941 /* AssetExchangeFacade.swift */; }; + 0C423ABD2CDBAEC900A64941 /* AssetExchangeGraphProvidingParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C423ABC2CDBAEC900A64941 /* AssetExchangeGraphProvidingParams.swift */; }; 0C4394D42BD6661F00578761 /* PasswordInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4394D32BD6661F00578761 /* PasswordInputView.swift */; }; 0C4394D72BD761D000578761 /* CloudBackupPasswordValidationResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4394D62BD761D000578761 /* CloudBackupPasswordValidationResult.swift */; }; 0C4394D92BD7626500578761 /* CloudBackupPasswordValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4394D82BD7626500578761 /* CloudBackupPasswordValidator.swift */; }; @@ -290,10 +346,30 @@ 0C59E8FE2AA773D6001E11F3 /* OperationDetailsDataProviderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C59E8FD2AA773D6001E11F3 /* OperationDetailsDataProviderFactory.swift */; }; 0C5FA9A52B830A650077934C /* WalletConnectUrlParsingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5FA9A42B830A650077934C /* WalletConnectUrlParsingService.swift */; }; 0C5FA9A72B8313950077934C /* String+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5FA9A62B8313950077934C /* String+Search.swift */; }; + 0C6108262CD5227900909928 /* HydraExchangeHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6108252CD5227900909928 /* HydraExchangeHost.swift */; }; + 0C6108282CD53CDE00909928 /* CrosschainExchangeHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6108272CD53CDE00909928 /* CrosschainExchangeHost.swift */; }; + 0C61082A2CD5DE9500909928 /* AssetHubExchangeHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6108292CD5DE9500909928 /* AssetHubExchangeHost.swift */; }; + 0C6108302CD5ED1400909928 /* ExtrinsicCustomFeeEstimatingFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C61082F2CD5ED1400909928 /* ExtrinsicCustomFeeEstimatingFactoryProtocol.swift */; }; + 0C6108322CD5EEBD00909928 /* AssetConversionFeeEstimatingFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6108312CD5EEBD00909928 /* AssetConversionFeeEstimatingFactory.swift */; }; + 0C6108342CD5F77E00909928 /* ExtrinsicFeeEstimatorHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6108332CD5F77E00909928 /* ExtrinsicFeeEstimatorHost.swift */; }; + 0C6108362CD5FC7300909928 /* ExtrinsicFeeInstallingWrapperFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6108352CD5FC7300909928 /* ExtrinsicFeeInstallingWrapperFactory.swift */; }; + 0C6108382CD7333100909928 /* ExtrinsicCustomFeeInstallingWrapperFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6108372CD7333100909928 /* ExtrinsicCustomFeeInstallingWrapperFactory.swift */; }; + 0C61083E2CD7382E00909928 /* AssetConversionFeeInstallingFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C61083D2CD7382E00909928 /* AssetConversionFeeInstallingFactory.swift */; }; + 0C6108412CD74DD200909928 /* AssetExchangeFeeEstimatingFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6108402CD74DD200909928 /* AssetExchangeFeeEstimatingFactory.swift */; }; + 0C6108432CD755A300909928 /* AssetExchangeGraphProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6108422CD755A300909928 /* AssetExchangeGraphProxy.swift */; }; + 0C6108452CD8827200909928 /* HydraSwapFeeCurrencyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6108442CD8827200909928 /* HydraSwapFeeCurrencyService.swift */; }; + 0C6108472CD882F900909928 /* SwapFeeCurrencyState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6108462CD882F900909928 /* SwapFeeCurrencyState.swift */; }; + 0C6108492CD889A000909928 /* AssetsExchange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6108482CD889A000909928 /* AssetsExchange.swift */; }; + 0C61084B2CD88F9B00909928 /* AssetsExchangeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C61084A2CD88F9B00909928 /* AssetsExchangeService.swift */; }; + 0C61084D2CD8909600909928 /* AssetExchangeFee.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C61084C2CD8909600909928 /* AssetExchangeFee.swift */; }; + 0C61084F2CD92C6F00909928 /* GraphEdgeFiltering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C61084E2CD92C6F00909928 /* GraphEdgeFiltering.swift */; }; + 0C6108512CD9D3BA00909928 /* GraphModel+BFS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6108502CD9D3BA00909928 /* GraphModel+BFS.swift */; }; 0C626D1B2A8F519100CDAF4E /* StakingMainPresenterFactory+NominationPools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C626D1A2A8F519100CDAF4E /* StakingMainPresenterFactory+NominationPools.swift */; }; 0C626D1D2A915E2F00CDAF4E /* StakingNominationPoolsStatics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C626D1C2A915E2F00CDAF4E /* StakingNominationPoolsStatics.swift */; }; 0C626D1F2A92AA0F00CDAF4E /* NominationPoolsDataProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C626D1E2A92AA0F00CDAF4E /* NominationPoolsDataProviding.swift */; }; 0C626D212A933A7D00CDAF4E /* StakingNPoolsViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C626D202A933A7D00CDAF4E /* StakingNPoolsViewModelFactory.swift */; }; + 0C6353452CE463BD00EAB200 /* AssetExchangePathFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6353442CE463BD00EAB200 /* AssetExchangePathFilter.swift */; }; + 0C6353472CE46FB200EAB200 /* AssetExchangeSufficiencyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6353462CE46FB200EAB200 /* AssetExchangeSufficiencyProvider.swift */; }; 0C63908B2BF073C20015D467 /* CloudBackupSyncMonitoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C63908A2BF073C20015D467 /* CloudBackupSyncMonitoring.swift */; }; 0C63908D2BF073D00015D467 /* ICloudBackupSyncMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C63908C2BF073D00015D467 /* ICloudBackupSyncMonitor.swift */; }; 0C6390912BF091610015D467 /* CloudBackupUpdateCalculationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6390902BF091610015D467 /* CloudBackupUpdateCalculationFactory.swift */; }; @@ -338,6 +414,8 @@ 0C7104A22C2D0A6200487E64 /* LedgerTxConfirmationParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7104A12C2D0A6200487E64 /* LedgerTxConfirmationParams.swift */; }; 0C7104A42C2D0FC500487E64 /* BaseLedgerTxConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7104A32C2D0FC500487E64 /* BaseLedgerTxConfirmInteractor.swift */; }; 0C7104A82C2D11EB00487E64 /* GenericLedgerTxConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7104A72C2D11EB00487E64 /* GenericLedgerTxConfirmInteractor.swift */; }; + 0C746DBC2CCE5E9900E9178B /* AssetExchangeExecutionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C746DBB2CCE5E9900E9178B /* AssetExchangeExecutionManager.swift */; }; + 0C746DBE2CCF440800E9178B /* AssetExchangeAtomicOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C746DBD2CCF440800E9178B /* AssetExchangeAtomicOperation.swift */; }; 0C75E2962C3F9263005A6232 /* DelegatedStakingPallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C75E2952C3F9263005A6232 /* DelegatedStakingPallet.swift */; }; 0C75E2982C3F92EC005A6232 /* DelegatedStakingPallet+Path.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C75E2972C3F92EC005A6232 /* DelegatedStakingPallet+Path.swift */; }; 0C77B55F2A83717000B5AE08 /* StaticValidatorListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C77B55E2A83717000B5AE08 /* StaticValidatorListViewController.swift */; }; @@ -384,18 +462,16 @@ 0C846B902BE5F069000EBFC2 /* StakingGlobalConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C846B8F2BE5F069000EBFC2 /* StakingGlobalConfigProvider.swift */; }; 0C846B922BE69079000EBFC2 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 0C846B912BE69079000EBFC2 /* PrivacyInfo.xcprivacy */; }; 0C846B952BE69417000EBFC2 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 0C846B942BE69417000EBFC2 /* PrivacyInfo.xcprivacy */; }; - 0C85FF222B6C001900FC0014 /* HydraSwapExtrinsicService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C85FF212B6C001900FC0014 /* HydraSwapExtrinsicService.swift */; }; 0C85FF242B6CB28200FC0014 /* AssetHubExtrinsicConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C85FF232B6CB28200FC0014 /* AssetHubExtrinsicConverter.swift */; }; - 0C85FF262B6CB9B400FC0014 /* HydraCallPathFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C85FF252B6CB9B400FC0014 /* HydraCallPathFactory.swift */; }; - 0C85FF282B6CBA9D00FC0014 /* AssetHubCallPathFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C85FF272B6CBA9D00FC0014 /* AssetHubCallPathFactory.swift */; }; 0C85FF2A2B6D230100FC0014 /* AssetHubReQuoteService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C85FF292B6D230100FC0014 /* AssetHubReQuoteService.swift */; }; 0C85FF2C2B6D23D600FC0014 /* ObservableSubscriptionSyncState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C85FF2B2B6D23D600FC0014 /* ObservableSubscriptionSyncState.swift */; }; 0C85FF2E2B6D300800FC0014 /* HydraOmnipoolFlowState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C85FF2D2B6D300800FC0014 /* HydraOmnipoolFlowState.swift */; }; 0C85FF302B6D35D600FC0014 /* AssetHubFlowState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C85FF2F2B6D35D600FC0014 /* AssetHubFlowState.swift */; }; - 0C85FF322B6D4A3500FC0014 /* AssetConversionFlowState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C85FF312B6D4A3500FC0014 /* AssetConversionFlowState.swift */; }; 0C85FF342B6D523B00FC0014 /* HydraOmnipoolQuoteFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C85FF332B6D523B00FC0014 /* HydraOmnipoolQuoteFactory.swift */; }; - 0C85FF372B6E0BB300FC0014 /* AssetConversionAggregationFactory+AssetHub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C85FF362B6E0BB300FC0014 /* AssetConversionAggregationFactory+AssetHub.swift */; }; - 0C85FF392B6E0C0600FC0014 /* AssetConversionAggregationFactory+HydraDx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C85FF382B6E0C0600FC0014 /* AssetConversionAggregationFactory+HydraDx.swift */; }; + 0C883A722CE2235600CAB4C8 /* HydraSwapEventsMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C883A712CE2235600CAB4C8 /* HydraSwapEventsMatcher.swift */; }; + 0C883A742CE241CB00CAB4C8 /* AssetConversionEventsMatching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C883A732CE241CB00CAB4C8 /* AssetConversionEventsMatching.swift */; }; + 0C883A762CE25E6800CAB4C8 /* HydraSwapEventParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C883A752CE25E6800CAB4C8 /* HydraSwapEventParser.swift */; }; + 0C883A782CE2640000CAB4C8 /* AssetConversionEventParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C883A772CE2640000CAB4C8 /* AssetConversionEventParser.swift */; }; 0C893E6A2A65591C00781503 /* PoolsMultistakingUpdateService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C893E692A65591C00781503 /* PoolsMultistakingUpdateService.swift */; }; 0C893E6D2A6562B400781503 /* NominationPools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C893E6C2A6562B400781503 /* NominationPools.swift */; }; 0C893E6F2A65702A00781503 /* NominationPools+CodingPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C893E6E2A65702A00781503 /* NominationPools+CodingPath.swift */; }; @@ -464,6 +540,7 @@ 0C9951CF2AE2BAE500B65615 /* PromotionBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9951CE2AE2BAE500B65615 /* PromotionBannerView.swift */; }; 0C9951D32AE2DB0200B65615 /* PromotionViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9951D22AE2DB0200B65615 /* PromotionViewModelFactory.swift */; }; 0C9A7F992AFC9A2B00938CD0 /* SwapPriceDifferenceConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9A7F982AFC9A2B00938CD0 /* SwapPriceDifferenceConfig.swift */; }; + 0C9BCC582CBD6B4000CBE21B /* AssetsExchangeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9BCC572CBD6B4000CBE21B /* AssetsExchangeProtocol.swift */; }; 0C9C642D2A8CE30A004DC078 /* SystemAccountValidating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9C642C2A8CE30A004DC078 /* SystemAccountValidating.swift */; }; 0C9C64302A8D6779004DC078 /* StakingNPoolsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9C642F2A8D6779004DC078 /* StakingNPoolsPresenter.swift */; }; 0C9C64322A8D67A0004DC078 /* StakingNPoolsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9C64312A8D67A0004DC078 /* StakingNPoolsInteractor.swift */; }; @@ -499,7 +576,6 @@ 0CA719872B78AA71000B086E /* HydraRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA719862B78AA71000B086E /* HydraRouter.swift */; }; 0CA719892B78AACD000B086E /* HydraRouter+Call.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA719882B78AACD000B086E /* HydraRouter+Call.swift */; }; 0CA7198C2B78FEF9000B086E /* HydraQuoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA7198B2B78FEF9000B086E /* HydraQuoteTests.swift */; }; - 0CA7198E2B791811000B086E /* HydraReQuoteService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA7198D2B791811000B086E /* HydraReQuoteService.swift */; }; 0CA719902B79C92E000B086E /* HydraRoutesOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA7198F2B79C92E000B086E /* HydraRoutesOperationFactory.swift */; }; 0CA719922B79C987000B086E /* HydraOmnipool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA719912B79C987000B086E /* HydraOmnipool.swift */; }; 0CA719942B79CA01000B086E /* HydraOmnipool+Storages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA719932B79CA01000B086E /* HydraOmnipool+Storages.swift */; }; @@ -507,6 +583,33 @@ 0CA719982B79D033000B086E /* HydraRoutesResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA719972B79D033000B086E /* HydraRoutesResolver.swift */; }; 0CA7199A2B79DF5B000B086E /* HydraStableswapApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA719992B79DF5B000B086E /* HydraStableswapApi.swift */; }; 0CA7821C2B03D0A9003F562A /* ExtrinsicProcessor+AssetHubSwapMatching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA7821B2B03D0A9003F562A /* ExtrinsicProcessor+AssetHubSwapMatching.swift */; }; + 0CA7CEE22CE0BBE9004328F2 /* SubstrateEventsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA7CEE12CE0BBE9004328F2 /* SubstrateEventsRepository.swift */; }; + 0CA7CEE62CE0C23B004328F2 /* SystemPallet+Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA7CEE32CE0C23B004328F2 /* SystemPallet+Events.swift */; }; + 0CA7CEE72CE0C23B004328F2 /* SystemPallet+StoragePath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA7CEE42CE0C23B004328F2 /* SystemPallet+StoragePath.swift */; }; + 0CA7CEE92CE0C37B004328F2 /* SystemPallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA7CEE82CE0C37B004328F2 /* SystemPallet.swift */; }; + 0CA7CEEC2CE0CA57004328F2 /* NativeTokenDepositEventMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA7CEEB2CE0CA57004328F2 /* NativeTokenDepositEventMatcher.swift */; }; + 0CA7CEEE2CE0CA75004328F2 /* TokenDepositEventMatching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA7CEED2CE0CA75004328F2 /* TokenDepositEventMatching.swift */; }; + 0CA7CEF02CE0CC13004328F2 /* BalancesPallet+EventCodingPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA7CEEF2CE0CC13004328F2 /* BalancesPallet+EventCodingPath.swift */; }; + 0CA7CEF22CE0D14F004328F2 /* PalletAssetsTokenDepositEventMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA7CEF12CE0D14F004328F2 /* PalletAssetsTokenDepositEventMatcher.swift */; }; + 0CA7CEF42CE0D210004328F2 /* PalletAssets+EventCodingPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA7CEF32CE0D210004328F2 /* PalletAssets+EventCodingPath.swift */; }; + 0CA7CEF62CE0D35B004328F2 /* PalletAssets+Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA7CEF52CE0D35B004328F2 /* PalletAssets+Events.swift */; }; + 0CA7CEF82CE0D575004328F2 /* TokensPalletDepositEventMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA7CEF72CE0D575004328F2 /* TokensPalletDepositEventMatcher.swift */; }; + 0CA7CEFA2CE13126004328F2 /* TokenDepositEventMatcherFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA7CEF92CE13126004328F2 /* TokenDepositEventMatcherFactory.swift */; }; + 0CA81D4C2CE4FF8800166969 /* AssetExchangeGraphFeeSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA81D4B2CE4FF8800166969 /* AssetExchangeGraphFeeSupport.swift */; }; + 0CA81D502CE502B200166969 /* AssetExchangeFeeSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA81D4F2CE502B200166969 /* AssetExchangeFeeSupport.swift */; }; + 0CA81D522CE50BDB00166969 /* HydraExchangeFeeSupportFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA81D512CE50BDB00166969 /* HydraExchangeFeeSupportFetcher.swift */; }; + 0CA81D542CE5108A00166969 /* AssetHubExchangeFeeSupportFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA81D532CE5108A00166969 /* AssetHubExchangeFeeSupportFetcher.swift */; }; + 0CA81D572CE5AF2600166969 /* AssetExchangeFeeSupportFetchersProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA81D562CE5AF2600166969 /* AssetExchangeFeeSupportFetchersProvider.swift */; }; + 0CA81D592CE60AA700166969 /* AssetExchangeEdgeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA81D582CE60AA700166969 /* AssetExchangeEdgeType.swift */; }; + 0CA8AD562CE06FA000ED9746 /* XcmTransactService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA8AD552CE06FA000ED9746 /* XcmTransactService.swift */; }; + 0CA8AD5A2CE0789100ED9746 /* WalletRemoteSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA8AD592CE0789100ED9746 /* WalletRemoteSubscription.swift */; }; + 0CA8AD622CE08FEE00ED9746 /* BlockEventsQueryFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA8AD5B2CE08FEE00ED9746 /* BlockEventsQueryFactory.swift */; }; + 0CA8AD632CE08FEE00ED9746 /* ExtrinsicStatusService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA8AD5D2CE08FEE00ED9746 /* ExtrinsicStatusService.swift */; }; + 0CA8AD642CE08FEE00ED9746 /* SubstrateExtrinsicEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA8AD5F2CE08FEE00ED9746 /* SubstrateExtrinsicEvents.swift */; }; + 0CA8AD652CE08FEE00ED9746 /* ExtrinsicSubmissionMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA8AD5E2CE08FEE00ED9746 /* ExtrinsicSubmissionMonitor.swift */; }; + 0CA8AD662CE08FEE00ED9746 /* ExtrinsicEventsMatching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA8AD5C2CE08FEE00ED9746 /* ExtrinsicEventsMatching.swift */; }; + 0CA8AD672CE08FEE00ED9746 /* SubstrateExtrinsicStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA8AD602CE08FEE00ED9746 /* SubstrateExtrinsicStatus.swift */; }; + 0CA8AD692CE0904D00ED9746 /* XcmDepositMonitoringService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA8AD682CE0904D00ED9746 /* XcmDepositMonitoringService.swift */; }; 0CA957222B6A507A009AD757 /* HydraSwapParamsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA957212B6A507A009AD757 /* HydraSwapParamsService.swift */; }; 0CA957252B6A566B009AD757 /* HydraDx+Call.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA957242B6A566B009AD757 /* HydraDx+Call.swift */; }; 0CAB7D9F2C46B05F0070CE4D /* PoolStakingRecommendingValidationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAB7D9E2C46B05F0070CE4D /* PoolStakingRecommendingValidationFactory.swift */; }; @@ -554,6 +657,10 @@ 0CB313792C0F499B00353724 /* CloudBackupSetupPasswordFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB313782C0F499B00353724 /* CloudBackupSetupPasswordFlow.swift */; }; 0CB313862C12BBBD00353724 /* CloudBackupEnablePasswordInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB313852C12BBBD00353724 /* CloudBackupEnablePasswordInteractor.swift */; }; 0CB313882C12C0FC00353724 /* CloudBackupEnablePasswordWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB313872C12C0FC00353724 /* CloudBackupEnablePasswordWireframe.swift */; }; + 0CB433462CC0C3F200F2CB59 /* CrosschainAssetsExchangeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB433452CC0C3F200F2CB59 /* CrosschainAssetsExchangeProvider.swift */; }; + 0CB433482CC0F9EF00F2CB59 /* AssetsHubExchange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB433472CC0F9EF00F2CB59 /* AssetsHubExchange.swift */; }; + 0CB4334C2CC10C3C00F2CB59 /* AssetsHydraExchangeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB4334B2CC10C3C00F2CB59 /* AssetsHydraExchangeProvider.swift */; }; + 0CB4334E2CC1196400F2CB59 /* WeightableEdge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB4334D2CC1196400F2CB59 /* WeightableEdge.swift */; }; 0CB64E5A2AFE9947008F268F /* GetTokenOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB64E592AFE9947008F268F /* GetTokenOperation.swift */; }; 0CB64E5C2B009DA9008F268F /* GetTokenOptionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB64E5B2B009DA9008F268F /* GetTokenOptionsModel.swift */; }; 0CB64E5E2B00AA8F008F268F /* GetTokenOptionsResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB64E5D2B00AA8F008F268F /* GetTokenOptionsResult.swift */; }; @@ -563,6 +670,16 @@ 0CB64E672B01E174008F268F /* TransferNetworkSelectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB64E662B01E174008F268F /* TransferNetworkSelectionViewModel.swift */; }; 0CB64E692B01F798008F268F /* TransferSetupOriginSelectionWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB64E682B01F798008F268F /* TransferSetupOriginSelectionWireframe.swift */; }; 0CB6B2842C57857C00FFE475 /* TriangularedButton+Title.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6B2832C57857C00FFE475 /* TriangularedButton+Title.swift */; }; + 0CBABFE72CED31690047F29E /* CrosschainExchangeMetaOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBABFE62CED31690047F29E /* CrosschainExchangeMetaOperation.swift */; }; + 0CBABFE92CED31C80047F29E /* AssetHubExchangeMetaOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBABFE82CED31C80047F29E /* AssetHubExchangeMetaOperation.swift */; }; + 0CBABFEB2CED37E30047F29E /* AssetExchangeQuote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBABFEA2CED37E30047F29E /* AssetExchangeQuote.swift */; }; + 0CBABFED2CED438A0047F29E /* AssetExchangeOperationPrototype.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBABFEC2CED438A0047F29E /* AssetExchangeOperationPrototype.swift */; }; + 0CBABFEF2CED46200047F29E /* HydraExchangeOperationPrototype.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBABFEE2CED46200047F29E /* HydraExchangeOperationPrototype.swift */; }; + 0CBABFF12CED46C90047F29E /* AssetHubExchangeOperationPrototype.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBABFF02CED46C90047F29E /* AssetHubExchangeOperationPrototype.swift */; }; + 0CBABFF32CED47D10047F29E /* CrosschainExchangeOperationPrototype.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBABFF22CED47D10047F29E /* CrosschainExchangeOperationPrototype.swift */; }; + 0CBABFF42CEE4D590047F29E /* ChainModelFetchError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCCDF8B2B67BCA000473D42 /* ChainModelFetchError.swift */; }; + 0CBABFF62CEEACB60047F29E /* RoundedButton+Set.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBABFF52CEEACB60047F29E /* RoundedButton+Set.swift */; }; + 0CBABFF92CEF33BE0047F29E /* SwapExecutionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBABFF82CEF33BE0047F29E /* SwapExecutionDetailsView.swift */; }; 0CBC29C62A421B5000F7B1F7 /* StakingMainWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC29C52A421B5000F7B1F7 /* StakingMainWireframe.swift */; }; 0CBC29C82A42AE2F00F7B1F7 /* StakingDashboardBuilderResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC29C72A42AE2F00F7B1F7 /* StakingDashboardBuilderResult.swift */; }; 0CBD23F822E358320E328F2D /* GovernanceTracksSettingsViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 442FCD700DEAFD358A48F35D /* GovernanceTracksSettingsViewLayout.swift */; }; @@ -592,6 +709,9 @@ 0CCA245D2AC6918800AEF23D /* XcmV3Junction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCA245C2AC6918800AEF23D /* XcmV3Junction.swift */; }; 0CCA245F2AC6974200AEF23D /* XcmV3Multilocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCA245E2AC6974200AEF23D /* XcmV3Multilocation.swift */; }; 0CCA24652AC6B51200AEF23D /* AssetHubSwapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCA24642AC6B51200AEF23D /* AssetHubSwapTests.swift */; }; + 0CCA70732CD0A4FD0082A9C8 /* CrosschainExchangeAtomicOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCA70722CD0A4FD0082A9C8 /* CrosschainExchangeAtomicOperation.swift */; }; + 0CCA70752CD0B2860082A9C8 /* MetaAccountModel+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCA70742CD0B2860082A9C8 /* MetaAccountModel+Async.swift */; }; + 0CCA70782CD0B4220082A9C8 /* SigningWrapperFactory+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCA70772CD0B4220082A9C8 /* SigningWrapperFactory+Async.swift */; }; 0CCCDF742B62AA3400473D42 /* ParaStkPreferredCollatorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCCDF732B62AA3400473D42 /* ParaStkPreferredCollatorFactory.swift */; }; 0CCCDF762B64B80500473D42 /* StorageKeysOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCCDF752B64B80500473D42 /* StorageKeysOperationFactory.swift */; }; 0CCCDF7E2B64BE5300473D42 /* HydraDx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCCDF7D2B64BE5300473D42 /* HydraDx.swift */; }; @@ -649,13 +769,10 @@ 0CCE3AAC2BF4762000D55F03 /* CloudBackupDiff+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCE3AAB2BF4762000D55F03 /* CloudBackupDiff+Helper.swift */; }; 0CCE3AAF2BF5BE4300D55F03 /* IdentityProxyFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCE3AAE2BF5BE4300D55F03 /* IdentityProxyFactory.swift */; }; 0CD1F4D100ED82D137AB9834 /* ParaStkStakeSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2F1EEBF48485F02BF690A4 /* ParaStkStakeSetupViewController.swift */; }; - 0CD352932ACAD7A500B3E446 /* AssetHubExtrinsicService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD352922ACAD7A500B3E446 /* AssetHubExtrinsicService.swift */; }; 0CD352952ACAF59900B3E446 /* BigRational.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD352942ACAF59900B3E446 /* BigRational.swift */; }; - 0CD352972ACAFADA00B3E446 /* AssetConversionOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD352962ACAFADA00B3E446 /* AssetConversionOperationFactory.swift */; }; + 0CD352972ACAFADA00B3E446 /* AssetConversionOperationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD352962ACAFADA00B3E446 /* AssetConversionOperationError.swift */; }; 0CD352982ACB01FD00B3E446 /* AccountGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845B822426EFE03E00D25C72 /* AccountGenerator.swift */; }; 0CD3529B2ACD3E4300B3E446 /* PalletAssets+Call.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD3529A2ACD3E4300B3E446 /* PalletAssets+Call.swift */; }; - 0CD3A67C2AEAA3B90059BBEC /* AssetConversionFeeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD3A67B2AEAA3B90059BBEC /* AssetConversionFeeService.swift */; }; - 0CD3A67E2AEAAB670059BBEC /* AssetHubFeeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD3A67D2AEAAB670059BBEC /* AssetHubFeeService.swift */; }; 0CD3A6802AEAC3C90059BBEC /* CompoundOperationWrapper+Add.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD3A67F2AEAC3C90059BBEC /* CompoundOperationWrapper+Add.swift */; }; 0CD54E4B2BCE306C007B58E7 /* CloudBackupCryptoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD54E4A2BCE306B007B58E7 /* CloudBackupCryptoManager.swift */; }; 0CD54E4D2BCE3B89007B58E7 /* CloudBackupSecretsImporting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD54E4C2BCE3B89007B58E7 /* CloudBackupSecretsImporting.swift */; }; @@ -716,9 +833,13 @@ 0CE933E22C07671A000F3EFE /* CloudBackupRemindPresentationResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE933E12C07671A000F3EFE /* CloudBackupRemindPresentationResult.swift */; }; 0CE933E42C07C225000F3EFE /* WalletCreationRequestFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE933E32C07C225000F3EFE /* WalletCreationRequestFactory.swift */; }; 0CE933E62C07DBE1000F3EFE /* WalletNameChanged.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE933E52C07DBE1000F3EFE /* WalletNameChanged.swift */; }; + 0CE94FF02D03AF9800E31D0C /* SwapPreferredFeeAssetModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE94FEF2D03AF9800E31D0C /* SwapPreferredFeeAssetModel.swift */; }; + 0CE94FF22D046A8700E31D0C /* AssetExchangeFeePayerMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE94FF12D046A8700E31D0C /* AssetExchangeFeePayerMatcher.swift */; }; + 0CE94FF42D04969100E31D0C /* SwapInterEDNotMet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE94FF32D04969100E31D0C /* SwapInterEDNotMet.swift */; }; + 0CE94FF62D04A3C100E31D0C /* AssetsExchangePathCostEstimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE94FF52D04A3C100E31D0C /* AssetsExchangePathCostEstimator.swift */; }; + 0CE94FF82D050EE300E31D0C /* AssetExchangeOperationPrototypeFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE94FF72D050EE300E31D0C /* AssetExchangeOperationPrototypeFactory.swift */; }; 0CEAEBBE2BEDDD230019778F /* CloudBackupSettingsViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEAEBBD2BEDDD230019778F /* CloudBackupSettingsViewModelFactory.swift */; }; 0CEB4ED12AF14B8B0048FD84 /* SubmoduleNavigationStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEB4ED02AF14B8B0048FD84 /* SubmoduleNavigationStrategy.swift */; }; - 0CEB4ED32AF1689D0048FD84 /* AssetConversionAggregationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEB4ED22AF1689D0048FD84 /* AssetConversionAggregationFactory.swift */; }; 0CEB4ED52AF20EB90048FD84 /* CancellableCallHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEB4ED42AF20EB90048FD84 /* CancellableCallHelper.swift */; }; 0CEB4ED92AF371EF0048FD84 /* AssetConversionTxPayment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEB4ED82AF371EF0048FD84 /* AssetConversionTxPayment.swift */; }; 0CEB6B2D2CA4121300609DC2 /* GovTreasurySpentRemoteHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEB6B2C2CA4121300609DC2 /* GovTreasurySpentRemoteHandler.swift */; }; @@ -746,8 +867,14 @@ 0CF193D32A84FC7C003F12F6 /* SelectedStakingViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF193D22A84FC7C003F12F6 /* SelectedStakingViewModelFactory.swift */; }; 0CF193D52A861926003F12F6 /* PredefinedTimeShortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF193D42A861925003F12F6 /* PredefinedTimeShortcut.swift */; }; 0CF193D72A861D7E003F12F6 /* StartStakingInfoConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF193D62A861D7E003F12F6 /* StartStakingInfoConstants.swift */; }; + 0CF3A6C92CE9423000F93C49 /* RouteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF3A6C82CE9423000F93C49 /* RouteView.swift */; }; + 0CF3A6CB2CE949ED00F93C49 /* SwapRouteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF3A6CA2CE949ED00F93C49 /* SwapRouteView.swift */; }; + 0CF4ADF92CEBF71A009C51FA /* AssetExchangeMetaOperationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF4ADF82CEBF71A009C51FA /* AssetExchangeMetaOperationProtocol.swift */; }; + 0CF4ADFD2CEC0A54009C51FA /* HydraExchangeMetaOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF4ADFC2CEC0A54009C51FA /* HydraExchangeMetaOperation.swift */; }; 0CF4ADFF2CECACF9009C51FA /* PolkadotRewardParamsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF4ADFE2CECACF9009C51FA /* PolkadotRewardParamsService.swift */; }; 0CF4AE0B2CECDCF1009C51FA /* PolkadotRewardEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF4AE0A2CECDCF1009C51FA /* PolkadotRewardEngine.swift */; }; + 0CF5F68A2CC7AF84007BAAC5 /* AssetExchangeGraphPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF5F6892CC7AF84007BAAC5 /* AssetExchangeGraphPath.swift */; }; + 0CF5F68C2CC7B1B6007BAAC5 /* AssetsExchageGraphReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF5F68B2CC7B1B6007BAAC5 /* AssetsExchageGraphReachability.swift */; }; 0CF609332B9438BD00DD4DB3 /* OperatingCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF609322B9438BD00DD4DB3 /* OperatingCall.swift */; }; 0CF609352B9442F400DD4DB3 /* PushNotificationsServiceFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF609342B9442F400DD4DB3 /* PushNotificationsServiceFacade.swift */; }; 0CF692892C20E2B1000FC395 /* BackupManualWarningPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF692882C20E2B1000FC395 /* BackupManualWarningPresentable.swift */; }; @@ -757,12 +884,21 @@ 0CF692922C2412AB000FC395 /* BackupMnemonicAccountType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF692912C2412AB000FC395 /* BackupMnemonicAccountType.swift */; }; 0CF8174A2BBD0E8B00CB9183 /* WalletNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF817492BBD0E8B00CB9183 /* WalletNameView.swift */; }; 0CF8176C2BC3B57100CB9183 /* CloudBackupErrorPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF8176B2BC3B57100CB9183 /* CloudBackupErrorPresentable.swift */; }; + 0CF976722CEFF01D001D2801 /* SwapExecutionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF976712CEFF01D001D2801 /* SwapExecutionModel.swift */; }; + 0CF976752CF082BF001D2801 /* SwapExecutionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF976742CF082BF001D2801 /* SwapExecutionViewModel.swift */; }; + 0CF976772CF08EF9001D2801 /* OperationExecutionProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF976762CF08EF9001D2801 /* OperationExecutionProgressView.swift */; }; + 0CF976792CF09076001D2801 /* SwapExecutionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF976782CF09076001D2801 /* SwapExecutionView.swift */; }; + 0CF9767B2CF09B86001D2801 /* SwapExecutionViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF9767A2CF09B86001D2801 /* SwapExecutionViewModelFactory.swift */; }; + 0CF9767D2CF1274F001D2801 /* AssetExchangeMetaOperationLabel+Display.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF9767C2CF1274F001D2801 /* AssetExchangeMetaOperationLabel+Display.swift */; }; + 0CF9767F2CF1293E001D2801 /* SwapExecutionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF9767E2CF1293E001D2801 /* SwapExecutionState.swift */; }; + 0CF976812CFD04F8001D2801 /* AssetsExchangeStateMediator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF976802CFD04F8001D2801 /* AssetsExchangeStateMediator.swift */; }; 0CFA16132B0CD8A0007AF885 /* GovSpentAmountExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFA16122B0CD8A0007AF885 /* GovSpentAmountExtractor.swift */; }; 0CFA16162B0CE51E007AF885 /* GovSpentAmountBatchHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFA16152B0CE51E007AF885 /* GovSpentAmountBatchHandler.swift */; }; 0CFA16192B0CE709007AF885 /* UtilityPallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFA16182B0CE709007AF885 /* UtilityPallet.swift */; }; 0CFA161C2B0CE851007AF885 /* Utility+Calls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFA161B2B0CE851007AF885 /* Utility+Calls.swift */; }; 0CFA161E2B0CED07007AF885 /* GovTreasurySpentLocalHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFA161D2B0CED07007AF885 /* GovTreasurySpentLocalHandler.swift */; }; 0CFA16202B0CEF31007AF885 /* GovTreasuryApproveHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFA161F2B0CEF31007AF885 /* GovTreasuryApproveHandler.swift */; }; + 0CFFB9D32D11A67C00172E8C /* XcmTokensArrivalDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFFB9D22D11A67C00172E8C /* XcmTokensArrivalDetector.swift */; }; 0D5245ED354CC52A842C85A0 /* TransferConfirmViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD8B98AB03AAF06AA891695 /* TransferConfirmViewLayout.swift */; }; 0D8213272889988B78188D9A /* DAppWalletAuthInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 337EC62037D657258BCBC02F /* DAppWalletAuthInteractor.swift */; }; 0DACB56C0BDD4C984FE3C15C /* AssetReceiveWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1179A25C22AF0875A1ADCD /* AssetReceiveWireframe.swift */; }; @@ -783,6 +919,7 @@ 106CC4BFC48B6BFFF31434A9 /* LedgerWalletConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BC1402B34E341312ABB378 /* LedgerWalletConfirmPresenter.swift */; }; 109512489F8CB32C2430808E /* ManualBackupKeyListViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29CD41F03830F4967EF06F91 /* ManualBackupKeyListViewLayout.swift */; }; 10DD08A4E459DB4757809318 /* ManualBackupWalletListWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08D63836373E5AB433A04596 /* ManualBackupWalletListWireframe.swift */; }; + 1116E062DCFC5E1353B9B4F8 /* SwapRouteDetailsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2BD7CE1EF80FCD3569BD44 /* SwapRouteDetailsProtocols.swift */; }; 1180349875F35B4D4DD88A4C /* StakingTypeViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4752D80077E85563CF3AD5D /* StakingTypeViewFactory.swift */; }; 11C6F4CD5B167DE4E9E7F654 /* DAppPhishingWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518305BB475DE40E94DCBD5D /* DAppPhishingWireframe.swift */; }; 1232A714A96F937330FC0AFA /* GovernanceDelegateConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B718CE9C51158F87D37894BB /* GovernanceDelegateConfirmViewFactory.swift */; }; @@ -1087,10 +1224,9 @@ 2D8EFB2A2C08892300866F90 /* MnemonicViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8EFB292C08892300866F90 /* MnemonicViewModelFactory.swift */; }; 2D8FF9D02C9AA54000089F53 /* BaseReferendumVoteConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8FF9CF2C9AA54000089F53 /* BaseReferendumVoteConfirmPresenter.swift */; }; 2D8FF9D32C9BD67500089F53 /* SwipeGovAlertPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8FF9D22C9BD67500089F53 /* SwipeGovAlertPresentable.swift */; }; - 2D95DF5B2C6F7D83009BB063 /* HydraExtrinsicAssetsCustomFeeEstimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D95DF5A2C6F7D83009BB063 /* HydraExtrinsicAssetsCustomFeeEstimator.swift */; }; 2D95DF5D2C6F9129009BB063 /* HydraExtrinsicFeeInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D95DF5C2C6F9129009BB063 /* HydraExtrinsicFeeInstaller.swift */; }; 2D95DF5F2C6FAFF6009BB063 /* ExtrinsicFeeEstimatingWrapperFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D95DF5E2C6FAFF6009BB063 /* ExtrinsicFeeEstimatingWrapperFactory.swift */; }; - 2D95DF612C74B79A009BB063 /* HydraFlowStateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D95DF602C74B79A009BB063 /* HydraFlowStateStore.swift */; }; + 2D95DF612C74B79A009BB063 /* AssetConversionFeeSharedStateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D95DF602C74B79A009BB063 /* AssetConversionFeeSharedStateStore.swift */; }; 2D95DF642C765E1B009BB063 /* mercuryoWidget.html in Resources */ = {isa = PBXBuildFile; fileRef = 2D95DF632C765E1B009BB063 /* mercuryoWidget.html */; }; 2DA85A652C7D9B8B00591900 /* CardTopUpTransferSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA85A642C7D9B8B00591900 /* CardTopUpTransferSetupViewController.swift */; }; 2DA85A672C7DD75300591900 /* CardTopUpTransferSetupViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA85A662C7DD75300591900 /* CardTopUpTransferSetupViewLayout.swift */; }; @@ -1211,6 +1347,7 @@ 3B7EEC888C19F954B5EB1012 /* OnChainTransferSetupWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9871C4FF3B05F6055AF82F14 /* OnChainTransferSetupWireframe.swift */; }; 3B87871B471FF8BA84DC7910 /* ParaStkYieldBoostStopWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 285212895DBAB0098F302DF9 /* ParaStkYieldBoostStopWireframe.swift */; }; 3BFD635E852E4D395025BEE8 /* ParaStkCollatorsSearchViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C602661DE4D6CAC482AF721 /* ParaStkCollatorsSearchViewFactory.swift */; }; + 3C219894A8E0598486B2D285 /* SwapFeeDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0FC3C19F3DEE499283E6468 /* SwapFeeDetailsViewController.swift */; }; 3C3C98149DA3BDE3CE692F3C /* NominationPoolBondMoreConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA576BBCED647C22E93F0202 /* NominationPoolBondMoreConfirmWireframe.swift */; }; 3C6C738F4AB7AC6FEA290D59 /* WalletsChoosePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 525813EB768E636A397C00BB /* WalletsChoosePresenter.swift */; }; 3CA86739CB09801714B194BD /* PurchaseWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C52C6CD7112BF0E1E3A98CE /* PurchaseWireframe.swift */; }; @@ -1415,11 +1552,13 @@ 68A888C7BAC1710AC564F777 /* NetworkManageNodeProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A493DC34C60C7115B4A37C /* NetworkManageNodeProtocols.swift */; }; 68AA6A84FD2403B059516244 /* StakingDashboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAD69608028E9FAE0F8E58D /* StakingDashboardViewController.swift */; }; 68BE7C4037E8A5C245CDFF0D /* ParaStkYieldBoostStopInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376F2C0E94A454FBBBB903F6 /* ParaStkYieldBoostStopInteractor.swift */; }; + 68CF0931493D315D0B315648 /* SwapFeeDetailsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A6CDEC83D8A51FD89673C31 /* SwapFeeDetailsProtocols.swift */; }; 68E01098C46BFEA570B95ED1 /* ProxiedsUpdatePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9EB3AE5724612E7F1BBA7CF /* ProxiedsUpdatePresenter.swift */; }; 6919F96BC31D29A3FD1557AE /* SwipeGovVotingConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D664781927E12007A28A971 /* SwipeGovVotingConfirmWireframe.swift */; }; 692114AAB4AC5E1C58A21FCD /* GovernanceTracksSettingsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40C01C724F616836BACFBE8B /* GovernanceTracksSettingsInteractor.swift */; }; 6927967A758F8FB354C62F49 /* CloudBackupRemindViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F65C2DCBA74EBD7DE89CF02C /* CloudBackupRemindViewController.swift */; }; 694FC2B5A9C40461F74763B5 /* AssetListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = D529B9CA8D6E2E6F375FC260 /* AssetListProtocols.swift */; }; + 6962A0D96A2F40C7D50FFFA7 /* SwapRouteDetailsViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDD7BBAB8B408BC84477468 /* SwapRouteDetailsViewLayout.swift */; }; 6A2B6DF2D6AE912D5FA62D94 /* ParitySignerAddressesWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8B10C37813EFE7D7663605E /* ParitySignerAddressesWireframe.swift */; }; 6A2B815B3AA8B31CF77023A7 /* NetworkNodeViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5CE1E167B357490396AAC5 /* NetworkNodeViewFactory.swift */; }; 6A776A53FEC109C875113B38 /* ParaStkCollatorFiltersViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED010772D7CE0450BDF30707 /* ParaStkCollatorFiltersViewFactory.swift */; }; @@ -1497,7 +1636,6 @@ 77171CAA2A98BC420032B387 /* NominationPoolDataValidatorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77171CA92A98BC420032B387 /* NominationPoolDataValidatorFactory.swift */; }; 7719018C2AE0E70B00D9C918 /* SwapDataValidatorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719018B2AE0E70B00D9C918 /* SwapDataValidatorFactory.swift */; }; 7719018E2AE0E71F00D9C918 /* SwapErrorPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719018D2AE0E71F00D9C918 /* SwapErrorPresentable.swift */; }; - 771901902AE2424B00D9C918 /* SwapsValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719018F2AE2424B00D9C918 /* SwapsValidationTests.swift */; }; 7719019B2AE670AE00D9C918 /* ShortTextInfoPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719019A2AE670AE00D9C918 /* ShortTextInfoPresentable.swift */; }; 7719019D2AE6996600D9C918 /* SwapPairView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719019C2AE6996600D9C918 /* SwapPairView.swift */; }; 7719019F2AE6C9DC00D9C918 /* SwapElementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719019E2AE6C9DC00D9C918 /* SwapElementView.swift */; }; @@ -1505,7 +1643,7 @@ 771901A42AE7E48800D9C918 /* SwapBaseProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901A32AE7E48800D9C918 /* SwapBaseProtocols.swift */; }; 771901A62AE8FF7E00D9C918 /* SwapInfoViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901A52AE8FF7E00D9C918 /* SwapInfoViewCell.swift */; }; 771901A82AE8FF9F00D9C918 /* SwapNetworkFeeViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901A72AE8FF9F00D9C918 /* SwapNetworkFeeViewCell.swift */; }; - 771901AB2AE9581400D9C918 /* SwapConfirmViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901AA2AE9581400D9C918 /* SwapConfirmViewModelFactory.swift */; }; + 771901AB2AE9581400D9C918 /* SwapDetailsViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901AA2AE9581400D9C918 /* SwapDetailsViewModelFactory.swift */; }; 771901B02AE97DA500D9C918 /* SwapConfirmViewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901AF2AE97DA500D9C918 /* SwapConfirmViewModels.swift */; }; 771901B22AEA3E5100D9C918 /* ShortTextInfoPresentableExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901B12AEA3E5100D9C918 /* ShortTextInfoPresentableExtensions.swift */; }; 77204EA42A1CD59100BBDE4A /* GenericBorderedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77204EA32A1CD59100BBDE4A /* GenericBorderedView.swift */; }; @@ -1877,6 +2015,7 @@ 7B0CCB3AAE9D4675CE6F0E6C /* ExportWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6B6B5BC6580FB580D3CF2AE /* ExportWireframe.swift */; }; 7B72E35164417147B889A20F /* Pods_novawalletAll_novawallet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CB3C834CCCD632B6CB30BEB /* Pods_novawalletAll_novawallet.framework */; }; 7BD09D3022967C4D90AB4693 /* DAppOperationConfirmViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859E0EF774DF0D498FEF8FCB /* DAppOperationConfirmViewLayout.swift */; }; + 7BF57D772C522E7728585166 /* SwapExecutionWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1285B676EA226704C14DDB /* SwapExecutionWireframe.swift */; }; 7C0135CA49EF6B535030643E /* ParaStkYieldBoostSetupPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C9EEC5FDEA7DAE42F2880C7 /* ParaStkYieldBoostSetupPresenter.swift */; }; 7C42FEDE4DFA920154010F83 /* BackupAttentionProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67A4A418E0DEB44B713C7B8 /* BackupAttentionProtocols.swift */; }; 7C4CB158ED48716626780F40 /* LedgerPerformOperationInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4809C2DB0C15A9B2890C1AC6 /* LedgerPerformOperationInteractor.swift */; }; @@ -1917,6 +2056,7 @@ 8329367F06C83BA7A0B12A34 /* CommonDelegationTracksPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7235097E09C94005B091B4 /* CommonDelegationTracksPresenter.swift */; }; 832B616B89972C96D98023DB /* ChangeWatchOnlyViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E54289A8A9354D5DDA15F0E1 /* ChangeWatchOnlyViewFactory.swift */; }; 835AE15B1F58CB87F775272F /* DelegateVotedReferendaPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5544826542EC3A92FCB2B1D7 /* DelegateVotedReferendaPresenter.swift */; }; + 8364E4802CC617E523EB87A7 /* SwapFeeDetailsViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FBBC5F8D082B58B4FDFABD7 /* SwapFeeDetailsViewLayout.swift */; }; 838D584B803A5A7BCBAD9395 /* StartStakingConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28FAB72B50D8C736AF67A39 /* StartStakingConfirmProtocols.swift */; }; 83A98B972B3EA69B357E5002 /* ControllerAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B556386CEEB76C95ED59897 /* ControllerAccountViewController.swift */; }; 83DCE6BEEAD957D9C7588DB5 /* ParitySignerTxScanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 817E84903F4A2CD5333518DE /* ParitySignerTxScanViewController.swift */; }; @@ -2584,7 +2724,7 @@ 84540172292F907B00213402 /* BlurBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84540171292F907B00213402 /* BlurBackgroundView.swift */; }; 8454C21D2632A78900657DAD /* EventRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8454C21C2632A78900657DAD /* EventRecord.swift */; }; 8454C2652632B0EF00657DAD /* EventCodingPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8454C2642632B0EF00657DAD /* EventCodingPath.swift */; }; - 8454C26A2632B8CE00657DAD /* BalanceDepositEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8454C2692632B8CE00657DAD /* BalanceDepositEvent.swift */; }; + 8454C26A2632B8CE00657DAD /* BalancesPallet+Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8454C2692632B8CE00657DAD /* BalancesPallet+Events.swift */; }; 8454C26F2632BBAA00657DAD /* ExtrinsicProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8454C26E2632BBAA00657DAD /* ExtrinsicProcessing.swift */; }; 8454C2832632FC2500657DAD /* ExtrinsicProcessingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8454C2822632FC2500657DAD /* ExtrinsicProcessingTests.swift */; }; 845532D02684690D00C2645D /* ParachainSlotLease.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845532CF2684690D00C2645D /* ParachainSlotLease.swift */; }; @@ -2930,7 +3070,6 @@ 8479F31426CD9A0E005D8D24 /* ChainRegistryIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8479F31326CD9A0E005D8D24 /* ChainRegistryIntegrationTests.swift */; }; 847A258E29B5D25E0054F90C /* GovJsonLocalStorageSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847A258D29B5D25E0054F90C /* GovJsonLocalStorageSubscriber.swift */; }; 847A259029B5D2710054F90C /* GovJsonLocalStorageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847A258F29B5D2710054F90C /* GovJsonLocalStorageHandler.swift */; }; - 847A25B928D7BB1F006AC9F5 /* BalancesTransferEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847A25B828D7BB1F006AC9F5 /* BalancesTransferEvent.swift */; }; 847A25BB28D7BB92006AC9F5 /* ExtrinsicProcessor+Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847A25BA28D7BB92006AC9F5 /* ExtrinsicProcessor+Events.swift */; }; 847A25BD28D7C0E7006AC9F5 /* TokenTransferedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847A25BC28D7C0E7006AC9F5 /* TokenTransferedEvent.swift */; }; 847A25BF28D7C2A2006AC9F5 /* AccountIdCodingWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847A25BE28D7C2A2006AC9F5 /* AccountIdCodingWrapper.swift */; }; @@ -3470,7 +3609,6 @@ 84B7C719289BFA79001A3566 /* DAppSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C6A4289BFA79001A3566 /* DAppSearchTests.swift */; }; 84B7C71A289BFA79001A3566 /* DAppListGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C6A6289BFA79001A3566 /* DAppListGenerator.swift */; }; 84B7C71B289BFA79001A3566 /* DAppListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C6A8289BFA79001A3566 /* DAppListTests.swift */; }; - 84B7C71C289BFA79001A3566 /* DAppTxDetailsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C6AA289BFA79001A3566 /* DAppTxDetailsTests.swift */; }; 84B7C71D289BFA79001A3566 /* MnemonicTextNormalizerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C6AC289BFA79001A3566 /* MnemonicTextNormalizerTest.swift */; }; 84B7C71E289BFA79001A3566 /* AccountImportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C6AD289BFA79001A3566 /* AccountImportTests.swift */; }; 84B7C71F289BFA79001A3566 /* CrowdloanContributionConfirmTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C6B0289BFA79001A3566 /* CrowdloanContributionConfirmTests.swift */; }; @@ -3510,7 +3648,6 @@ 84B7C745289BFA79001A3566 /* AssetsManageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C702289BFA79001A3566 /* AssetsManageTests.swift */; }; 84B7C746289BFA79001A3566 /* WalletHistoryFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C704289BFA79001A3566 /* WalletHistoryFilterTests.swift */; }; 84B7C747289BFA79001A3566 /* AccountManagementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C706289BFA79001A3566 /* AccountManagementTests.swift */; }; - 84B7C748289BFA79001A3566 /* WalletListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C709289BFA79001A3566 /* WalletListTests.swift */; }; 84B7C749289BFA79001A3566 /* ControllerAccountTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C70B289BFA79001A3566 /* ControllerAccountTests.swift */; }; 84B8AA7129F8E59300347A37 /* DAppInteractionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B8AA7029F8E59300347A37 /* DAppInteractionPresenter.swift */; }; 84B8AA7329F8EFC700347A37 /* DAppInteractionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B8AA7229F8EFC700347A37 /* DAppInteractionError.swift */; }; @@ -4071,7 +4208,6 @@ 84FBED072927B3B000FBEB83 /* EvmTransactionHistoryUpdaterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FBED062927B3B000FBEB83 /* EvmTransactionHistoryUpdaterFactory.swift */; }; 84FC190B29B7DB9F00BCCAA5 /* ExtrinsicServiceTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FC190A29B7DB9F00BCCAA5 /* ExtrinsicServiceTypes.swift */; }; 84FCCD98292E3610002D2D3D /* JSONRPCError+Evm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FCCD97292E3610002D2D3D /* JSONRPCError+Evm.swift */; }; - 84FD19FE27A3447E008E5E68 /* BalancesWithdrawEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FD19FD27A3447E008E5E68 /* BalancesWithdrawEvent.swift */; }; 84FD3DB12540C09800A234E3 /* TransactionHistoryMergeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FD3DB02540C09800A234E3 /* TransactionHistoryMergeManager.swift */; }; 84FD3DB52540ED0900A234E3 /* Block.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FD3DB42540ED0900A234E3 /* Block.swift */; }; 84FD3DB72540EF0700A234E3 /* TransactionSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FD3DB62540EF0700A234E3 /* TransactionSubscription.swift */; }; @@ -4394,6 +4530,7 @@ 8A2486E62C3915CB6D1FDED8 /* DelegationReferendumVotersInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A813E09FA3047357C7A75564 /* DelegationReferendumVotersInteractor.swift */; }; 8AEF593AFE8F59F7DC0A5753 /* CustomValidatorListInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 365CAE2753E7D5F9B9DB7D1F /* CustomValidatorListInteractor.swift */; }; 8B53466F5239B852F20F0C4E /* AssetOperationNetworkListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 891FBCE49A51A9120ED9F861 /* AssetOperationNetworkListViewController.swift */; }; + 8BB2AB5B4E97FAE0D27A4B60 /* SwapExecutionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501E16B2A68FEAEC039E8604 /* SwapExecutionViewLayout.swift */; }; 8C0C890FCCF0900DDE9CFD5F /* NotificationWalletListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD098B6DB870AFF6304CB542 /* NotificationWalletListProtocols.swift */; }; 8C68C4CFAF7CB9312C86D5B8 /* GovernanceDelegateSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFC1FCEA168E40235A1D3EA6 /* GovernanceDelegateSearchViewController.swift */; }; 8CDA490B390BFA261906F8FC /* CrowdloanContributionSetupViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23BC71941B91D3E372CDB11C /* CrowdloanContributionSetupViewLayout.swift */; }; @@ -4452,6 +4589,7 @@ 97F1D128D46B072D68940FC4 /* MoonbeamTermsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97F0F44AD48C7F3758A90D02 /* MoonbeamTermsViewFactory.swift */; }; 97FC0EC2B587AAB299FC75A1 /* OnboardingWalletReadyInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 641067C2CF2998FD70FE27BE /* OnboardingWalletReadyInteractor.swift */; }; 987813DA927669C364673B4E /* ParaStkCollatorInfoInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D93D6B6DB7BACFEA6F2738C /* ParaStkCollatorInfoInteractor.swift */; }; + 98815A40D614BCB2BEA01791 /* SwapExecutionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE605788A0F121ECFC71C30 /* SwapExecutionPresenter.swift */; }; 98DADEB52480817D191188C1 /* AssetListInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0100701AA69652CB91ACBD97 /* AssetListInteractor.swift */; }; 992678FD5D3F9D39FFC2BB53 /* LedgerTxConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEB918ED70D77D1832094A8A /* LedgerTxConfirmWireframe.swift */; }; 9942034DCB680824831B0AC1 /* AccountCreatePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBBEC474F607DD9F2A0F4FD /* AccountCreatePresenter.swift */; }; @@ -4662,6 +4800,7 @@ AEFA82BC4285117096BCBB16 /* StakingPayoutConfirmationInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBFDF844248CD43AAD13139F /* StakingPayoutConfirmationInteractor.swift */; }; AEFC6D6F2600D7CD000BD310 /* NetworkInfoViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEFC6D6E2600D7CD000BD310 /* NetworkInfoViewModelFactory.swift */; }; AF8193D9F818638254854232 /* StakingMainProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 470B64C45E547C25FCCCFC33 /* StakingMainProtocols.swift */; }; + AFFAC5B4085560356A9FDC7C /* SwapFeeDetailsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA18C9D87DC9E878D101CB91 /* SwapFeeDetailsViewFactory.swift */; }; B02EAF42C91E069FE6872EE0 /* SelectValidatorsConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0E02AA5D3EBA9B94950241 /* SelectValidatorsConfirmWireframe.swift */; }; B062216285AFF6AA1E1E78D3 /* GovernanceUnavailableTracksPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1768C3415D7D9CEEA8A8C700 /* GovernanceUnavailableTracksPresenter.swift */; }; B071927DF8DD5C3CA84494BA /* RecommendedValidatorListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F61D8973ADEB461DE2AD3E13 /* RecommendedValidatorListViewController.swift */; }; @@ -4808,6 +4947,7 @@ CF2F3A0F0999D6D054CD33D2 /* ReferendumSearchProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536D1CA47A5753B9E0389BEA /* ReferendumSearchProtocols.swift */; }; CFD1B4635D0ED2DC7DAD1EC1 /* CloudBackupRemindPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA24FD142810AEE259CC56C /* CloudBackupRemindPresenter.swift */; }; CFE7FE8F69D10E2B9E8DA791 /* GovernanceUnavailableTracksViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B0BF8DFAA80B405D4A5D891 /* GovernanceUnavailableTracksViewFactory.swift */; }; + D010C2D055F79587881692F3 /* SwapExecutionViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E1250905B8D8E175316B0A /* SwapExecutionViewFactory.swift */; }; D0A73CEDB6B3CF2DD4511963 /* NotificationsManagementViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7A434DA1BF2B133E9685A6 /* NotificationsManagementViewLayout.swift */; }; D0AD3C44BBFD6A9F9FDEC933 /* WalletConnectSessionDetailsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = C09D09E9ED7911D0BF763184 /* WalletConnectSessionDetailsWireframe.swift */; }; D11AEF58F47E3D59BC6D966D /* GovernanceTracksSettingsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81012A15E52FFDDB8608D86 /* GovernanceTracksSettingsProtocols.swift */; }; @@ -4815,10 +4955,12 @@ D153449DFFDC31C7CBB7E7EA /* OnboardingWalletReadyPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB5FCE21D2C05E8DDB55A78C /* OnboardingWalletReadyPresenter.swift */; }; D1C4208A89633395AF2FDB74 /* ReferendumVoteSetupViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A3105383F2825940D0105D5 /* ReferendumVoteSetupViewLayout.swift */; }; D1C6EABB48DC3EE254E5A095 /* CrowdloanContributionConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28F5B57A24265C36A5F19B78 /* CrowdloanContributionConfirmPresenter.swift */; }; + D1D2709811B89D89F8A08FAF /* SwapExecutionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D139E39F5ECE929FF724B8 /* SwapExecutionViewController.swift */; }; D260D31783A1DD5AB2DA5B91 /* StakingMoreOptionsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07D165A41A0F3F0EC1926175 /* StakingMoreOptionsProtocols.swift */; }; D264B2A8A516396051016CAB /* AssetReceivePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3934C46625930FA8D171D3E7 /* AssetReceivePresenter.swift */; }; D274117F06B12F955073D35B /* DelegationReferendumVotersViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7059B3F1E8DC94D36733B4C7 /* DelegationReferendumVotersViewLayout.swift */; }; D344C6DAC1F8BB6152BA8DD0 /* RecommendedValidatorListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C6573C52692E4A56E35FF9 /* RecommendedValidatorListProtocols.swift */; }; + D381A3467D7C07331DBEB68F /* SwapRouteDetailsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A3DA31DE8C8AD1D82D66DEF /* SwapRouteDetailsViewFactory.swift */; }; D39B1873B761440E4E0EA749 /* AdvancedExportPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EB218244A61B9743D9ACF69 /* AdvancedExportPresenter.swift */; }; D3B48F82A875E301D749AC0B /* StakingUnbondConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5674162035C7D9F226FA9964 /* StakingUnbondConfirmViewController.swift */; }; D3B74ED2525DE12423722DE2 /* AssetReceiveInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B243F7A096241F329224A18E /* AssetReceiveInteractor.swift */; }; @@ -4832,6 +4974,7 @@ D5F1C910930BA08E5F7681B2 /* StakingProxyManagementWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = A313F1654986753E341D7D39 /* StakingProxyManagementWireframe.swift */; }; D600448CB75095E6873E542F /* DAppTxDetailsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D383344BEDAEDC76A6BE2CE /* DAppTxDetailsProtocols.swift */; }; D6511F7C3E55197F82AB552C /* RecommendedValidatorListViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE4AF0849E32E5B9C72E2ABB /* RecommendedValidatorListViewFactory.swift */; }; + D655ED75A8D7BD4380FCF3A8 /* SwapExecutionInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8632A6D9689942DE89F174FA /* SwapExecutionInteractor.swift */; }; D6A2790F5C9E2AAA137E52CC /* InAppUpdatesPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DFDA05E0E68FE6D85E647C9 /* InAppUpdatesPresenter.swift */; }; D6B9C9345141D9463AF62C65 /* SwipeGovVotingListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7279B5EF846082A94136806F /* SwipeGovVotingListPresenter.swift */; }; D6D9D16440AB588F581AF5BA /* ParaStkYourCollatorsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6A6E9A6472158C37D3A62F5 /* ParaStkYourCollatorsPresenter.swift */; }; @@ -4934,6 +5077,7 @@ EA7BA01C6745ABE0E1AF7E1D /* SwapConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2FB715B933FB8E34A553A80 /* SwapConfirmViewController.swift */; }; EAAB9E53189BC6394C5900D2 /* GovernanceSelectTracksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6962C8E51EB317DE3AAE4BDF /* GovernanceSelectTracksViewController.swift */; }; EAAFB082E2BB0CA418714061 /* ReferendumFullDetailsViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57CDCB8CF3D8EBE5DA7A5A30 /* ReferendumFullDetailsViewLayout.swift */; }; + EB1102249D1DE711E723D7BA /* SwapFeeDetailsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17CDE3DFBC17377C17ED99F4 /* SwapFeeDetailsPresenter.swift */; }; EB11BF594D7E16A8885D47DD /* WalletConnectServiceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B193A261FDF933FE6C874B4E /* WalletConnectServiceFactory.swift */; }; EB20C6B406155664B981BA94 /* GovernanceYourDelegationsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46DF4E6D8DAF6913474DED5 /* GovernanceYourDelegationsInteractor.swift */; }; EB280B977471EA442D91395E /* SwapConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796859464B823B60746C5DE5 /* SwapConfirmProtocols.swift */; }; @@ -4961,6 +5105,7 @@ EEDDE41F8445C0CB2E99AFE4 /* ParaStkYieldBoostStartPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3089A0A7C992300CE839A050 /* ParaStkYieldBoostStartPresenter.swift */; }; EFEB65B229DB34B4B526003B /* ParaStkStakeConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CDC7A44F6B01FE389F34C3A /* ParaStkStakeConfirmInteractor.swift */; }; EFF8F905CE4E8A212FE79EE4 /* ParaStkYourCollatorsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5E099C1E3DC3730DD503BE /* ParaStkYourCollatorsViewController.swift */; }; + F007ED524D9B187C5159C5DA /* SwapRouteDetailsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399E91BE2E289FE301D7846A /* SwapRouteDetailsPresenter.swift */; }; F022F1444E0F75CCA42F4648 /* YourValidatorListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31780E84948D7FE632ECB02 /* YourValidatorListProtocols.swift */; }; F040165DFF9C8D7C5CC47581 /* AddDelegationProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C12FF06C3E4D642221EDCD /* AddDelegationProtocols.swift */; }; F041E02C4EE5E899B7F153B5 /* CloudBackupSettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18AFA9612CE9D32E08F6DBED /* CloudBackupSettingsPresenter.swift */; }; @@ -5232,9 +5377,21 @@ 0B62C2CBCFF1865A1CA0F1B4 /* LedgerNetworkSelectionProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerNetworkSelectionProtocols.swift; sourceTree = ""; }; 0BA85EE628D7029AC940DFA3 /* NominationPoolBondMoreBasePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreBasePresenter.swift; sourceTree = ""; }; 0C029E292CE4D2AB00649C28 /* SubstrateDataModel34.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel34.xcdatamodel; sourceTree = ""; }; + 0C0387502D066474000A2F24 /* AssetExchageUsdtConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchageUsdtConverter.swift; sourceTree = ""; }; + 0C03877B2D09A1AA000A2F24 /* MockAssetExchangePathCostEstimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAssetExchangePathCostEstimator.swift; sourceTree = ""; }; + 0C03877E2D0A1043000A2F24 /* SwapRouteDetailsItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapRouteDetailsItemView.swift; sourceTree = ""; }; + 0C0387802D0A1283000A2F24 /* AssetAmountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetAmountView.swift; sourceTree = ""; }; + 0C0387822D0A1878000A2F24 /* AssetAmountRouteItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetAmountRouteItemView.swift; sourceTree = ""; }; + 0C0387842D0A1C0A000A2F24 /* LabelRouteItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelRouteItemView.swift; sourceTree = ""; }; + 0C0387862D0A24E6000A2F24 /* SwapRouteDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapRouteDetailsView.swift; sourceTree = ""; }; + 0C0387892D0A2785000A2F24 /* SwapRouteDetailsViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapRouteDetailsViewModelFactory.swift; sourceTree = ""; }; + 0C03878B2D0B2686000A2F24 /* LinePatternView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinePatternView.swift; sourceTree = ""; }; + 0C0387942D0CD120000A2F24 /* SwapOperationFeeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapOperationFeeView.swift; sourceTree = ""; }; + 0C0387972D0CE605000A2F24 /* SwapFeeDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapFeeDetailsViewModel.swift; sourceTree = ""; }; + 0C0387992D0CEDC9000A2F24 /* SwapFeeDetailsViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapFeeDetailsViewModelFactory.swift; sourceTree = ""; }; + 0C03879B2D0F64F9000A2F24 /* HydraSwapParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraSwapParams.swift; sourceTree = ""; }; 0C053ECA2BA2FA5C003063A0 /* StorageLocationMigrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageLocationMigrationTests.swift; sourceTree = ""; }; 0C0CB37E2AC540B200EAC516 /* AssetConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversion.swift; sourceTree = ""; }; - 0C0CB3812AC545A800EAC516 /* AssetConversionExtrinsicService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionExtrinsicService.swift; sourceTree = ""; }; 0C0CB3842AC561FB00EAC516 /* AssetHubSwapOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubSwapOperationFactory.swift; sourceTree = ""; }; 0C0CB3872AC5688100EAC516 /* AssetConversionPallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionPallet.swift; sourceTree = ""; }; 0C0CB3892AC56A1600EAC516 /* AssetConversionPallet+Path.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AssetConversionPallet+Path.swift"; sourceTree = ""; }; @@ -5243,8 +5400,21 @@ 0C0E0A9C2B3EFD2500865F10 /* ProxyCallFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyCallFilter.swift; sourceTree = ""; }; 0C0E0A9F2B3F013800865F10 /* BalancesPallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalancesPallet.swift; sourceTree = ""; }; 0C0E0AA22B3F01FD00865F10 /* UniquesPallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniquesPallet.swift; sourceTree = ""; }; + 0C11D8512CC9F5F2003EC46D /* HydraOmnipoolExchangeEdge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraOmnipoolExchangeEdge.swift; sourceTree = ""; }; + 0C11D8532CC9F82C003EC46D /* HydraStableswapExchangeEdge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraStableswapExchangeEdge.swift; sourceTree = ""; }; + 0C11D8552CC9F9E6003EC46D /* HydraXYKExchangeEdge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraXYKExchangeEdge.swift; sourceTree = ""; }; + 0C11D8572CC9FED7003EC46D /* AssetsHydraStableswapExchange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsHydraStableswapExchange.swift; sourceTree = ""; }; + 0C11D8592CCA3377003EC46D /* AssetsHydraOmnipoolExchange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsHydraOmnipoolExchange.swift; sourceTree = ""; }; + 0C11D85B2CCA3CCC003EC46D /* AssetsHydraXYKExchange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsHydraXYKExchange.swift; sourceTree = ""; }; + 0C11D85D2CCA470D003EC46D /* AssetsExchangeRouteManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsExchangeRouteManager.swift; sourceTree = ""; }; + 0C11D85F2CCA486B003EC46D /* AssetExchangeRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangeRoute.swift; sourceTree = ""; }; + 0C11F0272CEFD99C008D19D2 /* CountdownLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountdownLoadingView.swift; sourceTree = ""; }; 0C12A2462AA1D96000C7FA49 /* SubqueryMultistakingTypeFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubqueryMultistakingTypeFactory.swift; sourceTree = ""; }; 0C12A2482AA35AE300C7FA49 /* RelaychainStakingValidatorFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaychainStakingValidatorFacade.swift; sourceTree = ""; }; + 0C12BC342CE952EF00AB919D /* SwapRouteViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapRouteViewCell.swift; sourceTree = ""; }; + 0C12BC362CEA7DCB00AB919D /* AssetExchangePrice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangePrice.swift; sourceTree = ""; }; + 0C12BC392CEA7ECB00AB919D /* AssetExchangePriceStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangePriceStore.swift; sourceTree = ""; }; + 0C12BC3D2CEA9DF800AB919D /* AssetExchangeTimeEstimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangeTimeEstimation.swift; sourceTree = ""; }; 0C12BC462CEB996700AB919D /* XcmJunctionV4.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmJunctionV4.swift; sourceTree = ""; }; 0C12BC472CEB996700AB919D /* XcmV4Multilocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmV4Multilocation.swift; sourceTree = ""; }; 0C13380F2AB832B30036BCD6 /* QRImageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRImageViewModel.swift; sourceTree = ""; }; @@ -5293,6 +5463,9 @@ 0C1998E92C4A22FD000EBFB8 /* BalancesRemoteSubscriptionService+Protocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BalancesRemoteSubscriptionService+Protocol.swift"; sourceTree = ""; }; 0C1998EC2C4CB24C000EBFB8 /* AssetHold+Display.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AssetHold+Display.swift"; sourceTree = ""; }; 0C1998EE2C4CC51D000EBFB8 /* SCLoadableControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SCLoadableControllerProtocol.swift; sourceTree = ""; }; + 0C19CC9E2CC6EBBC007F8ED8 /* CrosschainAssetsExchange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrosschainAssetsExchange.swift; sourceTree = ""; }; + 0C19CCA12CC6EC75007F8ED8 /* AssetsHubExchangeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsHubExchangeProvider.swift; sourceTree = ""; }; + 0C19CCA42CC6EDAE007F8ED8 /* AssetsExchangeBaseProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsExchangeBaseProvider.swift; sourceTree = ""; }; 0C1BE19D2A46EDB40010933C /* String+ScientificInt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+ScientificInt.swift"; sourceTree = ""; }; 0C1BE19F2A46F1F00010933C /* ScientificStringParsing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScientificStringParsing.swift; sourceTree = ""; }; 0C1BE1A12A46F93B0010933C /* BigUInt+Scientific.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BigUInt+Scientific.swift"; sourceTree = ""; }; @@ -5319,12 +5492,28 @@ 0C259EA32B46721B00CB86E4 /* ProxyMessageSheetViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyMessageSheetViewFactory.swift; sourceTree = ""; }; 0C259EA72B46C55C00CB86E4 /* ExtrinsicSigningErrorHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtrinsicSigningErrorHandling.swift; sourceTree = ""; }; 0C29B5372A4C68A500E35C6D /* AnimationUpdatibleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimationUpdatibleView.swift; sourceTree = ""; }; + 0C2A3C922CDC813B00A0E2B3 /* AssetsExchangeOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsExchangeOperationFactory.swift; sourceTree = ""; }; + 0C2A3C942CDC8ADB00A0E2B3 /* SwapAssetSelectionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapAssetSelectionModel.swift; sourceTree = ""; }; + 0C2A3C972CDCCECC00A0E2B3 /* SwapTokensFlowState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapTokensFlowState.swift; sourceTree = ""; }; + 0C2A3C992CDCEB8A00A0E2B3 /* SwapAssetSelectionClosure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapAssetSelectionClosure.swift; sourceTree = ""; }; 0C2B18AD2BFE378B00206EDE /* CloudBackupSyncService+Create.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudBackupSyncService+Create.swift"; sourceTree = ""; }; 0C2B18AF2BFE3FC600206EDE /* CloudBackupSyncResultChanges+Critical.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudBackupSyncResultChanges+Critical.swift"; sourceTree = ""; }; 0C2B18B12BFE48BD00206EDE /* CloudBackupUpdateApplicationFactory+Create.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudBackupUpdateApplicationFactory+Create.swift"; sourceTree = ""; }; 0C2B18B32BFEFD2A00206EDE /* CloudBackupConflictsResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudBackupConflictsResolver.swift; sourceTree = ""; }; 0C2B3C9875FDA7EE8D168900 /* ParaStkYieldBoostSetupWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostSetupWireframe.swift; sourceTree = ""; }; 0C2B583DB30C6C818B0F952D /* ParaStkRebondWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkRebondWireframe.swift; sourceTree = ""; }; + 0C2BD7CE1EF80FCD3569BD44 /* SwapRouteDetailsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapRouteDetailsProtocols.swift; sourceTree = ""; }; + 0C2DA8992CC21419001F79C8 /* GraphQuotableEdge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQuotableEdge.swift; sourceTree = ""; }; + 0C2DA89B2CC215D0001F79C8 /* AssetExchangeGraphEdge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangeGraphEdge.swift; sourceTree = ""; }; + 0C2DA89D2CC21853001F79C8 /* CrosschainExchangeEdge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrosschainExchangeEdge.swift; sourceTree = ""; }; + 0C2DA89F2CC21B09001F79C8 /* IndexedChainModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndexedChainModels.swift; sourceTree = ""; }; + 0C2DA8A12CC21E5F001F79C8 /* AssetHubExchangeEdge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubExchangeEdge.swift; sourceTree = ""; }; + 0C2DA8A32CC223D9001F79C8 /* AssetsHydraExchangeEdge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsHydraExchangeEdge.swift; sourceTree = ""; }; + 0C2DA8A52CC265F0001F79C8 /* AssetsExchangeGraphProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsExchangeGraphProvider.swift; sourceTree = ""; }; + 0C2DA8A72CC2679E001F79C8 /* AssetsExchangeGraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsExchangeGraph.swift; sourceTree = ""; }; + 0C2DA8A92CC2A2E2001F79C8 /* AnyAssetExchangeEdge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyAssetExchangeEdge.swift; sourceTree = ""; }; + 0C2DA8AC2CC2B156001F79C8 /* AssetsExchangeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsExchangeTests.swift; sourceTree = ""; }; + 0C2DA8AE2CC2F928001F79C8 /* AssetsExchangeGraphDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsExchangeGraphDescription.swift; sourceTree = ""; }; 0C2DF1CD2BB6825300DD58B1 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; 0C2DF1CE2BB6825300DD58B1 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; 0C2DF1CF2BB6825400DD58B1 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.stringsdict"; sourceTree = ""; }; @@ -5341,7 +5530,6 @@ 0C2F86952A72807E00593C01 /* NominationPoolsRewardEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsRewardEngine.swift; sourceTree = ""; }; 0C2F86972A728EE900593C01 /* NPoolsRewardEngineFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NPoolsRewardEngineFactory.swift; sourceTree = ""; }; 0C2F86992A72948100593C01 /* NominationPoolsApyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsApyTests.swift; sourceTree = ""; }; - 0C2FDF182AEB82AA006A6C59 /* AssetHubFeeModelBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubFeeModelBuilder.swift; sourceTree = ""; }; 0C3205BA2A8679F0002EB914 /* EvmGasPriceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmGasPriceProvider.swift; sourceTree = ""; }; 0C3205BD2A867A9C002EB914 /* EvmLegacyGasPriceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmLegacyGasPriceProvider.swift; sourceTree = ""; }; 0C3205BF2A867DD6002EB914 /* EvmMaxPriorityGasPriceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmMaxPriorityGasPriceProvider.swift; sourceTree = ""; }; @@ -5365,12 +5553,22 @@ 0C3205E72A898195002EB914 /* EvmValidationErrorPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmValidationErrorPresentable.swift; sourceTree = ""; }; 0C3205E92A8A0539002EB914 /* EvmValidationProviderFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmValidationProviderFactory.swift; sourceTree = ""; }; 0C3205EB2A8A122D002EB914 /* FeeOutputModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeeOutputModel.swift; sourceTree = ""; }; + 0C3272882CD1F83D00FC1B42 /* AssetExchangeAtomicOperationArgs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangeAtomicOperationArgs.swift; sourceTree = ""; }; + 0C32728C2CD1F90100FC1B42 /* AssetExchangeSwapLimit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangeSwapLimit.swift; sourceTree = ""; }; + 0C32728E2CD203F000FC1B42 /* XcmTotalFeeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmTotalFeeModel.swift; sourceTree = ""; }; + 0C3272902CD2409B00FC1B42 /* AssetExchangeOperationFee.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangeOperationFee.swift; sourceTree = ""; }; + 0C3272922CD246DD00FC1B42 /* AssetExchangeOperationFee+Crosschain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AssetExchangeOperationFee+Crosschain.swift"; sourceTree = ""; }; + 0C3272942CD24F9600FC1B42 /* AssetHubExchangeAtomicOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubExchangeAtomicOperation.swift; sourceTree = ""; }; + 0C3272962CD2821500FC1B42 /* ChainRegistry+Get.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChainRegistry+Get.swift"; sourceTree = ""; }; + 0C32729B2CD28C4000FC1B42 /* AssetExchangeOperationFee+AssetHub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AssetExchangeOperationFee+AssetHub.swift"; sourceTree = ""; }; + 0C32729D2CD2975400FC1B42 /* HydraExchangeAtomicOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraExchangeAtomicOperation.swift; sourceTree = ""; }; + 0C32729F2CD34CB200FC1B42 /* HydraExchangeExtrinsicParamsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraExchangeExtrinsicParamsFactory.swift; sourceTree = ""; }; + 0C3272A12CD34F7700FC1B42 /* HydraExchangeExtrinsicConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraExchangeExtrinsicConverter.swift; sourceTree = ""; }; + 0C33E8B52D0069EC0090096A /* AssetsExchangeFeeSupportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsExchangeFeeSupportProvider.swift; sourceTree = ""; }; + 0C33E8B92D011D2E0090096A /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = ""; }; 0C34D496D0F57E685237B3A7 /* StakingUnbondConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondConfirmInteractor.swift; sourceTree = ""; }; - 0C363E952B6B591C0065AFA6 /* HydraExtrinsicOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraExtrinsicOperationFactory.swift; sourceTree = ""; }; 0C363E972B6B63280065AFA6 /* HydraConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraConstants.swift; sourceTree = ""; }; - 0C363E992B6B69D60065AFA6 /* HydraFeeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraFeeService.swift; sourceTree = ""; }; 0C363E9B2B6B8C400065AFA6 /* HydraExtrinsicConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraExtrinsicConverter.swift; sourceTree = ""; }; - 0C363E9D2B6BB2E20065AFA6 /* HydraSwapsFeeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraSwapsFeeTests.swift; sourceTree = ""; }; 0C37AFB62B555F1400009ECA /* StakingValidatorExposureFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingValidatorExposureFacade.swift; sourceTree = ""; }; 0C37AFB82B5562B500009ECA /* StakingEraStakersExposureFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingEraStakersExposureFactory.swift; sourceTree = ""; }; 0C37AFBA2B556A9300009ECA /* StakingPagedExposureFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingPagedExposureFactory.swift; sourceTree = ""; }; @@ -5428,6 +5626,8 @@ 0C41D21D2BFB0719000950EE /* WalletsFetchHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletsFetchHelper.swift; sourceTree = ""; }; 0C41D21F2BFB07B4000950EE /* CloudBackupFetchHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudBackupFetchHelper.swift; sourceTree = ""; }; 0C41D2212BFB0BAC000950EE /* KeystoreValidationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeystoreValidationHelper.swift; sourceTree = ""; }; + 0C423ABA2CDBAE9B00A64941 /* AssetExchangeFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangeFacade.swift; sourceTree = ""; }; + 0C423ABC2CDBAEC900A64941 /* AssetExchangeGraphProvidingParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangeGraphProvidingParams.swift; sourceTree = ""; }; 0C432D57ACFA53F42E574CBD /* TokensManageViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensManageViewController.swift; sourceTree = ""; }; 0C4394D32BD6661F00578761 /* PasswordInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordInputView.swift; sourceTree = ""; }; 0C4394D62BD761D000578761 /* CloudBackupPasswordValidationResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudBackupPasswordValidationResult.swift; sourceTree = ""; }; @@ -5477,10 +5677,30 @@ 0C59E8FD2AA773D6001E11F3 /* OperationDetailsDataProviderFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationDetailsDataProviderFactory.swift; sourceTree = ""; }; 0C5FA9A42B830A650077934C /* WalletConnectUrlParsingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnectUrlParsingService.swift; sourceTree = ""; }; 0C5FA9A62B8313950077934C /* String+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Search.swift"; sourceTree = ""; }; + 0C6108252CD5227900909928 /* HydraExchangeHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraExchangeHost.swift; sourceTree = ""; }; + 0C6108272CD53CDE00909928 /* CrosschainExchangeHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrosschainExchangeHost.swift; sourceTree = ""; }; + 0C6108292CD5DE9500909928 /* AssetHubExchangeHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubExchangeHost.swift; sourceTree = ""; }; + 0C61082F2CD5ED1400909928 /* ExtrinsicCustomFeeEstimatingFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtrinsicCustomFeeEstimatingFactoryProtocol.swift; sourceTree = ""; }; + 0C6108312CD5EEBD00909928 /* AssetConversionFeeEstimatingFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionFeeEstimatingFactory.swift; sourceTree = ""; }; + 0C6108332CD5F77E00909928 /* ExtrinsicFeeEstimatorHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtrinsicFeeEstimatorHost.swift; sourceTree = ""; }; + 0C6108352CD5FC7300909928 /* ExtrinsicFeeInstallingWrapperFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtrinsicFeeInstallingWrapperFactory.swift; sourceTree = ""; }; + 0C6108372CD7333100909928 /* ExtrinsicCustomFeeInstallingWrapperFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtrinsicCustomFeeInstallingWrapperFactory.swift; sourceTree = ""; }; + 0C61083D2CD7382E00909928 /* AssetConversionFeeInstallingFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionFeeInstallingFactory.swift; sourceTree = ""; }; + 0C6108402CD74DD200909928 /* AssetExchangeFeeEstimatingFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangeFeeEstimatingFactory.swift; sourceTree = ""; }; + 0C6108422CD755A300909928 /* AssetExchangeGraphProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangeGraphProxy.swift; sourceTree = ""; }; + 0C6108442CD8827200909928 /* HydraSwapFeeCurrencyService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraSwapFeeCurrencyService.swift; sourceTree = ""; }; + 0C6108462CD882F900909928 /* SwapFeeCurrencyState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapFeeCurrencyState.swift; sourceTree = ""; }; + 0C6108482CD889A000909928 /* AssetsExchange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsExchange.swift; sourceTree = ""; }; + 0C61084A2CD88F9B00909928 /* AssetsExchangeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsExchangeService.swift; sourceTree = ""; }; + 0C61084C2CD8909600909928 /* AssetExchangeFee.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangeFee.swift; sourceTree = ""; }; + 0C61084E2CD92C6F00909928 /* GraphEdgeFiltering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphEdgeFiltering.swift; sourceTree = ""; }; + 0C6108502CD9D3BA00909928 /* GraphModel+BFS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GraphModel+BFS.swift"; sourceTree = ""; }; 0C626D1A2A8F519100CDAF4E /* StakingMainPresenterFactory+NominationPools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StakingMainPresenterFactory+NominationPools.swift"; sourceTree = ""; }; 0C626D1C2A915E2F00CDAF4E /* StakingNominationPoolsStatics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingNominationPoolsStatics.swift; sourceTree = ""; }; 0C626D1E2A92AA0F00CDAF4E /* NominationPoolsDataProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsDataProviding.swift; sourceTree = ""; }; 0C626D202A933A7D00CDAF4E /* StakingNPoolsViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingNPoolsViewModelFactory.swift; sourceTree = ""; }; + 0C6353442CE463BD00EAB200 /* AssetExchangePathFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangePathFilter.swift; sourceTree = ""; }; + 0C6353462CE46FB200EAB200 /* AssetExchangeSufficiencyProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangeSufficiencyProvider.swift; sourceTree = ""; }; 0C63908A2BF073C20015D467 /* CloudBackupSyncMonitoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudBackupSyncMonitoring.swift; sourceTree = ""; }; 0C63908C2BF073D00015D467 /* ICloudBackupSyncMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ICloudBackupSyncMonitor.swift; sourceTree = ""; }; 0C6390902BF091610015D467 /* CloudBackupUpdateCalculationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudBackupUpdateCalculationFactory.swift; sourceTree = ""; }; @@ -5526,6 +5746,8 @@ 0C7104A12C2D0A6200487E64 /* LedgerTxConfirmationParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LedgerTxConfirmationParams.swift; sourceTree = ""; }; 0C7104A32C2D0FC500487E64 /* BaseLedgerTxConfirmInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseLedgerTxConfirmInteractor.swift; sourceTree = ""; }; 0C7104A72C2D11EB00487E64 /* GenericLedgerTxConfirmInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericLedgerTxConfirmInteractor.swift; sourceTree = ""; }; + 0C746DBB2CCE5E9900E9178B /* AssetExchangeExecutionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangeExecutionManager.swift; sourceTree = ""; }; + 0C746DBD2CCF440800E9178B /* AssetExchangeAtomicOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangeAtomicOperation.swift; sourceTree = ""; }; 0C75E2952C3F9263005A6232 /* DelegatedStakingPallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatedStakingPallet.swift; sourceTree = ""; }; 0C75E2972C3F92EC005A6232 /* DelegatedStakingPallet+Path.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DelegatedStakingPallet+Path.swift"; sourceTree = ""; }; 0C77B55E2A83717000B5AE08 /* StaticValidatorListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticValidatorListViewController.swift; sourceTree = ""; }; @@ -5573,18 +5795,16 @@ 0C846B8F2BE5F069000EBFC2 /* StakingGlobalConfigProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingGlobalConfigProvider.swift; sourceTree = ""; }; 0C846B912BE69079000EBFC2 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 0C846B942BE69417000EBFC2 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; - 0C85FF212B6C001900FC0014 /* HydraSwapExtrinsicService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraSwapExtrinsicService.swift; sourceTree = ""; }; 0C85FF232B6CB28200FC0014 /* AssetHubExtrinsicConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubExtrinsicConverter.swift; sourceTree = ""; }; - 0C85FF252B6CB9B400FC0014 /* HydraCallPathFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraCallPathFactory.swift; sourceTree = ""; }; - 0C85FF272B6CBA9D00FC0014 /* AssetHubCallPathFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubCallPathFactory.swift; sourceTree = ""; }; 0C85FF292B6D230100FC0014 /* AssetHubReQuoteService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubReQuoteService.swift; sourceTree = ""; }; 0C85FF2B2B6D23D600FC0014 /* ObservableSubscriptionSyncState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableSubscriptionSyncState.swift; sourceTree = ""; }; 0C85FF2D2B6D300800FC0014 /* HydraOmnipoolFlowState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraOmnipoolFlowState.swift; sourceTree = ""; }; 0C85FF2F2B6D35D600FC0014 /* AssetHubFlowState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubFlowState.swift; sourceTree = ""; }; - 0C85FF312B6D4A3500FC0014 /* AssetConversionFlowState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionFlowState.swift; sourceTree = ""; }; 0C85FF332B6D523B00FC0014 /* HydraOmnipoolQuoteFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraOmnipoolQuoteFactory.swift; sourceTree = ""; }; - 0C85FF362B6E0BB300FC0014 /* AssetConversionAggregationFactory+AssetHub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AssetConversionAggregationFactory+AssetHub.swift"; sourceTree = ""; }; - 0C85FF382B6E0C0600FC0014 /* AssetConversionAggregationFactory+HydraDx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AssetConversionAggregationFactory+HydraDx.swift"; sourceTree = ""; }; + 0C883A712CE2235600CAB4C8 /* HydraSwapEventsMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraSwapEventsMatcher.swift; sourceTree = ""; }; + 0C883A732CE241CB00CAB4C8 /* AssetConversionEventsMatching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionEventsMatching.swift; sourceTree = ""; }; + 0C883A752CE25E6800CAB4C8 /* HydraSwapEventParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraSwapEventParser.swift; sourceTree = ""; }; + 0C883A772CE2640000CAB4C8 /* AssetConversionEventParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionEventParser.swift; sourceTree = ""; }; 0C893E692A65591C00781503 /* PoolsMultistakingUpdateService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoolsMultistakingUpdateService.swift; sourceTree = ""; }; 0C893E6C2A6562B400781503 /* NominationPools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPools.swift; sourceTree = ""; }; 0C893E6E2A65702A00781503 /* NominationPools+CodingPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NominationPools+CodingPath.swift"; sourceTree = ""; }; @@ -5683,6 +5903,7 @@ 0C9951CE2AE2BAE500B65615 /* PromotionBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionBannerView.swift; sourceTree = ""; }; 0C9951D22AE2DB0200B65615 /* PromotionViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionViewModelFactory.swift; sourceTree = ""; }; 0C9A7F982AFC9A2B00938CD0 /* SwapPriceDifferenceConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapPriceDifferenceConfig.swift; sourceTree = ""; }; + 0C9BCC572CBD6B4000CBE21B /* AssetsExchangeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsExchangeProtocol.swift; sourceTree = ""; }; 0C9C642C2A8CE30A004DC078 /* SystemAccountValidating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemAccountValidating.swift; sourceTree = ""; }; 0C9C642F2A8D6779004DC078 /* StakingNPoolsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingNPoolsPresenter.swift; sourceTree = ""; }; 0C9C64312A8D67A0004DC078 /* StakingNPoolsInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingNPoolsInteractor.swift; sourceTree = ""; }; @@ -5717,7 +5938,6 @@ 0CA719862B78AA71000B086E /* HydraRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraRouter.swift; sourceTree = ""; }; 0CA719882B78AACD000B086E /* HydraRouter+Call.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HydraRouter+Call.swift"; sourceTree = ""; }; 0CA7198B2B78FEF9000B086E /* HydraQuoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraQuoteTests.swift; sourceTree = ""; }; - 0CA7198D2B791811000B086E /* HydraReQuoteService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraReQuoteService.swift; sourceTree = ""; }; 0CA7198F2B79C92E000B086E /* HydraRoutesOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraRoutesOperationFactory.swift; sourceTree = ""; }; 0CA719912B79C987000B086E /* HydraOmnipool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraOmnipool.swift; sourceTree = ""; }; 0CA719932B79CA01000B086E /* HydraOmnipool+Storages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HydraOmnipool+Storages.swift"; sourceTree = ""; }; @@ -5725,6 +5945,33 @@ 0CA719972B79D033000B086E /* HydraRoutesResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraRoutesResolver.swift; sourceTree = ""; }; 0CA719992B79DF5B000B086E /* HydraStableswapApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraStableswapApi.swift; sourceTree = ""; }; 0CA7821B2B03D0A9003F562A /* ExtrinsicProcessor+AssetHubSwapMatching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ExtrinsicProcessor+AssetHubSwapMatching.swift"; sourceTree = ""; }; + 0CA7CEE12CE0BBE9004328F2 /* SubstrateEventsRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubstrateEventsRepository.swift; sourceTree = ""; }; + 0CA7CEE32CE0C23B004328F2 /* SystemPallet+Events.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SystemPallet+Events.swift"; sourceTree = ""; }; + 0CA7CEE42CE0C23B004328F2 /* SystemPallet+StoragePath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SystemPallet+StoragePath.swift"; sourceTree = ""; }; + 0CA7CEE82CE0C37B004328F2 /* SystemPallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemPallet.swift; sourceTree = ""; }; + 0CA7CEEB2CE0CA57004328F2 /* NativeTokenDepositEventMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTokenDepositEventMatcher.swift; sourceTree = ""; }; + 0CA7CEED2CE0CA75004328F2 /* TokenDepositEventMatching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenDepositEventMatching.swift; sourceTree = ""; }; + 0CA7CEEF2CE0CC13004328F2 /* BalancesPallet+EventCodingPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BalancesPallet+EventCodingPath.swift"; sourceTree = ""; }; + 0CA7CEF12CE0D14F004328F2 /* PalletAssetsTokenDepositEventMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PalletAssetsTokenDepositEventMatcher.swift; sourceTree = ""; }; + 0CA7CEF32CE0D210004328F2 /* PalletAssets+EventCodingPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PalletAssets+EventCodingPath.swift"; sourceTree = ""; }; + 0CA7CEF52CE0D35B004328F2 /* PalletAssets+Events.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PalletAssets+Events.swift"; sourceTree = ""; }; + 0CA7CEF72CE0D575004328F2 /* TokensPalletDepositEventMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokensPalletDepositEventMatcher.swift; sourceTree = ""; }; + 0CA7CEF92CE13126004328F2 /* TokenDepositEventMatcherFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenDepositEventMatcherFactory.swift; sourceTree = ""; }; + 0CA81D4B2CE4FF8800166969 /* AssetExchangeGraphFeeSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangeGraphFeeSupport.swift; sourceTree = ""; }; + 0CA81D4F2CE502B200166969 /* AssetExchangeFeeSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangeFeeSupport.swift; sourceTree = ""; }; + 0CA81D512CE50BDB00166969 /* HydraExchangeFeeSupportFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraExchangeFeeSupportFetcher.swift; sourceTree = ""; }; + 0CA81D532CE5108A00166969 /* AssetHubExchangeFeeSupportFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubExchangeFeeSupportFetcher.swift; sourceTree = ""; }; + 0CA81D562CE5AF2600166969 /* AssetExchangeFeeSupportFetchersProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangeFeeSupportFetchersProvider.swift; sourceTree = ""; }; + 0CA81D582CE60AA700166969 /* AssetExchangeEdgeType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangeEdgeType.swift; sourceTree = ""; }; + 0CA8AD552CE06FA000ED9746 /* XcmTransactService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmTransactService.swift; sourceTree = ""; }; + 0CA8AD592CE0789100ED9746 /* WalletRemoteSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletRemoteSubscription.swift; sourceTree = ""; }; + 0CA8AD5B2CE08FEE00ED9746 /* BlockEventsQueryFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockEventsQueryFactory.swift; sourceTree = ""; }; + 0CA8AD5C2CE08FEE00ED9746 /* ExtrinsicEventsMatching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtrinsicEventsMatching.swift; sourceTree = ""; }; + 0CA8AD5D2CE08FEE00ED9746 /* ExtrinsicStatusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtrinsicStatusService.swift; sourceTree = ""; }; + 0CA8AD5E2CE08FEE00ED9746 /* ExtrinsicSubmissionMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtrinsicSubmissionMonitor.swift; sourceTree = ""; }; + 0CA8AD5F2CE08FEE00ED9746 /* SubstrateExtrinsicEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubstrateExtrinsicEvents.swift; sourceTree = ""; }; + 0CA8AD602CE08FEE00ED9746 /* SubstrateExtrinsicStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubstrateExtrinsicStatus.swift; sourceTree = ""; }; + 0CA8AD682CE0904D00ED9746 /* XcmDepositMonitoringService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmDepositMonitoringService.swift; sourceTree = ""; }; 0CA957212B6A507A009AD757 /* HydraSwapParamsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraSwapParamsService.swift; sourceTree = ""; }; 0CA957242B6A566B009AD757 /* HydraDx+Call.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HydraDx+Call.swift"; sourceTree = ""; }; 0CAB7D9E2C46B05F0070CE4D /* PoolStakingRecommendingValidationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoolStakingRecommendingValidationFactory.swift; sourceTree = ""; }; @@ -5772,6 +6019,10 @@ 0CB313782C0F499B00353724 /* CloudBackupSetupPasswordFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudBackupSetupPasswordFlow.swift; sourceTree = ""; }; 0CB313852C12BBBD00353724 /* CloudBackupEnablePasswordInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudBackupEnablePasswordInteractor.swift; sourceTree = ""; }; 0CB313872C12C0FC00353724 /* CloudBackupEnablePasswordWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudBackupEnablePasswordWireframe.swift; sourceTree = ""; }; + 0CB433452CC0C3F200F2CB59 /* CrosschainAssetsExchangeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrosschainAssetsExchangeProvider.swift; sourceTree = ""; }; + 0CB433472CC0F9EF00F2CB59 /* AssetsHubExchange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsHubExchange.swift; sourceTree = ""; }; + 0CB4334B2CC10C3C00F2CB59 /* AssetsHydraExchangeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsHydraExchangeProvider.swift; sourceTree = ""; }; + 0CB4334D2CC1196400F2CB59 /* WeightableEdge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeightableEdge.swift; sourceTree = ""; }; 0CB64E592AFE9947008F268F /* GetTokenOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTokenOperation.swift; sourceTree = ""; }; 0CB64E5B2B009DA9008F268F /* GetTokenOptionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTokenOptionsModel.swift; sourceTree = ""; }; 0CB64E5D2B00AA8F008F268F /* GetTokenOptionsResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTokenOptionsResult.swift; sourceTree = ""; }; @@ -5781,6 +6032,15 @@ 0CB64E662B01E174008F268F /* TransferNetworkSelectionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferNetworkSelectionViewModel.swift; sourceTree = ""; }; 0CB64E682B01F798008F268F /* TransferSetupOriginSelectionWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferSetupOriginSelectionWireframe.swift; sourceTree = ""; }; 0CB6B2832C57857C00FFE475 /* TriangularedButton+Title.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TriangularedButton+Title.swift"; sourceTree = ""; }; + 0CBABFE62CED31690047F29E /* CrosschainExchangeMetaOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrosschainExchangeMetaOperation.swift; sourceTree = ""; }; + 0CBABFE82CED31C80047F29E /* AssetHubExchangeMetaOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubExchangeMetaOperation.swift; sourceTree = ""; }; + 0CBABFEA2CED37E30047F29E /* AssetExchangeQuote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangeQuote.swift; sourceTree = ""; }; + 0CBABFEC2CED438A0047F29E /* AssetExchangeOperationPrototype.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangeOperationPrototype.swift; sourceTree = ""; }; + 0CBABFEE2CED46200047F29E /* HydraExchangeOperationPrototype.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraExchangeOperationPrototype.swift; sourceTree = ""; }; + 0CBABFF02CED46C90047F29E /* AssetHubExchangeOperationPrototype.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubExchangeOperationPrototype.swift; sourceTree = ""; }; + 0CBABFF22CED47D10047F29E /* CrosschainExchangeOperationPrototype.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrosschainExchangeOperationPrototype.swift; sourceTree = ""; }; + 0CBABFF52CEEACB60047F29E /* RoundedButton+Set.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RoundedButton+Set.swift"; sourceTree = ""; }; + 0CBABFF82CEF33BE0047F29E /* SwapExecutionDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapExecutionDetailsView.swift; sourceTree = ""; }; 0CBC29C52A421B5000F7B1F7 /* StakingMainWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingMainWireframe.swift; sourceTree = ""; }; 0CBC29C72A42AE2F00F7B1F7 /* StakingDashboardBuilderResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingDashboardBuilderResult.swift; sourceTree = ""; }; 0CBF5DE62AB1A60500087EBF /* SharedOperationStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedOperationStatus.swift; sourceTree = ""; }; @@ -5827,6 +6087,9 @@ 0CCA245C2AC6918800AEF23D /* XcmV3Junction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmV3Junction.swift; sourceTree = ""; }; 0CCA245E2AC6974200AEF23D /* XcmV3Multilocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmV3Multilocation.swift; sourceTree = ""; }; 0CCA24642AC6B51200AEF23D /* AssetHubSwapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubSwapTests.swift; sourceTree = ""; }; + 0CCA70722CD0A4FD0082A9C8 /* CrosschainExchangeAtomicOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrosschainExchangeAtomicOperation.swift; sourceTree = ""; }; + 0CCA70742CD0B2860082A9C8 /* MetaAccountModel+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MetaAccountModel+Async.swift"; sourceTree = ""; }; + 0CCA70772CD0B4220082A9C8 /* SigningWrapperFactory+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SigningWrapperFactory+Async.swift"; sourceTree = ""; }; 0CCCDF732B62AA3400473D42 /* ParaStkPreferredCollatorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParaStkPreferredCollatorFactory.swift; sourceTree = ""; }; 0CCCDF752B64B80500473D42 /* StorageKeysOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageKeysOperationFactory.swift; sourceTree = ""; }; 0CCCDF7D2B64BE5300473D42 /* HydraDx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraDx.swift; sourceTree = ""; }; @@ -5871,12 +6134,9 @@ 0CCE3AA92BF395DC00D55F03 /* CloudBackupUpdateApplicationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudBackupUpdateApplicationFactory.swift; sourceTree = ""; }; 0CCE3AAB2BF4762000D55F03 /* CloudBackupDiff+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudBackupDiff+Helper.swift"; sourceTree = ""; }; 0CCE3AAE2BF5BE4300D55F03 /* IdentityProxyFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityProxyFactory.swift; sourceTree = ""; }; - 0CD352922ACAD7A500B3E446 /* AssetHubExtrinsicService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubExtrinsicService.swift; sourceTree = ""; }; 0CD352942ACAF59900B3E446 /* BigRational.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BigRational.swift; sourceTree = ""; }; - 0CD352962ACAFADA00B3E446 /* AssetConversionOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionOperationFactory.swift; sourceTree = ""; }; + 0CD352962ACAFADA00B3E446 /* AssetConversionOperationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionOperationError.swift; sourceTree = ""; }; 0CD3529A2ACD3E4300B3E446 /* PalletAssets+Call.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PalletAssets+Call.swift"; sourceTree = ""; }; - 0CD3A67B2AEAA3B90059BBEC /* AssetConversionFeeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionFeeService.swift; sourceTree = ""; }; - 0CD3A67D2AEAAB670059BBEC /* AssetHubFeeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubFeeService.swift; sourceTree = ""; }; 0CD3A67F2AEAC3C90059BBEC /* CompoundOperationWrapper+Add.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CompoundOperationWrapper+Add.swift"; sourceTree = ""; }; 0CD54E4A2BCE306B007B58E7 /* CloudBackupCryptoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudBackupCryptoManager.swift; sourceTree = ""; }; 0CD54E4C2BCE3B89007B58E7 /* CloudBackupSecretsImporting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudBackupSecretsImporting.swift; sourceTree = ""; }; @@ -5937,9 +6197,13 @@ 0CE933E12C07671A000F3EFE /* CloudBackupRemindPresentationResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudBackupRemindPresentationResult.swift; sourceTree = ""; }; 0CE933E32C07C225000F3EFE /* WalletCreationRequestFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletCreationRequestFactory.swift; sourceTree = ""; }; 0CE933E52C07DBE1000F3EFE /* WalletNameChanged.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletNameChanged.swift; sourceTree = ""; }; + 0CE94FEF2D03AF9800E31D0C /* SwapPreferredFeeAssetModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapPreferredFeeAssetModel.swift; sourceTree = ""; }; + 0CE94FF12D046A8700E31D0C /* AssetExchangeFeePayerMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangeFeePayerMatcher.swift; sourceTree = ""; }; + 0CE94FF32D04969100E31D0C /* SwapInterEDNotMet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapInterEDNotMet.swift; sourceTree = ""; }; + 0CE94FF52D04A3C100E31D0C /* AssetsExchangePathCostEstimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsExchangePathCostEstimator.swift; sourceTree = ""; }; + 0CE94FF72D050EE300E31D0C /* AssetExchangeOperationPrototypeFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangeOperationPrototypeFactory.swift; sourceTree = ""; }; 0CEAEBBD2BEDDD230019778F /* CloudBackupSettingsViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudBackupSettingsViewModelFactory.swift; sourceTree = ""; }; 0CEB4ED02AF14B8B0048FD84 /* SubmoduleNavigationStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmoduleNavigationStrategy.swift; sourceTree = ""; }; - 0CEB4ED22AF1689D0048FD84 /* AssetConversionAggregationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionAggregationFactory.swift; sourceTree = ""; }; 0CEB4ED42AF20EB90048FD84 /* CancellableCallHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellableCallHelper.swift; sourceTree = ""; }; 0CEB4ED82AF371EF0048FD84 /* AssetConversionTxPayment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionTxPayment.swift; sourceTree = ""; }; 0CEB6B2C2CA4121300609DC2 /* GovTreasurySpentRemoteHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GovTreasurySpentRemoteHandler.swift; sourceTree = ""; }; @@ -5967,8 +6231,14 @@ 0CF193D22A84FC7C003F12F6 /* SelectedStakingViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedStakingViewModelFactory.swift; sourceTree = ""; }; 0CF193D42A861925003F12F6 /* PredefinedTimeShortcut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PredefinedTimeShortcut.swift; sourceTree = ""; }; 0CF193D62A861D7E003F12F6 /* StartStakingInfoConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingInfoConstants.swift; sourceTree = ""; }; + 0CF3A6C82CE9423000F93C49 /* RouteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteView.swift; sourceTree = ""; }; + 0CF3A6CA2CE949ED00F93C49 /* SwapRouteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapRouteView.swift; sourceTree = ""; }; + 0CF4ADF82CEBF71A009C51FA /* AssetExchangeMetaOperationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangeMetaOperationProtocol.swift; sourceTree = ""; }; + 0CF4ADFC2CEC0A54009C51FA /* HydraExchangeMetaOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraExchangeMetaOperation.swift; sourceTree = ""; }; 0CF4ADFE2CECACF9009C51FA /* PolkadotRewardParamsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PolkadotRewardParamsService.swift; sourceTree = ""; }; 0CF4AE0A2CECDCF1009C51FA /* PolkadotRewardEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PolkadotRewardEngine.swift; sourceTree = ""; }; + 0CF5F6892CC7AF84007BAAC5 /* AssetExchangeGraphPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangeGraphPath.swift; sourceTree = ""; }; + 0CF5F68B2CC7B1B6007BAAC5 /* AssetsExchageGraphReachability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsExchageGraphReachability.swift; sourceTree = ""; }; 0CF609322B9438BD00DD4DB3 /* OperatingCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperatingCall.swift; sourceTree = ""; }; 0CF609342B9442F400DD4DB3 /* PushNotificationsServiceFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationsServiceFacade.swift; sourceTree = ""; }; 0CF692882C20E2B1000FC395 /* BackupManualWarningPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupManualWarningPresentable.swift; sourceTree = ""; }; @@ -5978,12 +6248,21 @@ 0CF692912C2412AB000FC395 /* BackupMnemonicAccountType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupMnemonicAccountType.swift; sourceTree = ""; }; 0CF817492BBD0E8B00CB9183 /* WalletNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletNameView.swift; sourceTree = ""; }; 0CF8176B2BC3B57100CB9183 /* CloudBackupErrorPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudBackupErrorPresentable.swift; sourceTree = ""; }; + 0CF976712CEFF01D001D2801 /* SwapExecutionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapExecutionModel.swift; sourceTree = ""; }; + 0CF976742CF082BF001D2801 /* SwapExecutionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapExecutionViewModel.swift; sourceTree = ""; }; + 0CF976762CF08EF9001D2801 /* OperationExecutionProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationExecutionProgressView.swift; sourceTree = ""; }; + 0CF976782CF09076001D2801 /* SwapExecutionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapExecutionView.swift; sourceTree = ""; }; + 0CF9767A2CF09B86001D2801 /* SwapExecutionViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapExecutionViewModelFactory.swift; sourceTree = ""; }; + 0CF9767C2CF1274F001D2801 /* AssetExchangeMetaOperationLabel+Display.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AssetExchangeMetaOperationLabel+Display.swift"; sourceTree = ""; }; + 0CF9767E2CF1293E001D2801 /* SwapExecutionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapExecutionState.swift; sourceTree = ""; }; + 0CF976802CFD04F8001D2801 /* AssetsExchangeStateMediator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsExchangeStateMediator.swift; sourceTree = ""; }; 0CFA16122B0CD8A0007AF885 /* GovSpentAmountExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GovSpentAmountExtractor.swift; sourceTree = ""; }; 0CFA16152B0CE51E007AF885 /* GovSpentAmountBatchHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GovSpentAmountBatchHandler.swift; sourceTree = ""; }; 0CFA16182B0CE709007AF885 /* UtilityPallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilityPallet.swift; sourceTree = ""; }; 0CFA161B2B0CE851007AF885 /* Utility+Calls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Utility+Calls.swift"; sourceTree = ""; }; 0CFA161D2B0CED07007AF885 /* GovTreasurySpentLocalHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GovTreasurySpentLocalHandler.swift; sourceTree = ""; }; 0CFA161F2B0CEF31007AF885 /* GovTreasuryApproveHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GovTreasuryApproveHandler.swift; sourceTree = ""; }; + 0CFFB9D22D11A67C00172E8C /* XcmTokensArrivalDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmTokensArrivalDetector.swift; sourceTree = ""; }; 0D37CF4AFB06AF3AC2F78057 /* ImportCloudPasswordPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ImportCloudPasswordPresenter.swift; sourceTree = ""; }; 0D3FE2CE7F9F2836755DBA63 /* GovernanceUnlockConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceUnlockConfirmProtocols.swift; sourceTree = ""; }; 0D65686560E2E6C18A5C34CB /* StartStakingInfoWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StartStakingInfoWireframe.swift; sourceTree = ""; }; @@ -6037,6 +6316,7 @@ 176299F44569F437B6317EA7 /* CloudBackupReviewChangesViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudBackupReviewChangesViewFactory.swift; sourceTree = ""; }; 1768C3415D7D9CEEA8A8C700 /* GovernanceUnavailableTracksPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceUnavailableTracksPresenter.swift; sourceTree = ""; }; 17C0AC11A4A195BB697578CE /* StakingPayoutConfirmationPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPayoutConfirmationPresenter.swift; sourceTree = ""; }; + 17CDE3DFBC17377C17ED99F4 /* SwapFeeDetailsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapFeeDetailsPresenter.swift; sourceTree = ""; }; 17F2450A063F4D66EFDF6B8A /* ParaStkUnstakeConfirmViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkUnstakeConfirmViewFactory.swift; sourceTree = ""; }; 182E3BCA71937085FBCBDD1D /* TokensManageAddWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensManageAddWireframe.swift; sourceTree = ""; }; 1855972C88E512AED37FD312 /* DAppBrowserViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppBrowserViewController.swift; sourceTree = ""; }; @@ -6073,6 +6353,7 @@ 1F7865BACFB8591F67D8EE06 /* TransferConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferConfirmProtocols.swift; sourceTree = ""; }; 1F943B80311761462ECA5185 /* WalletImportOptionsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletImportOptionsProtocols.swift; sourceTree = ""; }; 1FA6F8EC6245BA34F26AE276 /* NPoolsUnstakeSetupWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeSetupWireframe.swift; sourceTree = ""; }; + 1FBBC5F8D082B58B4FDFABD7 /* SwapFeeDetailsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapFeeDetailsViewLayout.swift; sourceTree = ""; }; 1FC2769454CA2CE2529278ED /* StakingRewardsNotificationsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRewardsNotificationsInteractor.swift; sourceTree = ""; }; 1FF860B3465854DCBC02DFB3 /* DAppBrowserPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppBrowserPresenter.swift; sourceTree = ""; }; 200C6B2C85846AED8CA9451A /* ExportMnemonicInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExportMnemonicInteractor.swift; sourceTree = ""; }; @@ -6315,10 +6596,9 @@ 2D8EFB292C08892300866F90 /* MnemonicViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MnemonicViewModelFactory.swift; sourceTree = ""; }; 2D8FF9CF2C9AA54000089F53 /* BaseReferendumVoteConfirmPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseReferendumVoteConfirmPresenter.swift; sourceTree = ""; }; 2D8FF9D22C9BD67500089F53 /* SwipeGovAlertPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeGovAlertPresentable.swift; sourceTree = ""; }; - 2D95DF5A2C6F7D83009BB063 /* HydraExtrinsicAssetsCustomFeeEstimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraExtrinsicAssetsCustomFeeEstimator.swift; sourceTree = ""; }; 2D95DF5C2C6F9129009BB063 /* HydraExtrinsicFeeInstaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraExtrinsicFeeInstaller.swift; sourceTree = ""; }; 2D95DF5E2C6FAFF6009BB063 /* ExtrinsicFeeEstimatingWrapperFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtrinsicFeeEstimatingWrapperFactory.swift; sourceTree = ""; }; - 2D95DF602C74B79A009BB063 /* HydraFlowStateStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraFlowStateStore.swift; sourceTree = ""; }; + 2D95DF602C74B79A009BB063 /* AssetConversionFeeSharedStateStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionFeeSharedStateStore.swift; sourceTree = ""; }; 2D95DF632C765E1B009BB063 /* mercuryoWidget.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = mercuryoWidget.html; sourceTree = ""; }; 2DA85A642C7D9B8B00591900 /* CardTopUpTransferSetupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardTopUpTransferSetupViewController.swift; sourceTree = ""; }; 2DA85A662C7DD75300591900 /* CardTopUpTransferSetupViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardTopUpTransferSetupViewLayout.swift; sourceTree = ""; }; @@ -6439,7 +6719,9 @@ 397F057FD5B16A58E5F30F07 /* NPoolsUnstakeConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeConfirmWireframe.swift; sourceTree = ""; }; 39907750D40A8DD7FE1288C8 /* CreateWatchOnlyViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateWatchOnlyViewController.swift; sourceTree = ""; }; 399700B22225DD916DFACAF9 /* DelegateVotedReferendaViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DelegateVotedReferendaViewFactory.swift; sourceTree = ""; }; + 399E91BE2E289FE301D7846A /* SwapRouteDetailsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapRouteDetailsPresenter.swift; sourceTree = ""; }; 3A46EE888D60C1538A0A3EFC /* NftDetailsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftDetailsProtocols.swift; sourceTree = ""; }; + 3A6CDEC83D8A51FD89673C31 /* SwapFeeDetailsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapFeeDetailsProtocols.swift; sourceTree = ""; }; 3A7235097E09C94005B091B4 /* CommonDelegationTracksPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommonDelegationTracksPresenter.swift; sourceTree = ""; }; 3A76BDAB14EEA1C4E23B884E /* ParitySignerTxQrViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerTxQrViewController.swift; sourceTree = ""; }; 3A93673EEA8F71E8DDA84557 /* StakingRedeemWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRedeemWireframe.swift; sourceTree = ""; }; @@ -6498,6 +6780,7 @@ 45C0B1C175A2470AAA50DAC5 /* DAppSettingsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppSettingsViewController.swift; sourceTree = ""; }; 461A9E7672D247C9CCF0B45D /* GovernanceRevokeDelegationTracksWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceRevokeDelegationTracksWireframe.swift; sourceTree = ""; }; 461B7FAD84690F82070E4431 /* StakingRebagConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRebagConfirmPresenter.swift; sourceTree = ""; }; + 461EFCB1DAACD7D5DBBB713C /* SwapRouteDetailsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapRouteDetailsViewController.swift; sourceTree = ""; }; 466D36DE48F51DC3023E3C5E /* StakingProxyManagementViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingProxyManagementViewController.swift; sourceTree = ""; }; 46BDF9947BE6366712E454DD /* LedgerDiscoverWalletCreateWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerDiscoverWalletCreateWireframe.swift; sourceTree = ""; }; 47042BBA5082B1EC1D017B56 /* AssetReceiveProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetReceiveProtocols.swift; sourceTree = ""; }; @@ -6522,6 +6805,7 @@ 4C602661DE4D6CAC482AF721 /* ParaStkCollatorsSearchViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkCollatorsSearchViewFactory.swift; sourceTree = ""; }; 4C71DEF78B69F017DF460AB7 /* CrowdloanContributionSetupViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionSetupViewController.swift; sourceTree = ""; }; 4CB3C834CCCD632B6CB30BEB /* Pods_novawalletAll_novawallet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_novawalletAll_novawallet.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4CE605788A0F121ECFC71C30 /* SwapExecutionPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapExecutionPresenter.swift; sourceTree = ""; }; 4D8246CDA02F544AF9DA2B11 /* ParaStkStakeSetupProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkStakeSetupProtocols.swift; sourceTree = ""; }; 4DAF102188EF43E4CFB62A5C /* SwipeGovReferendumDetailsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwipeGovReferendumDetailsViewFactory.swift; sourceTree = ""; }; 4DEE72D9DC1D96CCF8123B70 /* CloudBackupReviewChangesViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudBackupReviewChangesViewLayout.swift; sourceTree = ""; }; @@ -6538,6 +6822,7 @@ 4F9F9E43E296386A7F138326 /* AssetsSearchProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetsSearchProtocols.swift; sourceTree = ""; }; 4FD12F1DC3720B6C172357E4 /* ExportViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExportViewController.swift; sourceTree = ""; }; 5002B8FA2695F470587677D2 /* AccountConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountConfirmProtocols.swift; sourceTree = ""; }; + 501E16B2A68FEAEC039E8604 /* SwapExecutionViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapExecutionViewLayout.swift; sourceTree = ""; }; 502D42F4A480889BA226CAD3 /* StakingMainPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingMainPresenter.swift; sourceTree = ""; }; 50829CD47D3F60E3067418B4 /* ParaStkYieldBoostStartProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostStartProtocols.swift; sourceTree = ""; }; 50AFCEE78CDFC4FE3238E158 /* NPoolsClaimRewardsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsClaimRewardsViewController.swift; sourceTree = ""; }; @@ -6723,7 +7008,6 @@ 77171CA92A98BC420032B387 /* NominationPoolDataValidatorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolDataValidatorFactory.swift; sourceTree = ""; }; 7719018B2AE0E70B00D9C918 /* SwapDataValidatorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapDataValidatorFactory.swift; sourceTree = ""; }; 7719018D2AE0E71F00D9C918 /* SwapErrorPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapErrorPresentable.swift; sourceTree = ""; }; - 7719018F2AE2424B00D9C918 /* SwapsValidationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapsValidationTests.swift; sourceTree = ""; }; 7719019A2AE670AE00D9C918 /* ShortTextInfoPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortTextInfoPresentable.swift; sourceTree = ""; }; 7719019C2AE6996600D9C918 /* SwapPairView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapPairView.swift; sourceTree = ""; }; 7719019E2AE6C9DC00D9C918 /* SwapElementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapElementView.swift; sourceTree = ""; }; @@ -6731,7 +7015,7 @@ 771901A32AE7E48800D9C918 /* SwapBaseProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapBaseProtocols.swift; sourceTree = ""; }; 771901A52AE8FF7E00D9C918 /* SwapInfoViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapInfoViewCell.swift; sourceTree = ""; }; 771901A72AE8FF9F00D9C918 /* SwapNetworkFeeViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapNetworkFeeViewCell.swift; sourceTree = ""; }; - 771901AA2AE9581400D9C918 /* SwapConfirmViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapConfirmViewModelFactory.swift; sourceTree = ""; }; + 771901AA2AE9581400D9C918 /* SwapDetailsViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapDetailsViewModelFactory.swift; sourceTree = ""; }; 771901AF2AE97DA500D9C918 /* SwapConfirmViewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapConfirmViewModels.swift; sourceTree = ""; }; 771901B12AEA3E5100D9C918 /* ShortTextInfoPresentableExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortTextInfoPresentableExtensions.swift; sourceTree = ""; }; 771DA4542B7383BD003C07DD /* MultiassetUserDataModel12.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MultiassetUserDataModel12.xcdatamodel; sourceTree = ""; }; @@ -7755,7 +8039,7 @@ 84540171292F907B00213402 /* BlurBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurBackgroundView.swift; sourceTree = ""; }; 8454C21C2632A78900657DAD /* EventRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventRecord.swift; sourceTree = ""; }; 8454C2642632B0EF00657DAD /* EventCodingPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventCodingPath.swift; sourceTree = ""; }; - 8454C2692632B8CE00657DAD /* BalanceDepositEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceDepositEvent.swift; sourceTree = ""; }; + 8454C2692632B8CE00657DAD /* BalancesPallet+Events.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BalancesPallet+Events.swift"; sourceTree = ""; }; 8454C26E2632BBAA00657DAD /* ExtrinsicProcessing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtrinsicProcessing.swift; sourceTree = ""; }; 8454C2822632FC2500657DAD /* ExtrinsicProcessingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtrinsicProcessingTests.swift; sourceTree = ""; }; 845532CF2684690D00C2645D /* ParachainSlotLease.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParachainSlotLease.swift; sourceTree = ""; }; @@ -8102,7 +8386,6 @@ 8479F31326CD9A0E005D8D24 /* ChainRegistryIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainRegistryIntegrationTests.swift; sourceTree = ""; }; 847A258D29B5D25E0054F90C /* GovJsonLocalStorageSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GovJsonLocalStorageSubscriber.swift; sourceTree = ""; }; 847A258F29B5D2710054F90C /* GovJsonLocalStorageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GovJsonLocalStorageHandler.swift; sourceTree = ""; }; - 847A25B828D7BB1F006AC9F5 /* BalancesTransferEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalancesTransferEvent.swift; sourceTree = ""; }; 847A25BA28D7BB92006AC9F5 /* ExtrinsicProcessor+Events.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ExtrinsicProcessor+Events.swift"; sourceTree = ""; }; 847A25BC28D7C0E7006AC9F5 /* TokenTransferedEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenTransferedEvent.swift; sourceTree = ""; }; 847A25BE28D7C2A2006AC9F5 /* AccountIdCodingWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountIdCodingWrapper.swift; sourceTree = ""; }; @@ -8648,7 +8931,6 @@ 84B7C6A4289BFA79001A3566 /* DAppSearchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DAppSearchTests.swift; sourceTree = ""; }; 84B7C6A6289BFA79001A3566 /* DAppListGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DAppListGenerator.swift; sourceTree = ""; }; 84B7C6A8289BFA79001A3566 /* DAppListTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DAppListTests.swift; sourceTree = ""; }; - 84B7C6AA289BFA79001A3566 /* DAppTxDetailsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DAppTxDetailsTests.swift; sourceTree = ""; }; 84B7C6AC289BFA79001A3566 /* MnemonicTextNormalizerTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MnemonicTextNormalizerTest.swift; sourceTree = ""; }; 84B7C6AD289BFA79001A3566 /* AccountImportTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountImportTests.swift; sourceTree = ""; }; 84B7C6B0289BFA79001A3566 /* CrowdloanContributionConfirmTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionConfirmTests.swift; sourceTree = ""; }; @@ -8688,7 +8970,6 @@ 84B7C702289BFA79001A3566 /* AssetsManageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetsManageTests.swift; sourceTree = ""; }; 84B7C704289BFA79001A3566 /* WalletHistoryFilterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletHistoryFilterTests.swift; sourceTree = ""; }; 84B7C706289BFA79001A3566 /* AccountManagementTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountManagementTests.swift; sourceTree = ""; }; - 84B7C709289BFA79001A3566 /* WalletListTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletListTests.swift; sourceTree = ""; }; 84B7C70B289BFA79001A3566 /* ControllerAccountTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControllerAccountTests.swift; sourceTree = ""; }; 84B8AA7029F8E59300347A37 /* DAppInteractionPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAppInteractionPresenter.swift; sourceTree = ""; }; 84B8AA7229F8EFC700347A37 /* DAppInteractionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAppInteractionError.swift; sourceTree = ""; }; @@ -9253,7 +9534,6 @@ 84FBED062927B3B000FBEB83 /* EvmTransactionHistoryUpdaterFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmTransactionHistoryUpdaterFactory.swift; sourceTree = ""; }; 84FC190A29B7DB9F00BCCAA5 /* ExtrinsicServiceTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtrinsicServiceTypes.swift; sourceTree = ""; }; 84FCCD97292E3610002D2D3D /* JSONRPCError+Evm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONRPCError+Evm.swift"; sourceTree = ""; }; - 84FD19FD27A3447E008E5E68 /* BalancesWithdrawEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalancesWithdrawEvent.swift; sourceTree = ""; }; 84FD3DB02540C09800A234E3 /* TransactionHistoryMergeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionHistoryMergeManager.swift; sourceTree = ""; }; 84FD3DB42540ED0900A234E3 /* Block.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Block.swift; sourceTree = ""; }; 84FD3DB62540EF0700A234E3 /* TransactionSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionSubscription.swift; sourceTree = ""; }; @@ -9284,6 +9564,7 @@ 85F45A5C6145F863760F4409 /* AccountImportWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountImportWireframe.swift; sourceTree = ""; }; 8602E65CC4E81A7BE1727CE3 /* VotesViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = VotesViewLayout.swift; sourceTree = ""; }; 862545D6BD501914AE04D776 /* NPoolsRedeemViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsRedeemViewController.swift; sourceTree = ""; }; + 8632A6D9689942DE89F174FA /* SwapExecutionInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapExecutionInteractor.swift; sourceTree = ""; }; 86DCB6F3977BDE1BDC7BC3F9 /* ParaStkUnstakeConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkUnstakeConfirmPresenter.swift; sourceTree = ""; }; 86F7A369E31DCB9ABD556EE9 /* CrowdloanListPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanListPresenter.swift; sourceTree = ""; }; 86F7ACFB151C31B3A5044DCD /* StakingSelectPoolInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingSelectPoolInteractor.swift; sourceTree = ""; }; @@ -9581,6 +9862,7 @@ 899686C7351A2600FFA08371 /* TransferConfirmOnChainViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferConfirmOnChainViewFactory.swift; sourceTree = ""; }; 89AAAF09837D225A05769A7D /* ProxySignValidationPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ProxySignValidationPresenter.swift; sourceTree = ""; }; 89CFED2E01AB638656E251AF /* NftListProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftListProtocols.swift; sourceTree = ""; }; + 8A3DA31DE8C8AD1D82D66DEF /* SwapRouteDetailsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapRouteDetailsViewFactory.swift; sourceTree = ""; }; 8B0BF8DFAA80B405D4A5D891 /* GovernanceUnavailableTracksViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceUnavailableTracksViewFactory.swift; sourceTree = ""; }; 8B4C1B5D56DB69BA0AECF731 /* ExportSeedWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExportSeedWireframe.swift; sourceTree = ""; }; 8B56BDC7E6221DE292498D3A /* ParitySignerTxQrViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerTxQrViewFactory.swift; sourceTree = ""; }; @@ -9689,6 +9971,7 @@ A2AE82FB3A09A06A2CB1BF24 /* CloudBackupReviewChangesProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudBackupReviewChangesProtocols.swift; sourceTree = ""; }; A2BC4EFADD593325FF122765 /* StartStakingInfoBaseInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StartStakingInfoBaseInteractor.swift; sourceTree = ""; }; A2E14458DEC3317602A17527 /* GovernanceUnlockSetupViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceUnlockSetupViewLayout.swift; sourceTree = ""; }; + A2F8A21F1E63BF16DCCE89FE /* SwapExecutionProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapExecutionProtocols.swift; sourceTree = ""; }; A2FCBFC59ED9D7E6F046D2A1 /* GovernanceYourDelegationsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceYourDelegationsProtocols.swift; sourceTree = ""; }; A3104ABC4BECF08B0BA836AA /* AccountConfirmViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountConfirmViewController.swift; sourceTree = ""; }; A313F1654986753E341D7D39 /* StakingProxyManagementWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingProxyManagementWireframe.swift; sourceTree = ""; }; @@ -9976,6 +10259,7 @@ C2D8A1ACC06F8EA9CE6C982B /* BackupAttentionViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BackupAttentionViewController.swift; sourceTree = ""; }; C3971167D84B9FABEED47DE4 /* GenericLedgerWalletViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GenericLedgerWalletViewFactory.swift; sourceTree = ""; }; C3ABAD23C0039AFA8351C650 /* ReferendumDetailsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumDetailsProtocols.swift; sourceTree = ""; }; + C3E1250905B8D8E175316B0A /* SwapExecutionViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapExecutionViewFactory.swift; sourceTree = ""; }; C3E8ACEB2D157E376D53C6DE /* NominationPoolBondMoreConfirmViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreConfirmViewFactory.swift; sourceTree = ""; }; C4E807E9E12A130C50E8FFDF /* StakingDashboardViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingDashboardViewFactory.swift; sourceTree = ""; }; C503100478AB56E903598A78 /* ReferralCrowdloanPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferralCrowdloanPresenter.swift; sourceTree = ""; }; @@ -10108,6 +10392,7 @@ DF715CEF29477B59119520F1 /* ParitySignerAddressesInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerAddressesInteractor.swift; sourceTree = ""; }; DFF58EC3A44E4DDDFB4B5C84 /* ParaStkYieldBoostStopViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostStopViewLayout.swift; sourceTree = ""; }; E0B67F8F7D79D9546B1B92FA /* SwipeGovVotingConfirmViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwipeGovVotingConfirmViewFactory.swift; sourceTree = ""; }; + E0D139E39F5ECE929FF724B8 /* SwapExecutionViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapExecutionViewController.swift; sourceTree = ""; }; E0DB5EA5195D9433A4B90793 /* AdvancedWalletWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AdvancedWalletWireframe.swift; sourceTree = ""; }; E11575D8B4F64C2E805372A5 /* AccountExportPasswordViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountExportPasswordViewFactory.swift; sourceTree = ""; }; E12E4AA5C56575FD3ABA7693 /* NPoolsClaimRewardsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsClaimRewardsProtocols.swift; sourceTree = ""; }; @@ -10173,6 +10458,7 @@ EEB918ED70D77D1832094A8A /* LedgerTxConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerTxConfirmWireframe.swift; sourceTree = ""; }; EEBBEC474F607DD9F2A0F4FD /* AccountCreatePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountCreatePresenter.swift; sourceTree = ""; }; EED9939B17C4224C8E153F8A /* SelectValidatorsStartProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectValidatorsStartProtocols.swift; sourceTree = ""; }; + EEDD7BBAB8B408BC84477468 /* SwapRouteDetailsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapRouteDetailsViewLayout.swift; sourceTree = ""; }; EEEF1360E04CD27CCC13E472 /* ReferendumsFiltersPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumsFiltersPresenter.swift; sourceTree = ""; }; EF3AD755B2B3DCFB3D14DF91 /* ExportMnemonicProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExportMnemonicProtocols.swift; sourceTree = ""; }; EF4ED0EE2A3EF620DC51870B /* CrowdloanYourContributionsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanYourContributionsPresenter.swift; sourceTree = ""; }; @@ -10185,6 +10471,7 @@ F080BC55D9575EBE4216283C /* MarkdownDescriptionViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MarkdownDescriptionViewController.swift; sourceTree = ""; }; F08E58D49C9B543CC9A16EE3 /* SwipeGovVotingListViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwipeGovVotingListViewController.swift; sourceTree = ""; }; F0C9473AB7D1FE4A27403078 /* NominationPoolBondMoreSetupViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreSetupViewController.swift; sourceTree = ""; }; + F0FC3C19F3DEE499283E6468 /* SwapFeeDetailsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapFeeDetailsViewController.swift; sourceTree = ""; }; F1A9B9D741BABBCE6C70BE45 /* LedgerDiscoverProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerDiscoverProtocols.swift; sourceTree = ""; }; F23E38DCBC74C528D7839B76 /* CrowdloanContributionSetupInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionSetupInteractor.swift; sourceTree = ""; }; F23EDFB699CAEEADC9263A0D /* DAppAuthSettingsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppAuthSettingsViewFactory.swift; sourceTree = ""; }; @@ -10331,6 +10618,8 @@ F90A76631A8ED5D69C9F0A57 /* GovernanceNotificationsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceNotificationsViewFactory.swift; sourceTree = ""; }; F9DE9E86B1B598C26CB9AA78 /* Pods-novawalletTests.staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletTests.staging.xcconfig"; path = "Target Support Files/Pods-novawalletTests/Pods-novawalletTests.staging.xcconfig"; sourceTree = ""; }; F9F3BD600F80ED0426141843 /* SwapSlippageViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSlippageViewController.swift; sourceTree = ""; }; + FA1285B676EA226704C14DDB /* SwapExecutionWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapExecutionWireframe.swift; sourceTree = ""; }; + FA18C9D87DC9E878D101CB91 /* SwapFeeDetailsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapFeeDetailsViewFactory.swift; sourceTree = ""; }; FA3F824117720D3CE65A195F /* LedgerDiscoverPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerDiscoverPresenter.swift; sourceTree = ""; }; FA59CE2C7AE548ACA9D66FD7 /* CrowdloanContributionConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionConfirmWireframe.swift; sourceTree = ""; }; FA8FE2D2EF1036543F55D410 /* OnboardingImportOptionsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = OnboardingImportOptionsInteractor.swift; sourceTree = ""; }; @@ -10609,35 +10898,49 @@ path = Amount; sourceTree = ""; }; - 0C0CB37C2AC5408000EAC516 /* AssetConversion */ = { + 0C03877D2D0A1007000A2F24 /* View */ = { isa = PBXGroup; children = ( - 0C0CB3802AC5459200EAC516 /* Service */, - 0C0CB37D2AC5408900EAC516 /* Model */, + 0C03877E2D0A1043000A2F24 /* SwapRouteDetailsItemView.swift */, + 0C0387862D0A24E6000A2F24 /* SwapRouteDetailsView.swift */, ); - path = AssetConversion; + path = View; sourceTree = ""; }; - 0C0CB37D2AC5408900EAC516 /* Model */ = { + 0C0387882D0A2770000A2F24 /* ViewModel */ = { isa = PBXGroup; children = ( - 0C0CB37E2AC540B200EAC516 /* AssetConversion.swift */, + 0C0387892D0A2785000A2F24 /* SwapRouteDetailsViewModelFactory.swift */, ); - path = Model; + path = ViewModel; sourceTree = ""; }; - 0C0CB3802AC5459200EAC516 /* Service */ = { + 0C0387932D0CD080000A2F24 /* View */ = { isa = PBXGroup; children = ( - 0C1CCC3D2B5F862D00A6EA17 /* HydraDx */, + 0C0387942D0CD120000A2F24 /* SwapOperationFeeView.swift */, + ); + path = View; + sourceTree = ""; + }; + 0C0387962D0CE5EF000A2F24 /* ViewModel */ = { + isa = PBXGroup; + children = ( + 0C0387972D0CE605000A2F24 /* SwapFeeDetailsViewModel.swift */, + 0C0387992D0CEDC9000A2F24 /* SwapFeeDetailsViewModelFactory.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 0C0CB37C2AC5408000EAC516 /* AssetConversion */ = { + isa = PBXGroup; + children = ( + 0C0CB37E2AC540B200EAC516 /* AssetConversion.swift */, + 0CD352962ACAFADA00B3E446 /* AssetConversionOperationError.swift */, 0C0CB3832AC561CA00EAC516 /* AssetHub */, - 0C0CB3812AC545A800EAC516 /* AssetConversionExtrinsicService.swift */, - 0CD352962ACAFADA00B3E446 /* AssetConversionOperationFactory.swift */, - 0CD3A67B2AEAA3B90059BBEC /* AssetConversionFeeService.swift */, - 0CEB4ED22AF1689D0048FD84 /* AssetConversionAggregationFactory.swift */, - 0C85FF312B6D4A3500FC0014 /* AssetConversionFlowState.swift */, + 0C1CCC3D2B5F862D00A6EA17 /* HydraDx */, ); - path = Service; + path = AssetConversion; sourceTree = ""; }; 0C0CB3832AC561CA00EAC516 /* AssetHub */ = { @@ -10646,13 +10949,8 @@ 0C0CB3842AC561FB00EAC516 /* AssetHubSwapOperationFactory.swift */, 0C9D87AD2AC708070095FE8C /* AssetHubTokensConverter.swift */, 0C2200682ACA80BB0067BA61 /* AssetHubSwapQuoteBuilder.swift */, - 0CD352922ACAD7A500B3E446 /* AssetHubExtrinsicService.swift */, - 0CD3A67D2AEAAB670059BBEC /* AssetHubFeeService.swift */, 0C85FF232B6CB28200FC0014 /* AssetHubExtrinsicConverter.swift */, - 0C85FF272B6CBA9D00FC0014 /* AssetHubCallPathFactory.swift */, 0C85FF292B6D230100FC0014 /* AssetHubReQuoteService.swift */, - 0C85FF2F2B6D35D600FC0014 /* AssetHubFlowState.swift */, - 0C85FF362B6E0BB300FC0014 /* AssetConversionAggregationFactory+AssetHub.swift */, ); path = AssetHub; sourceTree = ""; @@ -10672,6 +10970,8 @@ 0C0E0A9F2B3F013800865F10 /* BalancesPallet.swift */, 0C1998DF2C496E4B000EBFB8 /* BalancePallet+Holds.swift */, 0C1998E72C49B5B9000EBFB8 /* BalancesPallet+StoragePath.swift */, + 0CA7CEEF2CE0CC13004328F2 /* BalancesPallet+EventCodingPath.swift */, + 8454C2692632B8CE00657DAD /* BalancesPallet+Events.swift */, ); path = BalancesPallet; sourceTree = ""; @@ -10684,6 +10984,16 @@ path = Uniques; sourceTree = ""; }; + 0C12BC382CEA7E8500AB919D /* Price */ = { + isa = PBXGroup; + children = ( + 0C12BC362CEA7DCB00AB919D /* AssetExchangePrice.swift */, + 0C12BC392CEA7ECB00AB919D /* AssetExchangePriceStore.swift */, + 0C0387502D066474000A2F24 /* AssetExchageUsdtConverter.swift */, + ); + path = Price; + sourceTree = ""; + }; 0C13D2F82A7D469E0054BB6F /* Recommendation */ = { isa = PBXGroup; children = ( @@ -10746,30 +11056,102 @@ path = Model; sourceTree = ""; }; - 0C1CCC3D2B5F862D00A6EA17 /* HydraDx */ = { + 0C19CC9D2CC6EB82007F8ED8 /* CrosschainExchange */ = { + isa = PBXGroup; + children = ( + 0CB433452CC0C3F200F2CB59 /* CrosschainAssetsExchangeProvider.swift */, + 0C19CC9E2CC6EBBC007F8ED8 /* CrosschainAssetsExchange.swift */, + 0C2DA89D2CC21853001F79C8 /* CrosschainExchangeEdge.swift */, + 0CCA70722CD0A4FD0082A9C8 /* CrosschainExchangeAtomicOperation.swift */, + 0C3272922CD246DD00FC1B42 /* AssetExchangeOperationFee+Crosschain.swift */, + 0C6108272CD53CDE00909928 /* CrosschainExchangeHost.swift */, + 0CBABFE62CED31690047F29E /* CrosschainExchangeMetaOperation.swift */, + 0CBABFF22CED47D10047F29E /* CrosschainExchangeOperationPrototype.swift */, + ); + path = CrosschainExchange; + sourceTree = ""; + }; + 0C19CCA02CC6EC36007F8ED8 /* AssetHubExchange */ = { + isa = PBXGroup; + children = ( + 0C883A732CE241CB00CAB4C8 /* AssetConversionEventsMatching.swift */, + 0CB433472CC0F9EF00F2CB59 /* AssetsHubExchange.swift */, + 0C19CCA12CC6EC75007F8ED8 /* AssetsHubExchangeProvider.swift */, + 0C2DA8A12CC21E5F001F79C8 /* AssetHubExchangeEdge.swift */, + 0C3272942CD24F9600FC1B42 /* AssetHubExchangeAtomicOperation.swift */, + 0C32729B2CD28C4000FC1B42 /* AssetExchangeOperationFee+AssetHub.swift */, + 0C6108292CD5DE9500909928 /* AssetHubExchangeHost.swift */, + 0C883A772CE2640000CAB4C8 /* AssetConversionEventParser.swift */, + 0CBABFE82CED31C80047F29E /* AssetHubExchangeMetaOperation.swift */, + 0CBABFF02CED46C90047F29E /* AssetHubExchangeOperationPrototype.swift */, + 0C85FF2F2B6D35D600FC0014 /* AssetHubFlowState.swift */, + ); + path = AssetHubExchange; + sourceTree = ""; + }; + 0C19CCA32CC6ED9D007F8ED8 /* Common */ = { + isa = PBXGroup; + children = ( + 0C2DA89B2CC215D0001F79C8 /* AssetExchangeGraphEdge.swift */, + 0C2DA8A92CC2A2E2001F79C8 /* AnyAssetExchangeEdge.swift */, + 0C2DA8992CC21419001F79C8 /* GraphQuotableEdge.swift */, + 0C19CCA42CC6EDAE007F8ED8 /* AssetsExchangeBaseProvider.swift */, + 0CF5F6892CC7AF84007BAAC5 /* AssetExchangeGraphPath.swift */, + 0CF5F68B2CC7B1B6007BAAC5 /* AssetsExchageGraphReachability.swift */, + 0C32728C2CD1F90100FC1B42 /* AssetExchangeSwapLimit.swift */, + 0C3272902CD2409B00FC1B42 /* AssetExchangeOperationFee.swift */, + 0C3272882CD1F83D00FC1B42 /* AssetExchangeAtomicOperationArgs.swift */, + 0C11D85F2CCA486B003EC46D /* AssetExchangeRoute.swift */, + 0C61084C2CD8909600909928 /* AssetExchangeFee.swift */, + 0C746DBD2CCF440800E9178B /* AssetExchangeAtomicOperation.swift */, + 0C6353462CE46FB200EAB200 /* AssetExchangeSufficiencyProvider.swift */, + 0CA81D582CE60AA700166969 /* AssetExchangeEdgeType.swift */, + 0C12BC3D2CEA9DF800AB919D /* AssetExchangeTimeEstimation.swift */, + 0CF4ADF82CEBF71A009C51FA /* AssetExchangeMetaOperationProtocol.swift */, + 0CBABFEA2CED37E30047F29E /* AssetExchangeQuote.swift */, + 0CBABFEC2CED438A0047F29E /* AssetExchangeOperationPrototype.swift */, + 0CE94FF12D046A8700E31D0C /* AssetExchangeFeePayerMatcher.swift */, + ); + path = Common; + sourceTree = ""; + }; + 0C19CCA62CC6F121007F8ED8 /* HydraExchange */ = { isa = PBXGroup; children = ( 0C3976672C325E1100998DD6 /* XYKPool */, 0CA719812B787DF5000B086E /* Stableswap */, 0CA719802B787D9E000B086E /* Omnipool */, + 0C883A712CE2235600CAB4C8 /* HydraSwapEventsMatcher.swift */, + 0CB4334B2CC10C3C00F2CB59 /* AssetsHydraExchangeProvider.swift */, + 0C2DA8A32CC223D9001F79C8 /* AssetsHydraExchangeEdge.swift */, + 0C32729D2CD2975400FC1B42 /* HydraExchangeAtomicOperation.swift */, + 0C32729F2CD34CB200FC1B42 /* HydraExchangeExtrinsicParamsFactory.swift */, + 0C3272A12CD34F7700FC1B42 /* HydraExchangeExtrinsicConverter.swift */, + 0C6108252CD5227900909928 /* HydraExchangeHost.swift */, + 0C883A752CE25E6800CAB4C8 /* HydraSwapEventParser.swift */, + 0CF4ADFC2CEC0A54009C51FA /* HydraExchangeMetaOperation.swift */, + 0CBABFEE2CED46200047F29E /* HydraExchangeOperationPrototype.swift */, + ); + path = HydraExchange; + sourceTree = ""; + }; + 0C1CCC3D2B5F862D00A6EA17 /* HydraDx */ = { + isa = PBXGroup; + children = ( 0CCCDF832B64C63D00473D42 /* HydraDxTokenConverter.swift */, 0C363E972B6B63280065AFA6 /* HydraConstants.swift */, - 0C363E992B6B69D60065AFA6 /* HydraFeeService.swift */, - 0C363E952B6B591C0065AFA6 /* HydraExtrinsicOperationFactory.swift */, 0C363E9B2B6B8C400065AFA6 /* HydraExtrinsicConverter.swift */, - 0C85FF212B6C001900FC0014 /* HydraSwapExtrinsicService.swift */, 0CA957212B6A507A009AD757 /* HydraSwapParamsService.swift */, + 0C6108442CD8827200909928 /* HydraSwapFeeCurrencyService.swift */, 0C7A3FCD2B6A874500764603 /* HydraSwapRemoteState.swift */, - 0C85FF252B6CB9B400FC0014 /* HydraCallPathFactory.swift */, + 0C6108462CD882F900909928 /* SwapFeeCurrencyState.swift */, 0CCDB2E32B749523007BC5D6 /* HydraTokensFactory.swift */, - 0C85FF382B6E0C0600FC0014 /* AssetConversionAggregationFactory+HydraDx.swift */, 0CA7197C2B7737D5000B086E /* HydraQuoteFactory.swift */, 0CA7197E2B773E99000B086E /* HydraSwapRoute.swift */, 0CA719822B787FCA000B086E /* HydraFlowState.swift */, - 2D95DF602C74B79A009BB063 /* HydraFlowStateStore.swift */, - 0CA7198D2B791811000B086E /* HydraReQuoteService.swift */, 0CA7198F2B79C92E000B086E /* HydraRoutesOperationFactory.swift */, 0CA719972B79D033000B086E /* HydraRoutesResolver.swift */, + 0C03879B2D0F64F9000A2F24 /* HydraSwapParams.swift */, ); path = HydraDx; sourceTree = ""; @@ -10827,6 +11209,24 @@ path = Model; sourceTree = ""; }; + 0C2A3C962CDCCEB200A0E2B3 /* Model */ = { + isa = PBXGroup; + children = ( + 0C2A3C972CDCCECC00A0E2B3 /* SwapTokensFlowState.swift */, + ); + path = Model; + sourceTree = ""; + }; + 0C2DA8AB2CC2B120001F79C8 /* AssetsExchange */ = { + isa = PBXGroup; + children = ( + 0C2DA8AC2CC2B156001F79C8 /* AssetsExchangeTests.swift */, + 0C2DA8AE2CC2F928001F79C8 /* AssetsExchangeGraphDescription.swift */, + 0C03877B2D09A1AA000A2F24 /* MockAssetExchangePathCostEstimator.swift */, + ); + path = AssetsExchange; + sourceTree = ""; + }; 0C2F86872A723E4200593C01 /* NominationPools */ = { isa = PBXGroup; children = ( @@ -10849,11 +11249,12 @@ 0C2FDF172AEB8292006A6C59 /* Model */ = { isa = PBXGroup; children = ( - 0C2FDF182AEB82AA006A6C59 /* AssetHubFeeModelBuilder.swift */, 0C13DFCC2AF8A5A300E5F355 /* SwapMaxModel.swift */, 0C13DFDA2AFA691400E5F355 /* SlippageConfig.swift */, 0C13DFE02AFBBAF600E5F355 /* SwapBaseViewModelFactory.swift */, + 771901AA2AE9581400D9C918 /* SwapDetailsViewModelFactory.swift */, 0C9A7F982AFC9A2B00938CD0 /* SwapPriceDifferenceConfig.swift */, + 0CE94FF32D04969100E31D0C /* SwapInterEDNotMet.swift */, ); path = Model; sourceTree = ""; @@ -10971,6 +11372,8 @@ 0C3976AC2C33A64700998DD6 /* HydraXYKQuoteParamsService.swift */, 0C3976AE2C33A6C400998DD6 /* HydraXYKQuoteRemoteState.swift */, 0C3976B02C33ABA600998DD6 /* HydraXYKFlowState.swift */, + 0C11D8552CC9F9E6003EC46D /* HydraXYKExchangeEdge.swift */, + 0C11D85B2CCA3CCC003EC46D /* AssetsHydraXYKExchange.swift */, ); path = XYKPool; sourceTree = ""; @@ -11002,6 +11405,15 @@ path = CloudBackup; sourceTree = ""; }; + 0C423AB92CDBAE7300A64941 /* Facade */ = { + isa = PBXGroup; + children = ( + 0C423ABA2CDBAE9B00A64941 /* AssetExchangeFacade.swift */, + 0C423ABC2CDBAEC900A64941 /* AssetExchangeGraphProvidingParams.swift */, + ); + path = Facade; + sourceTree = ""; + }; 0C4394D52BD7617200578761 /* Model */ = { isa = PBXGroup; children = ( @@ -11132,6 +11544,57 @@ path = OperationDataProviders; sourceTree = ""; }; + 0C61082B2CD5E55000909928 /* AssetHub */ = { + isa = PBXGroup; + children = ( + 2D32BE062C6A49900047F520 /* ExtrinsicAssetConversionFeeInstaller.swift */, + ); + path = AssetHub; + sourceTree = ""; + }; + 0C61082C2CD5EC4200909928 /* Model */ = { + isa = PBXGroup; + children = ( + 0C8AF7A92B33455A00005AC9 /* ExtrinsicFee.swift */, + 2D32BE0C2C6A49900047F520 /* ExtrinsicFeeProxy.swift */, + 2D32BE0F2C6A49900047F520 /* MultiExtrinsicFeeProxy.swift */, + 2D32BE102C6A49900047F520 /* TransactionFeeProxy.swift */, + ); + path = Model; + sourceTree = ""; + }; + 0C61082D2CD5EC5000909928 /* Native */ = { + isa = PBXGroup; + children = ( + 2D32BE0D2C6A49900047F520 /* ExtrinsicNativeFeeEstimator.swift */, + 2D32BE0E2C6A49900047F520 /* ExtrinsicNativeFeeInstaller.swift */, + ); + path = Native; + sourceTree = ""; + }; + 0C61082E2CD5EC9300909928 /* FeeViaSwap */ = { + isa = PBXGroup; + children = ( + 0C61082B2CD5E55000909928 /* AssetHub */, + 2D95DF592C6F7D53009BB063 /* Hydra */, + 2D32BE052C6A49900047F520 /* ExtrinsicAssetConversionFeeEstimator.swift */, + 0C6108312CD5EEBD00909928 /* AssetConversionFeeEstimatingFactory.swift */, + 0C6108332CD5F77E00909928 /* ExtrinsicFeeEstimatorHost.swift */, + 0C61083D2CD7382E00909928 /* AssetConversionFeeInstallingFactory.swift */, + 2D95DF602C74B79A009BB063 /* AssetConversionFeeSharedStateStore.swift */, + ); + path = FeeViaSwap; + sourceTree = ""; + }; + 0C61083F2CD74D5600909928 /* FeeEstimating */ = { + isa = PBXGroup; + children = ( + 0C6108402CD74DD200909928 /* AssetExchangeFeeEstimatingFactory.swift */, + 0C6108422CD755A300909928 /* AssetExchangeGraphProxy.swift */, + ); + path = FeeEstimating; + sourceTree = ""; + }; 0C6610292A73814900E44634 /* StakingSharedState */ = { isa = PBXGroup; children = ( @@ -11409,6 +11872,33 @@ path = PromotionBannerView; sourceTree = ""; }; + 0C9BCC562CBD6B1600CBE21B /* AssetExchange */ = { + isa = PBXGroup; + children = ( + 0C12BC382CEA7E8500AB919D /* Price */, + 0CA81D552CE5AE6200166969 /* FeeCapability */, + 0C423AB92CDBAE7300A64941 /* Facade */, + 0C61083F2CD74D5600909928 /* FeeEstimating */, + 0C19CCA32CC6ED9D007F8ED8 /* Common */, + 0C19CCA62CC6F121007F8ED8 /* HydraExchange */, + 0C19CCA02CC6EC36007F8ED8 /* AssetHubExchange */, + 0C19CC9D2CC6EB82007F8ED8 /* CrosschainExchange */, + 0C9BCC572CBD6B4000CBE21B /* AssetsExchangeProtocol.swift */, + 0C2DA8A52CC265F0001F79C8 /* AssetsExchangeGraphProvider.swift */, + 0C2DA8A72CC2679E001F79C8 /* AssetsExchangeGraph.swift */, + 0C11D85D2CCA470D003EC46D /* AssetsExchangeRouteManager.swift */, + 0C746DBB2CCE5E9900E9178B /* AssetExchangeExecutionManager.swift */, + 0C6108482CD889A000909928 /* AssetsExchange.swift */, + 0C61084A2CD88F9B00909928 /* AssetsExchangeService.swift */, + 0C2A3C922CDC813B00A0E2B3 /* AssetsExchangeOperationFactory.swift */, + 0C6353442CE463BD00EAB200 /* AssetExchangePathFilter.swift */, + 0CF976802CFD04F8001D2801 /* AssetsExchangeStateMediator.swift */, + 0CE94FF52D04A3C100E31D0C /* AssetsExchangePathCostEstimator.swift */, + 0CE94FF72D050EE300E31D0C /* AssetExchangeOperationPrototypeFactory.swift */, + ); + path = AssetExchange; + sourceTree = ""; + }; 0C9C642B2A8CE2D4004DC078 /* SystemAccounts */ = { isa = PBXGroup; children = ( @@ -11481,6 +11971,8 @@ 0C85FF2D2B6D300800FC0014 /* HydraOmnipoolFlowState.swift */, 0C85FF332B6D523B00FC0014 /* HydraOmnipoolQuoteFactory.swift */, 0CA719842B789111000B086E /* HydraOmnipoolQuoteParams.swift */, + 0C11D8592CCA3377003EC46D /* AssetsHydraOmnipoolExchange.swift */, + 0C11D8512CC9F5F2003EC46D /* HydraOmnipoolExchangeEdge.swift */, ); path = Omnipool; sourceTree = ""; @@ -11498,6 +11990,8 @@ 0CCDB2F82B75E21F007BC5D6 /* HydraStableswapQuoteParams.swift */, 0CCDB2FA2B75E938007BC5D6 /* HydraStableswapQuoteFactory.swift */, 0CA719992B79DF5B000B086E /* HydraStableswapApi.swift */, + 0C11D8532CC9F82C003EC46D /* HydraStableswapExchangeEdge.swift */, + 0C11D8572CC9FED7003EC46D /* AssetsHydraStableswapExchange.swift */, ); path = Stableswap; sourceTree = ""; @@ -11506,7 +12000,6 @@ isa = PBXGroup; children = ( 0CCCDF852B64CCC100473D42 /* HydraOmnipoolSwapTests.swift */, - 0C363E9D2B6BB2E20065AFA6 /* HydraSwapsFeeTests.swift */, 0CA7197A2B773172000B086E /* HydraStableswapTests.swift */, 0CA7198B2B78FEF9000B086E /* HydraQuoteTests.swift */, 0C39766E2C326C4E00998DD6 /* HydraXYKTests.swift */, @@ -11514,6 +12007,55 @@ path = HydraDx; sourceTree = ""; }; + 0CA7CEE52CE0C23B004328F2 /* System */ = { + isa = PBXGroup; + children = ( + 0CA7CEE32CE0C23B004328F2 /* SystemPallet+Events.swift */, + 0CA7CEE42CE0C23B004328F2 /* SystemPallet+StoragePath.swift */, + 0CA7CEE82CE0C37B004328F2 /* SystemPallet.swift */, + ); + path = System; + sourceTree = ""; + }; + 0CA7CEEA2CE0CA13004328F2 /* TokenDepositEventMatching */ = { + isa = PBXGroup; + children = ( + 0CA7CEEB2CE0CA57004328F2 /* NativeTokenDepositEventMatcher.swift */, + 0CA7CEED2CE0CA75004328F2 /* TokenDepositEventMatching.swift */, + 0CA7CEF12CE0D14F004328F2 /* PalletAssetsTokenDepositEventMatcher.swift */, + 0CA7CEF72CE0D575004328F2 /* TokensPalletDepositEventMatcher.swift */, + 0CA7CEF92CE13126004328F2 /* TokenDepositEventMatcherFactory.swift */, + ); + path = TokenDepositEventMatching; + sourceTree = ""; + }; + 0CA81D552CE5AE6200166969 /* FeeCapability */ = { + isa = PBXGroup; + children = ( + 0CA81D532CE5108A00166969 /* AssetHubExchangeFeeSupportFetcher.swift */, + 0CA81D512CE50BDB00166969 /* HydraExchangeFeeSupportFetcher.swift */, + 0CA81D4B2CE4FF8800166969 /* AssetExchangeGraphFeeSupport.swift */, + 0CA81D4F2CE502B200166969 /* AssetExchangeFeeSupport.swift */, + 0CA81D562CE5AF2600166969 /* AssetExchangeFeeSupportFetchersProvider.swift */, + 0C33E8B52D0069EC0090096A /* AssetsExchangeFeeSupportProvider.swift */, + ); + path = FeeCapability; + sourceTree = ""; + }; + 0CA8AD612CE08FEE00ED9746 /* ExtrinsicStatusService */ = { + isa = PBXGroup; + children = ( + 0CA8AD5B2CE08FEE00ED9746 /* BlockEventsQueryFactory.swift */, + 0CA8AD5C2CE08FEE00ED9746 /* ExtrinsicEventsMatching.swift */, + 0CA8AD5D2CE08FEE00ED9746 /* ExtrinsicStatusService.swift */, + 0CA8AD5E2CE08FEE00ED9746 /* ExtrinsicSubmissionMonitor.swift */, + 0CA8AD5F2CE08FEE00ED9746 /* SubstrateExtrinsicEvents.swift */, + 0CA8AD602CE08FEE00ED9746 /* SubstrateExtrinsicStatus.swift */, + 0CA7CEE12CE0BBE9004328F2 /* SubstrateEventsRepository.swift */, + ); + path = ExtrinsicStatusService; + sourceTree = ""; + }; 0CA957232B6A565D009AD757 /* HydraDx */ = { isa = PBXGroup; children = ( @@ -11703,6 +12245,15 @@ path = View; sourceTree = ""; }; + 0CBABFF72CEF33AE0047F29E /* View */ = { + isa = PBXGroup; + children = ( + 0CBABFF82CEF33BE0047F29E /* SwapExecutionDetailsView.swift */, + 0CF976782CF09076001D2801 /* SwapExecutionView.swift */, + ); + path = View; + sourceTree = ""; + }; 0CC0F7EE2BADEA6F002C49F6 /* Config */ = { isa = PBXGroup; children = ( @@ -11834,7 +12385,10 @@ children = ( 0CCDB2EA2B74DA55007BC5D6 /* GraphModel.swift */, 0CE360202C33F398006A6CE4 /* GraphModel+Dijkstra.swift */, + 0C6108502CD9D3BA00909928 /* GraphModel+BFS.swift */, 0CE360222C343300006A6CE4 /* PriorityQueue.swift */, + 0CB4334D2CC1196400F2CB59 /* WeightableEdge.swift */, + 0C61084E2CD92C6F00909928 /* GraphEdgeFiltering.swift */, ); path = Graphs; sourceTree = ""; @@ -12045,6 +12599,17 @@ path = AssetConverters; sourceTree = ""; }; + 0CF3A6C72CE9422400F93C49 /* View */ = { + isa = PBXGroup; + children = ( + 0CF3A6C82CE9423000F93C49 /* RouteView.swift */, + 0CF3A6CA2CE949ED00F93C49 /* SwapRouteView.swift */, + 0C0387822D0A1878000A2F24 /* AssetAmountRouteItemView.swift */, + 0C0387842D0A1C0A000A2F24 /* LabelRouteItemView.swift */, + ); + path = View; + sourceTree = ""; + }; 0CF6928A2C231C89000FC395 /* SharedStatusView */ = { isa = PBXGroup; children = ( @@ -12070,6 +12635,25 @@ path = Monitor; sourceTree = ""; }; + 0CF976702CEFF00E001D2801 /* Model */ = { + isa = PBXGroup; + children = ( + 0CF976712CEFF01D001D2801 /* SwapExecutionModel.swift */, + 0CF9767E2CF1293E001D2801 /* SwapExecutionState.swift */, + ); + path = Model; + sourceTree = ""; + }; + 0CF976732CF082B2001D2801 /* ViewModel */ = { + isa = PBXGroup; + children = ( + 0CF976742CF082BF001D2801 /* SwapExecutionViewModel.swift */, + 0CF9767A2CF09B86001D2801 /* SwapExecutionViewModelFactory.swift */, + 0CF9767C2CF1274F001D2801 /* AssetExchangeMetaOperationLabel+Display.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; 0CFA16142B0CD8A9007AF885 /* Parser */ = { isa = PBXGroup; children = ( @@ -12630,20 +13214,17 @@ 2D32BE112C6A49900047F520 /* FeeManaging */ = { isa = PBXGroup; children = ( - 2D95DF592C6F7D53009BB063 /* Hydra */, - 2D32BE052C6A49900047F520 /* ExtrinsicAssetConversionFeeEstimator.swift */, - 2D32BE062C6A49900047F520 /* ExtrinsicAssetConversionFeeInstaller.swift */, + 0C61082E2CD5EC9300909928 /* FeeViaSwap */, + 0C61082C2CD5EC4200909928 /* Model */, + 0C61082D2CD5EC5000909928 /* Native */, 2D32BE082C6A49900047F520 /* ExtrinsicFeeEstimationProtocol.swift */, 2D32BE092C6A49900047F520 /* ExtrinsicFeeEstimationRegistry.swift */, 2D32BE0A2C6A49900047F520 /* ExtrinsicFeeEstimationResult.swift */, 2D32BE0B2C6A49900047F520 /* ExtrinsicFeeInstalling.swift */, 2D95DF5E2C6FAFF6009BB063 /* ExtrinsicFeeEstimatingWrapperFactory.swift */, - 0C8AF7A92B33455A00005AC9 /* ExtrinsicFee.swift */, - 2D32BE0C2C6A49900047F520 /* ExtrinsicFeeProxy.swift */, - 2D32BE0D2C6A49900047F520 /* ExtrinsicNativeFeeEstimator.swift */, - 2D32BE0E2C6A49900047F520 /* ExtrinsicNativeFeeInstaller.swift */, - 2D32BE0F2C6A49900047F520 /* MultiExtrinsicFeeProxy.swift */, - 2D32BE102C6A49900047F520 /* TransactionFeeProxy.swift */, + 0C61082F2CD5ED1400909928 /* ExtrinsicCustomFeeEstimatingFactoryProtocol.swift */, + 0C6108352CD5FC7300909928 /* ExtrinsicFeeInstallingWrapperFactory.swift */, + 0C6108372CD7333100909928 /* ExtrinsicCustomFeeInstallingWrapperFactory.swift */, ); path = FeeManaging; sourceTree = ""; @@ -13020,7 +13601,6 @@ 2D95DF592C6F7D53009BB063 /* Hydra */ = { isa = PBXGroup; children = ( - 2D95DF5A2C6F7D83009BB063 /* HydraExtrinsicAssetsCustomFeeEstimator.swift */, 2D95DF5C2C6F9129009BB063 /* HydraExtrinsicFeeInstaller.swift */, ); path = Hydra; @@ -13570,6 +14150,20 @@ path = MoonbeamTerms; sourceTree = ""; }; + 525CC4A0903874911A28B60E /* FeeDetails */ = { + isa = PBXGroup; + children = ( + 0C0387962D0CE5EF000A2F24 /* ViewModel */, + 0C0387932D0CD080000A2F24 /* View */, + 3A6CDEC83D8A51FD89673C31 /* SwapFeeDetailsProtocols.swift */, + 17CDE3DFBC17377C17ED99F4 /* SwapFeeDetailsPresenter.swift */, + F0FC3C19F3DEE499283E6468 /* SwapFeeDetailsViewController.swift */, + 1FBBC5F8D082B58B4FDFABD7 /* SwapFeeDetailsViewLayout.swift */, + FA18C9D87DC9E878D101CB91 /* SwapFeeDetailsViewFactory.swift */, + ); + path = FeeDetails; + sourceTree = ""; + }; 52E57E961FA41BD3ABDDCDF0 /* SwipeGovSetup */ = { isa = PBXGroup; children = ( @@ -13700,6 +14294,20 @@ path = StakingRewardFilters; sourceTree = ""; }; + 5F08F57C053DB946BD47EFE5 /* RouteDetails */ = { + isa = PBXGroup; + children = ( + 0C0387882D0A2770000A2F24 /* ViewModel */, + 0C03877D2D0A1007000A2F24 /* View */, + 0C2BD7CE1EF80FCD3569BD44 /* SwapRouteDetailsProtocols.swift */, + 399E91BE2E289FE301D7846A /* SwapRouteDetailsPresenter.swift */, + 461EFCB1DAACD7D5DBBB713C /* SwapRouteDetailsViewController.swift */, + EEDD7BBAB8B408BC84477468 /* SwapRouteDetailsViewLayout.swift */, + 8A3DA31DE8C8AD1D82D66DEF /* SwapRouteDetailsViewFactory.swift */, + ); + path = RouteDetails; + sourceTree = ""; + }; 5F9F36C10D734E30870F81AB /* KnownNetworksList */ = { isa = PBXGroup; children = ( @@ -13875,7 +14483,6 @@ 771901912AE2425400D9C918 /* Swaps */ = { isa = PBXGroup; children = ( - 7719018F2AE2424B00D9C918 /* SwapsValidationTests.swift */, 0C3976B22C33BFB700998DD6 /* HydraPoolAccountTests.swift */, ); path = Swaps; @@ -13892,6 +14499,7 @@ 77E304AA2AEBB214006FD6F0 /* SlippageBounds.swift */, 77E304AC2AEC16B2006FD6F0 /* SwapPriceDifferenceViewModelFactoryProtocol.swift */, 0C13DFD02AF8AE3E00E5F355 /* SwapBasePresenter.swift */, + 0C2A3C992CDCEB8A00A0E2B3 /* SwapAssetSelectionClosure.swift */, ); path = Base; sourceTree = ""; @@ -13902,6 +14510,7 @@ 771901A72AE8FF9F00D9C918 /* SwapNetworkFeeViewCell.swift */, 771901A52AE8FF7E00D9C918 /* SwapInfoViewCell.swift */, 77740BBF2AD4A80D00E8C06F /* SwapInfoView.swift */, + 0C12BC342CE952EF00AB919D /* SwapRouteViewCell.swift */, ); path = View; sourceTree = ""; @@ -13919,7 +14528,6 @@ 771901AE2AE9733200D9C918 /* Model */ = { isa = PBXGroup; children = ( - 771901AA2AE9581400D9C918 /* SwapConfirmViewModelFactory.swift */, 771901AF2AE97DA500D9C918 /* SwapConfirmViewModels.swift */, 77E304A82AEB9F76006FD6F0 /* SwapConfirmInitState.swift */, ); @@ -14349,6 +14957,7 @@ 77C9BCBD2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift */, 0C13DFD42AFA4F1500E5F355 /* SwapIssueCheckParams.swift */, 0C13DFD62AFA50A200E5F355 /* SwapIssueViewModelFactory.swift */, + 0CE94FEF2D03AF9800E31D0C /* SwapPreferredFeeAssetModel.swift */, ); path = Model; sourceTree = ""; @@ -14356,11 +14965,16 @@ 77C9BCBF2ACD2E0300022EA2 /* Swaps */ = { isa = PBXGroup; children = ( + 0CF3A6C72CE9422400F93C49 /* View */, + 0C2A3C962CDCCEB200A0E2B3 /* Model */, 7719018A2AE0E62500D9C918 /* Validation */, 288677D19FEB54E369E6B619 /* Slippage */, 771901A02AE7E33A00D9C918 /* Base */, 29BD7DA0076BA8BC3411221A /* Setup */, 7E5E800395DC908962C169CF /* Confirm */, + E53BE56E5726646BC073E502 /* Execution */, + 5F08F57C053DB946BD47EFE5 /* RouteDetails */, + 525CC4A0903874911A28B60E /* FeeDetails */, ); path = Swaps; sourceTree = ""; @@ -14375,6 +14989,7 @@ 77C9BCCD2ACD691300022EA2 /* SwapAssetsOperationViewFactory.swift */, 77C9BCCF2ACD8A3600022EA2 /* SwapAssetsOperationViewLayout.swift */, 77C9BCD12ACD8AF400022EA2 /* SwapAssetsOperationViewController.swift */, + 0C2A3C942CDC8ADB00A0E2B3 /* SwapAssetSelectionModel.swift */, ); path = Swaps; sourceTree = ""; @@ -14974,6 +15589,7 @@ 0C1CCC402B60138F00A6EA17 /* XcmDeliveryRequest.swift */, 0C1CCC422B6017E100A6EA17 /* XcmDeliveryFee.swift */, 0C1CCC462B60DA0C00A6EA17 /* XcmFeeModel.swift */, + 0C32728E2CD203F000FC1B42 /* XcmTotalFeeModel.swift */, ); path = Xcm; sourceTree = ""; @@ -15007,6 +15623,8 @@ 84155DE8253980D700A27058 /* Services */ = { isa = PBXGroup; children = ( + 0C0CB37C2AC5408000EAC516 /* AssetConversion */, + 0C9BCC562CBD6B1600CBE21B /* AssetExchange */, 0CC0F8202BB52C6F002C49F6 /* CloudBackup */, 77B011712B6CCCBB003A5868 /* Web3AlertService */, 77D2ED5D2B18A4E200A0E58C /* Proxy */, @@ -15745,6 +16363,7 @@ 8438E1D024BFAAD2001BDB13 /* novawalletIntegrationTests */ = { isa = PBXGroup; children = ( + 0C2DA8AB2CC2B120001F79C8 /* AssetsExchange */, 0C8FDBA72CAE537800775D7F /* SwipeGov */, 0C71046E2C29B60D00487E64 /* MetadataShortener */, 0CA26D8E2BBBBD9A0086748F /* CloudBackup */, @@ -15799,6 +16418,7 @@ 8438E1DC24C18F11001BDB13 /* Types */ = { isa = PBXGroup; children = ( + 0CA7CEE52CE0C23B004328F2 /* System */, 0C75E2942C3F923A005A6232 /* DelegatedStaking */, 0C38B5012B7A86BA00882A8B /* TokensPallet */, 0C38B4FE2B7A82E000882A8B /* TransactionPaymentPallet */, @@ -15829,7 +16449,6 @@ 848DAEFC282293BD00D56F55 /* ParachainStaking */, 8463A73725E3AA47003B8160 /* AccountInfo.swift */, 84BE207725E7D62100B4748C /* ActiveEraInfo.swift */, - 8454C2692632B8CE00657DAD /* BalanceDepositEvent.swift */, 84A6171A2625AC3E007B75E1 /* CallCodingPath.swift */, 8425EA9925EA83FA00C307C9 /* ChainData+Value.swift */, 84DB4E6B25EA740600A6DF41 /* ConstantCodingPath.swift */, @@ -15852,13 +16471,11 @@ 84F2FEFE25E7ADE7008338D5 /* ValidatorPrefs.swift */, 2AC7BC812731A1A1001D99B0 /* BalanceLock.swift */, 84F18D4927A1869E00CA7554 /* OrmlAccount.swift */, - 84FD19FD27A3447E008E5E68 /* BalancesWithdrawEvent.swift */, 8499FEC327BEC5F400712589 /* UniquesClassMetadata.swift */, 8499FEC527BEC68000712589 /* UniquesInstanceMetadata.swift */, 84F1CB3F27CF6BEF0095D523 /* UniquesClassDetails.swift */, 8430D6C42800040A00FFB6AE /* EthereumExecuted.swift */, 84A3B8A12836DA2600DE2669 /* LastAccountIdKeyWrapper.swift */, - 847A25B828D7BB1F006AC9F5 /* BalancesTransferEvent.swift */, 847A25BC28D7C0E7006AC9F5 /* TokenTransferedEvent.swift */, ); path = Types; @@ -17833,7 +18450,6 @@ 0CD8461C2BDCA7320026B5CA /* ExportWallet */, 0CD8461B2BDCA72B0026B5CA /* ImportWallet */, 0CD8461A2BDCA6670026B5CA /* Onboarding */, - 0C0CB37C2AC5408000EAC516 /* AssetConversion */, 88C5F079297EE429001CCADE /* InAppUpdates */, 845B891A2959608D00EE25B0 /* SecurityLayer */, ABAF6F503B172CEE34E19030 /* MarkdownDescription */, @@ -18178,6 +18794,7 @@ 77CC82A42A984EDA002D022F /* UINavigaionController+Pop.swift */, 0CD846262BDDF43A0026B5CA /* UIImage+Resize.swift */, 0CEB6B4E2CA6AC9700609DC2 /* UIEdgeInsets+Init.swift */, + 0CBABFF52CEEACB60047F29E /* RoundedButton+Set.swift */, ); path = UIKit; sourceTree = ""; @@ -18251,6 +18868,7 @@ 0C7CF4D52BD4E5350015DD45 /* KeychainProxy.swift */, 0CDEF1652C27EC83003878F2 /* RuntimeMetadataRepositoryFactory.swift */, 0C7104782C2AC0F300487E64 /* InMemoryCaching.swift */, + 0C33E8B92D011D2E0090096A /* Debouncer.swift */, ); path = Helpers; sourceTree = ""; @@ -18383,6 +19001,7 @@ 0C6D66AA2A8C0B6700AAB988 /* BaseSigner.swift */, 0C495B912B429BCC00B8D339 /* ProxySigningWrapper.swift */, 0C212FD62C1B4546003F1FE3 /* MetaAccountModel+CryptoType.swift */, + 0CCA70772CD0B4220082A9C8 /* SigningWrapperFactory+Async.swift */, ); path = Crypto; sourceTree = ""; @@ -18496,6 +19115,10 @@ 77740BBB2AD4A7B800E8C06F /* CollapsableContainerView.swift */, 77ECB46F2ACEEE2D0015CE9F /* NetworkFeeInfoView.swift */, 0CAC03052B4D86E000DDEC3A /* LocalizableControllerBackedView.swift */, + 0C11F0272CEFD99C008D19D2 /* CountdownLoadingView.swift */, + 0CF976762CF08EF9001D2801 /* OperationExecutionProgressView.swift */, + 0C0387802D0A1283000A2F24 /* AssetAmountView.swift */, + 0C03878B2D0B2686000A2F24 /* LinePatternView.swift */, ); path = View; sourceTree = ""; @@ -18830,6 +19453,8 @@ 8498534E2A17390900993977 /* PalletAssets.swift */, 84B73AD3279B4D570071AE16 /* AssetAccount.swift */, 84B73AD5279B4E0B0071AE16 /* AssetDetails.swift */, + 0CA7CEF32CE0D210004328F2 /* PalletAssets+EventCodingPath.swift */, + 0CA7CEF52CE0D35B004328F2 /* PalletAssets+Events.swift */, ); path = Assets; sourceTree = ""; @@ -19181,6 +19806,7 @@ 84A59158292AA65500BCCF8F /* Substrate */ = { isa = PBXGroup; children = ( + 0CA8AD612CE08FEE00ED9746 /* ExtrinsicStatusService */, 2D32BE112C6A49900047F520 /* FeeManaging */, 0C495B962B452AC500B8D339 /* ExtrinsicSenderResolution */, 84C1DBB829C0A0D700F295A5 /* Xcm */, @@ -19422,7 +20048,6 @@ 84B7C701289BFA79001A3566 /* AssetsManage */, 84B7C703289BFA79001A3566 /* WalletHistoryFilter */, 84B7C705289BFA79001A3566 /* AccountManagement */, - 84B7C708289BFA79001A3566 /* WalletList */, 84B7C70A289BFA79001A3566 /* ControllerAccount */, ); path = Modules; @@ -19503,7 +20128,6 @@ 84B7C6A3289BFA79001A3566 /* DAppSearch */, 84B7C6A5289BFA79001A3566 /* Model */, 84B7C6A7289BFA79001A3566 /* DAppList */, - 84B7C6A9289BFA79001A3566 /* DAppTxDetails */, ); path = DApps; sourceTree = ""; @@ -19556,14 +20180,6 @@ path = DAppList; sourceTree = ""; }; - 84B7C6A9289BFA79001A3566 /* DAppTxDetails */ = { - isa = PBXGroup; - children = ( - 84B7C6AA289BFA79001A3566 /* DAppTxDetailsTests.swift */, - ); - path = DAppTxDetails; - sourceTree = ""; - }; 84B7C6AB289BFA79001A3566 /* AccountImport */ = { isa = PBXGroup; children = ( @@ -19896,14 +20512,6 @@ path = AccountManagement; sourceTree = ""; }; - 84B7C708289BFA79001A3566 /* WalletList */ = { - isa = PBXGroup; - children = ( - 84B7C709289BFA79001A3566 /* WalletListTests.swift */, - ); - path = WalletList; - sourceTree = ""; - }; 84B7C70A289BFA79001A3566 /* ControllerAccount */ = { isa = PBXGroup; children = ( @@ -19964,6 +20572,7 @@ 77D2ED6A2B1DD78100A0E58C /* ProxyAccountModel.swift */, 0C37AFC72B59815500009ECA /* ProxiedSettings.swift */, 0CE9338D2C019998000F3EFE /* MetaAccountModelType+ExtrinsicLimit.swift */, + 0CCA70742CD0B2860082A9C8 /* MetaAccountModel+Async.swift */, ); path = Account; sourceTree = ""; @@ -19983,6 +20592,7 @@ 0CCD18BC2BA1EF030068D73A /* ChainModel+Remote.swift */, 0CCD18BE2BA1EF360068D73A /* AssetModel+Remote.swift */, 0CCD18C32BA1F03E0068D73A /* ChainModeModel+Remote.swift */, + 0C2DA89F2CC21B09001F79C8 /* IndexedChainModels.swift */, ); path = LocalChain; sourceTree = ""; @@ -20042,6 +20652,7 @@ 0CA5BDEA2C4171D9000A4CDD /* BalanceRemoteSubscriptionHandlingProxy.swift */, 0CA5BDEC2C41792A000A4CDD /* SubstrateAssetsUpdatingService.swift */, 0C1998E92C4A22FD000EBFB8 /* BalancesRemoteSubscriptionService+Protocol.swift */, + 0CA8AD592CE0789100ED9746 /* WalletRemoteSubscription.swift */, ); path = Substrate; sourceTree = ""; @@ -20155,12 +20766,16 @@ 84C1DBB829C0A0D700F295A5 /* Xcm */ = { isa = PBXGroup; children = ( + 0CA7CEEA2CE0CA13004328F2 /* TokenDepositEventMatching */, 844AE53B2861B3BC0020ECBC /* XcmTransferService.swift */, 84468A102867BCD400BCBE00 /* XcmTransferService+Protocol.swift */, 84FFE45C28620833002432BB /* XcmTransferResolutionService.swift */, 842B18072865923B0014CC57 /* XcmExtrinsicFeeProxy.swift */, 84C1DBB929C0A11200F295A5 /* XcmTransferService+Fee.swift */, 84C1DBBB29C0A18000F295A5 /* XcmTransferService+Compose.swift */, + 0CA8AD552CE06FA000ED9746 /* XcmTransactService.swift */, + 0CA8AD682CE0904D00ED9746 /* XcmDepositMonitoringService.swift */, + 0CFFB9D22D11A67C00172E8C /* XcmTokensArrivalDetector.swift */, ); path = Xcm; sourceTree = ""; @@ -20483,6 +21098,7 @@ 844CB57526FA064700396E13 /* ChainRegistryError.swift */, 84BAD21B293B1BC200C55C49 /* ChainModelConversion.swift */, 0CEB6B422CA50FC500609DC2 /* ChainRegistry+AsyncWait.swift */, + 0C3272962CD2821500FC1B42 /* ChainRegistry+Get.swift */, ); path = ChainRegistry; sourceTree = ""; @@ -23527,6 +24143,23 @@ path = DAppWalletAuth; sourceTree = ""; }; + E53BE56E5726646BC073E502 /* Execution */ = { + isa = PBXGroup; + children = ( + 0CF976732CF082B2001D2801 /* ViewModel */, + 0CF976702CEFF00E001D2801 /* Model */, + 0CBABFF72CEF33AE0047F29E /* View */, + A2F8A21F1E63BF16DCCE89FE /* SwapExecutionProtocols.swift */, + FA1285B676EA226704C14DDB /* SwapExecutionWireframe.swift */, + 4CE605788A0F121ECFC71C30 /* SwapExecutionPresenter.swift */, + 8632A6D9689942DE89F174FA /* SwapExecutionInteractor.swift */, + E0D139E39F5ECE929FF724B8 /* SwapExecutionViewController.swift */, + 501E16B2A68FEAEC039E8604 /* SwapExecutionViewLayout.swift */, + C3E1250905B8D8E175316B0A /* SwapExecutionViewFactory.swift */, + ); + path = Execution; + sourceTree = ""; + }; E8A61B9F062344AB1C1A7C32 /* AddDelegation */ = { isa = PBXGroup; children = ( @@ -24761,6 +25394,7 @@ 7778FE492B9104EE0023E801 /* PriceAssetInfoFactory.swift in Sources */, 77AF376B2BA2C1A800FD1A8E /* ReferendumStatusExtensions.swift in Sources */, 0CCD18D42BA1F4550068D73A /* PrimitiveBalanceViewModelFactory.swift in Sources */, + 0CBABFF42CEE4D590047F29E /* ChainModelFetchError.swift in Sources */, 2DCC875B2C5820BA0028C3CA /* Logger.swift in Sources */, 77AE4FAB2B905E9B00426519 /* TimeInterval+Time.swift in Sources */, 77EDF9222B96D654003266B1 /* Web3Alert.swift in Sources */, @@ -24794,7 +25428,7 @@ 0CAC03042B4D0C0100DDEC3A /* WalletRemoteQueryFactoryTests.swift in Sources */, 8455F19A2A1DEEAF003F072D /* SubqueryMultistakingTests.swift in Sources */, 0C59E8EB2AA71C3E001E11F3 /* ExternalAssetBalanceIntegrationTests.swift in Sources */, - 0C363E9E2B6BB2E20065AFA6 /* HydraSwapsFeeTests.swift in Sources */, + 0C03877C2D09A1AA000A2F24 /* MockAssetExchangePathCostEstimator.swift in Sources */, 0CA26D902BBBBDB40086748F /* CloudBackupAvailabilityTests.swift in Sources */, AEE4E37225ECF83F00D6DF31 /* StakingInfoTests.swift in Sources */, E2F3E726280823CF00CF31B5 /* ETHAccountInjection.swift in Sources */, @@ -24802,6 +25436,7 @@ 843B6F4F28EEEF610086D4E0 /* Gov2OperationFactoryTests.swift in Sources */, 0C8AF7B62B36C75400005AC9 /* Pdc20OperationTests.swift in Sources */, 84FACB7125F57E4400F32ED4 /* SubstrateStorageTestFacade.swift in Sources */, + 0C2DA8AD2CC2B156001F79C8 /* AssetsExchangeTests.swift in Sources */, 8418167728253B120007684A /* ParachainStakingCollatorsTests.swift in Sources */, 0CA7198C2B78FEF9000B086E /* HydraQuoteTests.swift in Sources */, 0C7104702C29B62800487E64 /* MetadataHashGenerationTests.swift in Sources */, @@ -24811,6 +25446,7 @@ 84BFE8A228C2420A00140F1F /* AutocompounDelegateStakeTests.swift in Sources */, F49F49A326A5C45600A25931 /* BabeEraOperationFactoryTests.swift in Sources */, 77DC54842B1FE2F500C7E45A /* ProxySyncIntegrationTests.swift in Sources */, + 0C2DA8AF2CC2F928001F79C8 /* AssetsExchangeGraphDescription.swift in Sources */, 84532D5F28E4210E00EF4ADC /* ConvictionVotesFetchTests.swift in Sources */, 0C992C432B54F5D900ACC129 /* EraStakersPagedSearchOperationFactoryTests.swift in Sources */, 0C3205C22A868236002EB914 /* EvmGasPriceIntegrationTests.swift in Sources */, @@ -24933,11 +25569,12 @@ F477CD26262EAD30004DF739 /* StakingPayoutViewModelFactory.swift in Sources */, 7728E58B2A123AEE007901E0 /* ReferendumsSearchOperationFactory.swift in Sources */, 2D65F6D52CCF938D00CC6F72 /* AppearanceIconsOptions+Init.swift in Sources */, - 84FD19FE27A3447E008E5E68 /* BalancesWithdrawEvent.swift in Sources */, 844E1E19266142080076AC59 /* BondedState+Status.swift in Sources */, + 0CF3A6C92CE9423000F93C49 /* RouteView.swift in Sources */, F4CE0F9A27343FFD0094CD8A /* AcalaContributionConfirmVC.swift in Sources */, 8493D0E126FF4F5000A28008 /* ScaleCoder+Extension.swift in Sources */, 880E4105298D0E550077B18B /* GovernanceDelegationsLocalWrapperFactory.swift in Sources */, + 0C883A782CE2640000CAB4C8 /* AssetConversionEventParser.swift in Sources */, 84C11F172840BD46007F7C05 /* ParaStkCollatorsOperationFactory.swift in Sources */, 8428766724ADF22000D91AD8 /* TransformAnimator+Common.swift in Sources */, 8419F054295ED55000A14E05 /* LocalChainExternalApi.swift in Sources */, @@ -24945,6 +25582,7 @@ 0CE9338E2C019998000F3EFE /* MetaAccountModelType+ExtrinsicLimit.swift in Sources */, 77F9FB0D2A9D9C5600820625 /* NominationPoolBondMoreBaseProtocols.swift in Sources */, AEFC6D6F2600D7CD000BD310 /* NetworkInfoViewModelFactory.swift in Sources */, + 0C03879C2D0F64F9000A2F24 /* HydraSwapParams.swift in Sources */, 84AA004526C6A04A00BCB4DC /* CommonTypesSyncService.swift in Sources */, 8490147424A94A37008F705E /* RootProtocol.swift in Sources */, 842876A424AE049B00D91AD8 /* SelectableSubtitleListViewModel.swift in Sources */, @@ -24991,6 +25629,7 @@ 84FBED052927B1CA00FBEB83 /* EvmEventParser.swift in Sources */, 2DCC87642C596D180028C3CA /* CloudBackupConfirmPasswordPresenter.swift in Sources */, 84A1742428ED3CF70096F943 /* ReferendumLocal.swift in Sources */, + 0CBABFEF2CED46200047F29E /* HydraExchangeOperationPrototype.swift in Sources */, 0C5FA9A72B8313950077934C /* String+Search.swift in Sources */, 841AB7922993D01A00A362E8 /* GovernanceDelegateConfirmInteractorError.swift in Sources */, 774091F92ACB1F4B00172516 /* SwapAmountInputView.swift in Sources */, @@ -25111,6 +25750,7 @@ 84DED4032666537600A153BB /* CrowdloanBonusService.swift in Sources */, 844EFB5A265FCD9F0090ACB1 /* CrowdloanContributionProtocols.swift in Sources */, 77F033992A8142E5006BC67E /* DirectStakingTypeAccountViewModel.swift in Sources */, + 0C6108262CD5227900909928 /* HydraExchangeHost.swift in Sources */, AE8B8837267351E300AB0AA9 /* CustomValidatorListComposer.swift in Sources */, 846B749C28B4D3B400C39B93 /* CancelOperationPresentable.swift in Sources */, 846E5018277AD3D50049B659 /* DAppList.swift in Sources */, @@ -25149,6 +25789,7 @@ 849628CB299684D70073F143 /* GovernanceYourDelegationGroup.swift in Sources */, 8860F3E0289D241E00C0BF86 /* CurrencyViewSectionModel.swift in Sources */, 84B8AA7D29F9063400347A37 /* WalletConnectStateAuthorizing.swift in Sources */, + 0CA81D4C2CE4FF8800166969 /* AssetExchangeGraphFeeSupport.swift in Sources */, 842806F32847A51400702F3A /* AccountDetailsSelectionView.swift in Sources */, 840B3D652899BBF100DA1DA9 /* ParitySignerScanViewFactory.swift in Sources */, 84C5ADDB2812A244006D7388 /* WalletAccountViewModelFactory.swift in Sources */, @@ -25167,7 +25808,6 @@ 0C80F0A42B3D7DBD00579E23 /* WalletIconView.swift in Sources */, 84715786291136B100D7D003 /* GovernanceUnlocksViewModel.swift in Sources */, 84DD5F64263DFAB700425ACF /* ErrorConditionViolation.swift in Sources */, - 0C2FDF192AEB82AA006A6C59 /* AssetHubFeeModelBuilder.swift in Sources */, 773B2E432B756F8600A3BC11 /* Web3AlertLocalModel.swift in Sources */, 0C13DFCD2AF8A5A300E5F355 /* SwapMaxModel.swift in Sources */, 0CA719892B78AACD000B086E /* HydraRouter+Call.swift in Sources */, @@ -25210,6 +25850,7 @@ 842BA36F27B64F1000D31EEF /* DAppListViewModelFactory.swift in Sources */, 84C5ADE42813EBAB006D7388 /* GradientBannerView.swift in Sources */, 841E6AF625EC12100007DDFE /* PreparedNomination.swift in Sources */, + 0C3272A22CD34F7700FC1B42 /* HydraExchangeExtrinsicConverter.swift in Sources */, 77DF40392B7DC69300ABDB53 /* SettingsLocalSubscriptionFactory.swift in Sources */, 848C3D0926248A3B005481C3 /* TransferCall.swift in Sources */, 843CE3A327D1FB8A00436F4E /* NftDetailsPriceView.swift in Sources */, @@ -25246,6 +25887,7 @@ 77A0B2F52A3CA39D00CBF653 /* StakingMoreOptionCollectionViewCell.swift in Sources */, 88F34FDB28FFE6AA00712BDE /* RequestedAmountRow.swift in Sources */, 880CC0AA29E7F151008C7F65 /* EquilibriumLocksSubscription.swift in Sources */, + 0C03878A2D0A2785000A2F24 /* SwapRouteDetailsViewModelFactory.swift in Sources */, 8490145324A93FD1008F705E /* NovaLoadingViewFactory.swift in Sources */, 8472976C260B1CAD009B86D0 /* InitiatedBondingConfirmInteractor.swift in Sources */, 0C6390912BF091610015D467 /* CloudBackupUpdateCalculationFactory.swift in Sources */, @@ -25258,10 +25900,12 @@ 0C85FF302B6D35D600FC0014 /* AssetHubFlowState.swift in Sources */, 842876B324AE059700D91AD8 /* AboutData.swift in Sources */, 8489A6D427FDA0BB0040C066 /* StashLedgerStateProtocol.swift in Sources */, + 0C11D8522CC9F5F2003EC46D /* HydraOmnipoolExchangeEdge.swift in Sources */, 84EBC55B24F660F500459D15 /* SelectedWalletSwitched.swift in Sources */, 840B3D6A2899CD2900DA1DA9 /* ParitySignerAddressScan.swift in Sources */, 883DB178297A411300EFB7D8 /* AddDelegationStyles.swift in Sources */, 2D389B082C8616F900256C7B /* SwipeGovGradientFactory.swift in Sources */, + 0C11D85A2CCA3377003EC46D /* AssetsHydraOmnipoolExchange.swift in Sources */, 777BD86429F979DA004969A2 /* SelectableFilterCell.swift in Sources */, 0C992C3F2B53AEF700ACC129 /* OperationSearchService.swift in Sources */, 8849AD6229C3532600F4F7FF /* String+Split.swift in Sources */, @@ -25269,10 +25913,13 @@ 0C12BC492CEB996700AB919D /* XcmJunctionV4.swift in Sources */, 8466781C27ED9644007935D3 /* AsyncWarningConditionViolation.swift in Sources */, 0C3976A92C330A1D00998DD6 /* HydraXYK+PoolAccount.swift in Sources */, + 0C61084B2CD88F9B00909928 /* AssetsExchangeService.swift in Sources */, 849067CA299BCCB700B2983E /* GovernanceRevokeDelegationConfirmPresenter+Update.swift in Sources */, + 0CA7CEEC2CE0CA57004328F2 /* NativeTokenDepositEventMatcher.swift in Sources */, 0C8AF7AF2B343C6B00005AC9 /* ExtrinsicSenderResolutionFactory.swift in Sources */, 0C3205BE2A867A9C002EB914 /* EvmLegacyGasPriceProvider.swift in Sources */, 0CE629D92AA9B68C00E250BD /* AssetBalanceViewModel.swift in Sources */, + 0CF9767D2CF1274F001D2801 /* AssetExchangeMetaOperationLabel+Display.swift in Sources */, 84452F9325D5EE7300F47EC5 /* DataOperationFactory.swift in Sources */, 841E5538282CF3F400C8438F /* StakingMainViewModelFactory.swift in Sources */, 0CA5BDFD2C455A73000A4CDD /* NominationPoolsMigrateCall.swift in Sources */, @@ -25280,11 +25927,13 @@ 844DBC6F274E702C009F8351 /* AccountImportBaseView.swift in Sources */, 8466781A27ECA021007935D3 /* PersistentExtrinsicService.swift in Sources */, 849E0CD025CFDDB700B33506 /* StorageUpdateData+Decoding.swift in Sources */, + 0C61084D2CD8909600909928 /* AssetExchangeFee.swift in Sources */, 884A752D299B56F200FEFC30 /* DelegateReferendumsModelFactoryProtocol.swift in Sources */, 844C3E6D2A08E1B300C4305F /* BalancesStore.swift in Sources */, 8430AAE126022CA1005B1066 /* BaseStakingState.swift in Sources */, 8490141124A92F6D008F705E /* OnboardingMainViewController.swift in Sources */, D9D163642744F60D00681C1F /* ExternalContribution.swift in Sources */, + 0CE94FF42D04969100E31D0C /* SwapInterEDNotMet.swift in Sources */, 0CCCDF842B64C63D00473D42 /* HydraDxTokenConverter.swift in Sources */, 84155DED2539817200A27058 /* ApplicationService.swift in Sources */, 84403D7F25E91BC100494FD4 /* SuperIdentity.swift in Sources */, @@ -25306,6 +25955,7 @@ 84893C0724DA890F008F6A3F /* CommonError.swift in Sources */, 84F1CB3E27CF4F5A0095D523 /* BorderedLabelView.swift in Sources */, 849DEC6125EE13CE00C64C19 /* AddressOptionsPresentable.swift in Sources */, + 0CB4334E2CC1196400F2CB59 /* WeightableEdge.swift in Sources */, 84A3773027B52E020026D6D1 /* DAppEthereumConfirmInteractor.swift in Sources */, 84F30EF1260001A600039D09 /* DataProviderChange+Helper.swift in Sources */, 84755CEB298CF9CF0092D1A8 /* ViewModelViewPair.swift in Sources */, @@ -25337,6 +25987,7 @@ 88F7716428BF6B59008C028A /* GenericMultiValueView.swift in Sources */, 847157802910F30500D7D003 /* GovernanceUnlockInteractorError.swift in Sources */, 843612BB278FD6E000DC739E /* DAppSigningType.swift in Sources */, + 0CA8AD562CE06FA000ED9746 /* XcmTransactService.swift in Sources */, AE2C84D525EF989A00986716 /* ValidatorInfoWireframe.swift in Sources */, 849D14CC2994EEA50048E947 /* GovernanceDelegateFlowDisplayInfo.swift in Sources */, F44CD8F426242825005DDF23 /* PayoutRewardsService+Fetch.swift in Sources */, @@ -25378,6 +26029,7 @@ 2A66CFAF25D10EDF0006E4C1 /* PhishingItem.swift in Sources */, 882C29AA28DC7B3D009CA4B6 /* SubstrateStorageMigrator.swift in Sources */, 0CCD18BF2BA1EF360068D73A /* AssetModel+Remote.swift in Sources */, + 0C883A722CE2235600CAB4C8 /* HydraSwapEventsMatcher.swift in Sources */, 2D1C5D1E2C25ACB500E2DBDD /* CustomNetworkAddInteractor.swift in Sources */, 8487583E27F070B300495306 /* ApplicationSettingsPresentable.swift in Sources */, 84FEF3E32807E8000042CBE7 /* DAppBrowserPage.swift in Sources */, @@ -25419,6 +26071,7 @@ 849013DE24A927E2008F705E /* LocalizationManager+Shared.swift in Sources */, 8412AF9B2789ABBC008A6C22 /* PolkadotExtensionMetadataResponse.swift in Sources */, 84F1CB3727CE5EC30095D523 /* NftListViewModelFactory.swift in Sources */, + 0CA7CEFA2CE13126004328F2 /* TokenDepositEventMatcherFactory.swift in Sources */, 849013DC24A927E2008F705E /* ApplicationConfigs.swift in Sources */, 84DA03DB275A31B500E8B326 /* DAppListHeaderView.swift in Sources */, 84ACEBFA261E684A00AAE665 /* WalletHistoryFilter.swift in Sources */, @@ -25478,6 +26131,7 @@ 0C9D87AE2AC708070095FE8C /* AssetHubTokensConverter.swift in Sources */, 84B8AA7529F8FD2400347A37 /* DAppInteractionFactory.swift in Sources */, 0C38B5072B7B3B4B00882A8B /* HydraOmnipool+Events.swift in Sources */, + 0C03878C2D0B2686000A2F24 /* LinePatternView.swift in Sources */, 844EFB65265FD61D0090ACB1 /* CrowdloanContributeConfirmViewModel.swift in Sources */, AEE5FAFF26415E0C002B8FDC /* StakingRebondSetupPresenter.swift in Sources */, 84A3B8A02836D74B00DE2669 /* StorageKeyDecodingProtocol.swift in Sources */, @@ -25486,6 +26140,7 @@ 8407715E28CBE28D007DBD24 /* ParaStkYieldBoostSetupPresenter+Cancel.swift in Sources */, 88DC3E27292CABCB00DBCE4D /* AssetListStyles.swift in Sources */, 8472975D260B1B71009B86D0 /* ExistingBonding.swift in Sources */, + 0C2A3C932CDC813B00A0E2B3 /* AssetsExchangeOperationFactory.swift in Sources */, 8490144C24A93D0B008F705E /* NovaNavigationController.swift in Sources */, 848077D02837C637003B7C79 /* ParachainStakingValidatorFactory.swift in Sources */, 84F2FEFF25E7ADE7008338D5 /* ValidatorPrefs.swift in Sources */, @@ -25514,6 +26169,7 @@ 77F189402A49972300E8B933 /* UILabel+bind.swift in Sources */, 8467FCFE24E5C50B005D486C /* KeystoreImportService.swift in Sources */, 0CC0F80A2BB2D32E002C49F6 /* FirebaseAppCheckProviderFactory.swift in Sources */, + 0CF4ADF92CEBF71A009C51FA /* AssetExchangeMetaOperationProtocol.swift in Sources */, 847A259029B5D2710054F90C /* GovJsonLocalStorageHandler.swift in Sources */, 77C9BCCE2ACD691300022EA2 /* SwapAssetsOperationViewFactory.swift in Sources */, 84C6801024D6EE4500006BF5 /* PlusIndicatorView.swift in Sources */, @@ -25522,6 +26178,7 @@ 0CE933952C0488E7000F3EFE /* CloudBackupReviewViewModel.swift in Sources */, 77171CAA2A98BC420032B387 /* NominationPoolDataValidatorFactory.swift in Sources */, 2D6F51A22CDB8C7400564C00 /* GovernanceChainSelectionViewFactory.swift in Sources */, + 0CBABFF12CED46C90047F29E /* AssetHubExchangeOperationPrototype.swift in Sources */, 84350AD9284604CE0031EF24 /* ParaStkYourCollatorsViewModel.swift in Sources */, 8490141624A92F6D008F705E /* OnboardingMainViewFactory.swift in Sources */, 0C962F862AA859F200C0B551 /* TransactionHistoryLocalFilter.swift in Sources */, @@ -25559,8 +26216,9 @@ 84CA68D926BE9E7F003B9453 /* SpecVersionSubscription.swift in Sources */, 0C13DFCB2AF6182500E5F355 /* SwapModel.swift in Sources */, 84C74363251E4C2F009576C6 /* DummySigner.swift in Sources */, - 8454C26A2632B8CE00657DAD /* BalanceDepositEvent.swift in Sources */, + 8454C26A2632B8CE00657DAD /* BalancesPallet+Events.swift in Sources */, 0C0CB38A2AC56A1600EAC516 /* AssetConversionPallet+Path.swift in Sources */, + 0C6108382CD7333100909928 /* ExtrinsicCustomFeeInstallingWrapperFactory.swift in Sources */, 2D1C5D2A2C25AD0200E2DBDD /* CustomNetworkPresenterProtocols.swift in Sources */, 84DD5F3E263DE5FF00425ACF /* DataValidationProtocols.swift in Sources */, 2D32BE1B2C6A49900047F520 /* ExtrinsicNativeFeeInstaller.swift in Sources */, @@ -25569,7 +26227,6 @@ AEE5FB0126415E2A002B8FDC /* StakingRebondSetupInteractor.swift in Sources */, 2D039DE22BFBB08400A928E5 /* ExportNetworkView.swift in Sources */, 84C1B98624F5424700FE5470 /* ChainAccountViewModelFactory.swift in Sources */, - 847A25B928D7BB1F006AC9F5 /* BalancesTransferEvent.swift in Sources */, 84E258A52893D27E00DC8A51 /* AddressScanPresentable.swift in Sources */, AEE5FB1026457806002B8FDC /* StakingRewardDestSetupViewFactory.swift in Sources */, 849B563327A70D71007D5528 /* ExtrinsicProcessor+Fee.swift in Sources */, @@ -25586,6 +26243,7 @@ 77A0B2F32A3CA37E00CBF653 /* StakingMoreOptionsViewModelFactory.swift in Sources */, 849014B924AA87E3008F705E /* PinSetupViewController.swift in Sources */, 84F98D8A25E3DD3F0040418E /* StorageCodingPath.swift in Sources */, + 0C0387852D0A1C0A000A2F24 /* LabelRouteItemView.swift in Sources */, 8846F74229D759DD00B8B776 /* Data+proquint.swift in Sources */, 842D8B782A4098C300660005 /* ShimmeringLabel.swift in Sources */, 0C7A3FCE2B6A874500764603 /* HydraSwapRemoteState.swift in Sources */, @@ -25602,10 +26260,12 @@ 2D32BE152C6A49900047F520 /* ExtrinsicFeeEstimationProtocol.swift in Sources */, 848F8B1928635A5600204BC4 /* TransferSetupPresenter.swift in Sources */, 841221A028F051EE00715C82 /* GovMetadataLocalStorageSubscriber.swift in Sources */, + 0CA81D572CE5AF2600166969 /* AssetExchangeFeeSupportFetchersProvider.swift in Sources */, 84ABB3322A16150400B5E95A /* AssetReceiveInfo.swift in Sources */, 84100F3626A6069200A5054E /* IconTitleValueView.swift in Sources */, 771901A82AE8FF9F00D9C918 /* SwapNetworkFeeViewCell.swift in Sources */, 77EF9A7C2B9ECF2D0034D619 /* GovernanceNotificationMessageHandler.swift in Sources */, + 0C12BC3E2CEA9DF800AB919D /* AssetExchangeTimeEstimation.swift in Sources */, 88B560BC28F80DCB00A5EB59 /* VoteRowView.swift in Sources */, 84FCCD98292E3610002D2D3D /* JSONRPCError+Evm.swift in Sources */, 842876AB24AE049B00D91AD8 /* SelectionItemViewProtocol.swift in Sources */, @@ -25617,6 +26277,7 @@ 771901A62AE8FF7E00D9C918 /* SwapInfoViewCell.swift in Sources */, 8428765C24ADDE0200D91AD8 /* SettingsViewController.swift in Sources */, 84746B3028153E4C002642F4 /* GradientBannerModel.swift in Sources */, + 0C61082A2CD5DE9500909928 /* AssetHubExchangeHost.swift in Sources */, 8433B34A29B63661005E5D0F /* ExtrinsicSplitter.swift in Sources */, 843B1D7A263EED5C00AF8957 /* StakingUnbondConfirmLayout.swift in Sources */, 0C3205DA2A896029002EB914 /* EvmConstantNonceProvider.swift in Sources */, @@ -25663,10 +26324,12 @@ 84ABB32C2A16146600B5E95A /* WalletEmptyStateDataSource+Module.swift in Sources */, 8443045C2A28734B00DE36DE /* ParachainMultistakingUpdateService.swift in Sources */, 84893C0524DA8663008F6A3F /* AccountCreationError.swift in Sources */, + 0C03879A2D0CEDC9000A2F24 /* SwapFeeDetailsViewModelFactory.swift in Sources */, 84282FBD26D05A54002CA322 /* ChainRegistryFacade.swift in Sources */, 0C59E8C92AA5C7EC001E11F3 /* ExternalAssetBalance.swift in Sources */, 842EBB3B2890B06500B952D8 /* WalletSelectionPresenter.swift in Sources */, 88D1F1B22981212700316A1A /* InAppUpdatesServiceWireframeProtocol.swift in Sources */, + 0C2DA89E2CC21853001F79C8 /* CrosschainExchangeEdge.swift in Sources */, 84C1DBC329C27A6600F295A5 /* Paras.swift in Sources */, 846B749228B420FC00C39B93 /* ChainAccountAddTableViewCell.swift in Sources */, 842822A2289BCB5500163031 /* SwitchAccount+ParitySignerScanWireframe.swift in Sources */, @@ -25729,7 +26392,7 @@ 8470D6D0253E321C009E9A5D /* StorageSubscriptionProtocols.swift in Sources */, 8499FE6C27BD319300712589 /* RMRKV2OperationFactory.swift in Sources */, 84D184F42A04F5210060C1BD /* StackInfoTableCell+WallectConnect.swift in Sources */, - 2D95DF612C74B79A009BB063 /* HydraFlowStateStore.swift in Sources */, + 2D95DF612C74B79A009BB063 /* AssetConversionFeeSharedStateStore.swift in Sources */, 88A5317D28B9170100AF18F5 /* NSCollectionLayoutSection+create.swift in Sources */, 8444D1BE2671465A00AF6D8C /* CrowdloanBonusServiceError.swift in Sources */, 8448F7A22882ABF50080CEA9 /* CustomSearchView.swift in Sources */, @@ -25782,6 +26445,7 @@ 84FD3DB72540EF0700A234E3 /* TransactionSubscription.swift in Sources */, 8482F628280C49940006C3A0 /* DAppsAuthViewModelFactory.swift in Sources */, 84C3F7832602086100D47501 /* StakingViewState.swift in Sources */, + 0C2DA8A82CC2679E001F79C8 /* AssetsExchangeGraph.swift in Sources */, F46F9F902733E02B00FFA556 /* MoonbeamKeys.swift in Sources */, 849014DC24AA8F60008F705E /* MainTabBarPresenter.swift in Sources */, 8467F4C526B1E4E200C5B6F4 /* StakingDurationFetching.swift in Sources */, @@ -25818,10 +26482,13 @@ 84EDF66929C4B94C002173E6 /* EvmNativeBalanceUpdatingService.swift in Sources */, 0CEB6B2D2CA4121300609DC2 /* GovTreasurySpentRemoteHandler.swift in Sources */, 84F4A9A42551A8F3000CF0A3 /* AccountExportPasswordError.swift in Sources */, + 0C6108512CD9D3BA00909928 /* GraphModel+BFS.swift in Sources */, 84B73ADE279D90BD0071AE16 /* AssetsTransfer.swift in Sources */, 0CAB7D9F2C46B05F0070CE4D /* PoolStakingRecommendingValidationFactory.swift in Sources */, 84CFF1E726526FBC00DB7CF7 /* StakingBondMoreViewLayout.swift in Sources */, 84CA68DB26BEA33F003B9453 /* ChainRegistryFactory.swift in Sources */, + 0CB433482CC0F9EF00F2CB59 /* AssetsHubExchange.swift in Sources */, + 0CA8AD5A2CE0789100ED9746 /* WalletRemoteSubscription.swift in Sources */, AE39839A272BFC2300BC8A85 /* ImportChainAccount.swift in Sources */, 8463A72D25E3A8E1003B8160 /* ChainStorageDecodedItem.swift in Sources */, 84DD5F30263D84F300425ACF /* RuntimeConstantFetching.swift in Sources */, @@ -25888,6 +26555,7 @@ 889F7F3B292373560024CB1E /* RemoteAssetModel+Evm.swift in Sources */, 849D3227291CE25E00D25839 /* MarkdownViewContainer.swift in Sources */, 0C1CCC3C2B5F79D300A6EA17 /* ProxyErrorPresentable.swift in Sources */, + 0CF9767B2CF09B86001D2801 /* SwapExecutionViewModelFactory.swift in Sources */, 0C59E8E12AA60FF0001E11F3 /* CrowdloanExternalServiceFactory.swift in Sources */, 2D4F55012CCA60E900B65B76 /* AssetsSearchCollectionManager.swift in Sources */, 847297CF260B4035009B86D0 /* ChangeTargetConfirmInteractor.swift in Sources */, @@ -25901,6 +26569,7 @@ 84031C17263EC95C008FD9D4 /* SetPayeeCall.swift in Sources */, 8473B4762A20722E003DE213 /* StakingDashboardRelaychainMapper.swift in Sources */, 849F14472943616A00D9F9BA /* TokensManageAddInteractorError.swift in Sources */, + 0C6108362CD5FC7300909928 /* ExtrinsicFeeInstallingWrapperFactory.swift in Sources */, 84A70D9B29CCECC100C648AD /* GovMetadataOperationFactory.swift in Sources */, 8490895428D4BA7C00C3CCE9 /* CustomSiMappers.swift in Sources */, 84100F3A26A60B0900A5054E /* YourValidatorListStatusSectionView.swift in Sources */, @@ -25948,6 +26617,7 @@ 842E9E982A2A28A700759972 /* StakingDashboardProviderFactory.swift in Sources */, 845B07ED291594E1005785D3 /* DemocracyReferendum.swift in Sources */, 84466B4028B77B4500FA1E0D /* SignatureVerificationWrapper.swift in Sources */, + 0CBABFF32CED47D10047F29E /* CrosschainExchangeOperationPrototype.swift in Sources */, 844DBC62274D1E29009F8351 /* SecretTypeTableViewCell.swift in Sources */, 0CB261F92A9F1F2200287305 /* NPoolsRedeemError.swift in Sources */, 847297A2260B3146009B86D0 /* ChangeTargetsSelectValidatorsStartWireframe.swift in Sources */, @@ -25972,6 +26642,7 @@ AEA0C8BA268113F900F9666F /* YourValidatorList+RecommendedList.swift in Sources */, 2D6F51A52CDBCFAC00564C00 /* AssetListStyleSwitcher.swift in Sources */, 0C38B5032B7A86CE00882A8B /* TokensPallet.swift in Sources */, + 0C746DBE2CCF440800E9178B /* AssetExchangeAtomicOperation.swift in Sources */, 88421056289BBA8D00306F2C /* CurrencyPresenter.swift in Sources */, 84D33996262250B800130A89 /* String+Substrate.swift in Sources */, 8442003428E9BD3200C49C4A /* VoteChildPresenterFactory.swift in Sources */, @@ -25998,7 +26669,6 @@ 84953F662934C7D90033F47D /* EtherscanERC20HistoryResponse.swift in Sources */, AEAC68F526E9F93B00346599 /* CoingeckoDefinitions.swift in Sources */, 8410DBCB26EA31DE00FE1738 /* AccountProviderFactory.swift in Sources */, - 0CD3A67C2AEAA3B90059BBEC /* AssetConversionFeeService.swift in Sources */, 841E554F282E2C0300C8438F /* StakingParachainInteractor+InputProtocol.swift in Sources */, 84B1318C29ED70BF004EA1FF /* EvmFallbackGasLimit.swift in Sources */, 84DB9E902640A48E00F23DD3 /* StakingRedeemViewModel.swift in Sources */, @@ -26029,6 +26699,7 @@ 843CE3AA27D22B5200436F4E /* UniquesDetailsInteractor.swift in Sources */, 8428765D24ADDE0200D91AD8 /* SettingsInteractor.swift in Sources */, 848F5FE3298912E80058CD74 /* GovernanceOffchainDelegationsFactory.swift in Sources */, + 0C6108282CD53CDE00909928 /* CrosschainExchangeHost.swift in Sources */, 849014C124AA87E4008F705E /* LocalAuthProtocol.swift in Sources */, 841E6B0A25EC1C140007DDFE /* ValidatorOperationFactory.swift in Sources */, 8472C5B5265CF9C500E2481B /* StakingRewardDestConfirmProtocols.swift in Sources */, @@ -26059,6 +26730,7 @@ 2A9F8D50274E4EB6003720E0 /* AccountCreateViewLayout.swift in Sources */, 84E8AC7527BB975700402635 /* RMRKV1OperationFactory.swift in Sources */, 84786E1525FA57B90089DFF7 /* StakingLedger.swift in Sources */, + 0C12BC3A2CEA7ECB00AB919D /* AssetExchangePriceStore.swift in Sources */, 778215D82B9593C6006C7D13 /* UserDefaultMigrator.swift in Sources */, 8497FC6026317783002FEAA7 /* AccountInfoViewModel.swift in Sources */, 84DEE7042797FBF800B9A39E /* ExtrinsicSignedExtensionFactory.swift in Sources */, @@ -26092,6 +26764,7 @@ 8442002B28E9ACDB00C49C4A /* VotePresenter.swift in Sources */, 84EFB78F28AB897D003B8396 /* LedgerResponse.swift in Sources */, F429C3CC272940A2000214EE /* MoonbeamVerifyRemarkRequest.swift in Sources */, + 0CA7CEE92CE0C37B004328F2 /* SystemPallet.swift in Sources */, 849014FC24AA9939008F705E /* Charset+Mnemonic.swift in Sources */, F418E86F264D0F0400699085 /* Migrating.swift in Sources */, AEE5FAFB26414C4F002B8FDC /* StakingRebondSetupProtocols.swift in Sources */, @@ -26136,9 +26809,11 @@ 0CED3A0A2BD1677B00B1E994 /* CloudBackupSecretsConstants.swift in Sources */, 84466B3A28B7653E00FA1E0D /* LedgerPerformOperationProtocols.swift in Sources */, 8430AB1726023D2D005B1066 /* BaseStashNextState.swift in Sources */, + 0C11D8602CCA486B003EC46D /* AssetExchangeRoute.swift in Sources */, 84A6AB64290B021E001B57B2 /* CopyPresentable.swift in Sources */, 841AAC2126F6860B00F0A25E /* AssetBalanceFormatterFactory.swift in Sources */, 0C77B5652A8374EA00B5AE08 /* StaticValidatorListPresenter.swift in Sources */, + 0CA8AD692CE0904D00ED9746 /* XcmDepositMonitoringService.swift in Sources */, 2D4F55112CCB7C4C00B65B76 /* SendAssetOperationCollectionDataSource.swift in Sources */, 8418167528251BBC0007684A /* StorageListSyncResult.swift in Sources */, 0CF4ADFF2CECACF9009C51FA /* PolkadotRewardParamsService.swift in Sources */, @@ -26146,12 +26821,14 @@ 8465DA3F298EEC6C00C7CFF1 /* GovernanceAddDelegationTracksInteractor.swift in Sources */, 2DBB288D2CD42C5200DFA0AD /* QRCreationOperationFactory.swift in Sources */, 845B823429C8FF0700D187CB /* EtherscanNativeOperationFactory.swift in Sources */, + 0C423ABD2CDBAEC900A64941 /* AssetExchangeGraphProvidingParams.swift in Sources */, 88D997B028ABC8C0006135A5 /* YourContributionsTableViewCell.swift in Sources */, 849ABE5B2627739400011A2A /* ListReducing.swift in Sources */, 0C7CF4CC2BD374930015DD45 /* CloudBackupFileManaging.swift in Sources */, 2DBB28912CD42ED800DFA0AD /* IconInfo.swift in Sources */, 88C5F07C297EE79C001CCADE /* Release.swift in Sources */, 845B89222959620000EE25B0 /* SecurityLayerPresenter.swift in Sources */, + 0C746DBC2CCE5E9900E9178B /* AssetExchangeExecutionManager.swift in Sources */, 849E07F4284A04F400DE0440 /* ParaStkAccountSubscribeHandlingFactory.swift in Sources */, 0C59E8D82AA60490001E11F3 /* ExternalAssetBalanceStreambleSource.swift in Sources */, 84BC7047289EFFFA008A9758 /* ChainWalletDisplayAddress.swift in Sources */, @@ -26199,6 +26876,7 @@ F402BC83273ACDC30075F803 /* AstarBonusService.swift in Sources */, 888797B429F11EF90078633F /* SettingsBaseTableViewCell.swift in Sources */, 84205897260C795B007D26C6 /* NominatorState+Status.swift in Sources */, + 0C0387812D0A1283000A2F24 /* AssetAmountView.swift in Sources */, AE8B883226733BAD00AB0AA9 /* CustomValidatorListHeaderView.swift in Sources */, 8488D5DB298167D10019B388 /* GovernanceDelegateTypeView.swift in Sources */, 84CC726228AF8C7A003429E7 /* LedgerApplicationRequest.swift in Sources */, @@ -26223,6 +26901,7 @@ 0C9C642D2A8CE30A004DC078 /* SystemAccountValidating.swift in Sources */, 88D02FF42943207400E26390 /* BigInt+Decimal.swift in Sources */, 847999B82889510C00D1BAD2 /* TextInputViewDelegate.swift in Sources */, + 0C33E8B62D0069EC0090096A /* AssetsExchangeFeeSupportProvider.swift in Sources */, 842A738427DDF55A006EE1EA /* OperationIdOptionsPresentable.swift in Sources */, 88C5F082297F0706001CCADE /* ReleaseVersion.swift in Sources */, 849013E224A9288B008F705E /* Language.swift in Sources */, @@ -26232,10 +26911,12 @@ 84FC190B29B7DB9F00BCCAA5 /* ExtrinsicServiceTypes.swift in Sources */, 849707A128F3E0AC00DD0A02 /* ReferendumVoterLocal.swift in Sources */, 774091FC2ACC053000172516 /* SwapAssetView.swift in Sources */, + 0C61083E2CD7382E00909928 /* AssetConversionFeeInstallingFactory.swift in Sources */, 84754C882510BAFE00854599 /* ModalAlertFactory.swift in Sources */, 8430AACC2602249B005B1066 /* InitialStakingState.swift in Sources */, 0C56B4FB2A4B0C320030F9C9 /* AssetListBaseBuilder.swift in Sources */, 2D32BE1A2C6A49900047F520 /* ExtrinsicNativeFeeEstimator.swift in Sources */, + 0C33E8BA2D011D2E0090096A /* Debouncer.swift in Sources */, 84EE2FA52891205500A98816 /* WalletManageViewController.swift in Sources */, 0CB313742C0F45E300353724 /* CloudBackupUpdatePasswordWireframe.swift in Sources */, 84786DA825F9F58E0089DFF7 /* EraValidatorService+Fetch.swift in Sources */, @@ -26247,10 +26928,8 @@ 845B811F28F451A40040CE84 /* GovernanceActionOperationFactory.swift in Sources */, 84754CA22513DB8800854599 /* EmptyAccountIcon.swift in Sources */, 8814774229B07E4C001E98A1 /* ResizableImageActionIndicator.swift in Sources */, - 0C363E962B6B591C0065AFA6 /* HydraExtrinsicOperationFactory.swift in Sources */, 8824D424290324260022D778 /* PrettyPrintedJSONOperationFactory.swift in Sources */, 778D979B2A24D1D8002BA681 /* BaseAssetsSearchViewLayout.swift in Sources */, - 0C85FF222B6C001900FC0014 /* HydraSwapExtrinsicService.swift in Sources */, AEA0C8C6268131C500F9666F /* InitiatedBondingSelectedValidatorsListWireframe.swift in Sources */, 880059E328EF128000E87B9B /* ReferendumInfoView.swift in Sources */, 84F2FEFA25E797E8008338D5 /* StorageRequestFactory.swift in Sources */, @@ -26262,6 +26941,7 @@ 842A738C27DE576C006EE1EA /* NovaTintImageProcessor.swift in Sources */, 84FA835A265CE5BE00FDF727 /* TitleMultiValueView.swift in Sources */, 849014BB24AA87E4008F705E /* PinSetupWireframe.swift in Sources */, + 0C6108322CD5EEBD00909928 /* AssetConversionFeeEstimatingFactory.swift in Sources */, 0C77B55F2A83717000B5AE08 /* StaticValidatorListViewController.swift in Sources */, AEE4E34825E90C6000D6DF31 /* RewardCalculatorService.swift in Sources */, F40D0AF2260CCE5800CBD43B /* StakingPayoutBaseTableCell.swift in Sources */, @@ -26290,6 +26970,7 @@ 8858917A29A700F000320896 /* DelegationReferendumVotersViewModelFactory.swift in Sources */, 8441007C28A3CD5900DCCA11 /* ParitySignerScanInteractor.swift in Sources */, 84DA03D427592FAA00E8B326 /* ChainAccountView.swift in Sources */, + 0CCA70752CD0B2860082A9C8 /* MetaAccountModel+Async.swift in Sources */, 842B18082865923B0014CC57 /* XcmExtrinsicFeeProxy.swift in Sources */, 889D889728F022AA00C4320F /* UILabel+Style.swift in Sources */, 843612C72790032400DC739E /* DAppOperationProcessedResult.swift in Sources */, @@ -26335,6 +27016,7 @@ 846CD24D2656FEB800A2E4B6 /* StorageKeysQueryService.swift in Sources */, 0CD846272BDDF43A0026B5CA /* UIImage+Resize.swift in Sources */, 8465DA39298EC86E00C7CFF1 /* TitleDetailsSheetViewModel.swift in Sources */, + 0CA81D542CE5108A00166969 /* AssetHubExchangeFeeSupportFetcher.swift in Sources */, 8468B86C24F63CEF00B76BC6 /* AddAccount+AccountConfirmInteractor.swift in Sources */, 887A717C28FEF03E00B13C7E /* BaselinedView.swift in Sources */, 84ABB32E2A16146700B5E95A /* WalletHistoryBackgroundView.swift in Sources */, @@ -26353,6 +27035,7 @@ 84F30EE425FFAC0800039D09 /* StreamableProviderOptions+Substrate.swift in Sources */, 845B811228F429BB0040CE84 /* SupportPallet.swift in Sources */, 0C3205C62A877594002EB914 /* EvmGasPriceWithFallbackProvider.swift in Sources */, + 0C0387872D0A24E6000A2F24 /* SwapRouteDetailsView.swift in Sources */, 8470D6D4253E35F0009E9A5D /* StorageUpdate.swift in Sources */, 8460E715284AC0AA002896E9 /* ParaStkBaseUnstakeInteractor.swift in Sources */, 84355CEE28B614A7004E5C5E /* MessageSheetImageGraphicsView.swift in Sources */, @@ -26472,6 +27155,7 @@ 84D8754028EB1A59004065BD /* ReferendumsInteractorError.swift in Sources */, 883DB17C297A520300EFB7D8 /* GovernanceDelegatesFilter.swift in Sources */, 77ED167A2A0CF41700E1FC8C /* StakingRewardFiltersViewModel.swift in Sources */, + 0C19CCA22CC6EC75007F8ED8 /* AssetsHubExchangeProvider.swift in Sources */, 849014A624AA801B008F705E /* TextSharingSource.swift in Sources */, 840AD5FC26B4512100E09D6A /* SelectedLanguageMigrator.swift in Sources */, 8804AD83295B7596001C4E09 /* DAppDesktopModeSettingsView.swift in Sources */, @@ -26507,6 +27191,7 @@ 84DA03D82759362100E8B326 /* ChainAccountViewModel.swift in Sources */, 84C1FA18278D44DD008B2711 /* PolkadotExtensionPayload.swift in Sources */, 84F1A0712869DA51007DB053 /* AssetBalanceExistence.swift in Sources */, + 0CA81D502CE502B200166969 /* AssetExchangeFeeSupport.swift in Sources */, 84ABB32D2A16146700B5E95A /* WalletEmptyStateDataSource.swift in Sources */, 0CEB6B382CA450C200609DC2 /* XcmV4.swift in Sources */, 84FBECF62926B38300FBEB83 /* ERC20BalanceUpdateService.swift in Sources */, @@ -26514,6 +27199,7 @@ 8407715C28CBE25B007DBD24 /* ParaStkYieldBoostSetupPresenter+Schedule.swift in Sources */, 848CCB482832EF4400A1FD00 /* GeneralLocalStorageHandler.swift in Sources */, 849013D024A9267F008F705E /* R.generated.swift in Sources */, + 0C3272912CD2409B00FC1B42 /* AssetExchangeOperationFee.swift in Sources */, 844CED2529260B6A001A7757 /* EvmQueryContractMessageFactory.swift in Sources */, 8442003828EAA16600C49C4A /* ReferendumsPresenter.swift in Sources */, 84DC3CE52796768A0038E2ED /* SubqueryHistoryContext.swift in Sources */, @@ -26580,6 +27266,7 @@ 848CC93D28D9F6D8009EB4B0 /* OnChainDispatchTime.swift in Sources */, 841E5534282CD9A900C8438F /* StakingType.swift in Sources */, F436BB9E2726FBF7004B1794 /* Coordinator.swift in Sources */, + 0C0387952D0CD120000A2F24 /* SwapOperationFeeView.swift in Sources */, 0CB313682C0F0DB100353724 /* CloudBackupEnterPasswordCheckInteractor.swift in Sources */, 848F5FE1298911B80058CD74 /* SubqueryDelegationsOperationFactory.swift in Sources */, 84BB3CF8267D276D00676FFE /* CrowdloanTableViewCell.swift in Sources */, @@ -26592,7 +27279,6 @@ 8428768524AE046300D91AD8 /* LanguageSelectionProtocols.swift in Sources */, AEA0C8A4267B6B1900F9666F /* SelectedValidatorListProtocols.swift in Sources */, 0CB64E652B01E0CC008F268F /* TransferNetworkSelectionCell.swift in Sources */, - 0CA7198E2B791811000B086E /* HydraReQuoteService.swift in Sources */, 0C3205EC2A8A122D002EB914 /* FeeOutputModel.swift in Sources */, 84D911AA292C923D0032EF33 /* Data+Fill.swift in Sources */, F4B39C4E27326E8400BB6E10 /* AcalaContributionSetupViewController.swift in Sources */, @@ -26624,6 +27310,7 @@ 77F033A22A84E00F006BC67E /* StakingPoolView.swift in Sources */, 77E304A92AEB9F76006FD6F0 /* SwapConfirmInitState.swift in Sources */, 88421055289BBA8D00306F2C /* CurrencyViewLayout.swift in Sources */, + 0CF9767F2CF1293E001D2801 /* SwapExecutionState.swift in Sources */, 845C407D2702812E00BFA50B /* StakingAccountUpdatingService.swift in Sources */, 8430D6C92801A2B500FFB6AE /* WebSocketProtocols.swift in Sources */, 8459A9CC2746A1E9000D6278 /* CrowdloanOffchainSubscriptionHandler.swift in Sources */, @@ -26745,6 +27432,7 @@ 841E2E4E2738159400F250C1 /* RemoteSubscriptionHandlingFactory.swift in Sources */, 84E8BA2229FFB38600FD9F40 /* EthereumTransaction.swift in Sources */, 846A2601267C768500429A7F /* CrowdloanContributionMapper.swift in Sources */, + 0C3272892CD1F83D00FC1B42 /* AssetExchangeAtomicOperationArgs.swift in Sources */, 84CFF1EA26526FBC00DB7CF7 /* StakingBondMoreConfirmationViewFactory.swift in Sources */, 77EFFC8A2A6E7A24009E28F8 /* AccountExistense.swift in Sources */, 8466BB472640152A00E065A8 /* StakingUnbondConfirmViewModelFactory.swift in Sources */, @@ -26773,6 +27461,7 @@ 849DEC5925ED756F00C64C19 /* SubstrateCallFactory.swift in Sources */, 843612BD278FE54D00DC739E /* DAppOperationConfirmInteractor+Protocol.swift in Sources */, 84B24FA62A2F232700F9BF59 /* StakingDashboardActiveCell.swift in Sources */, + 0CCA70732CD0A4FD0082A9C8 /* CrosschainExchangeAtomicOperation.swift in Sources */, 84E0EE0A292D3CB4008B2953 /* GovernanceChainSelectionWireframe.swift in Sources */, 844DBC5E274D1B13009F8351 /* IconWithTitleSubtitleTableViewCell.swift in Sources */, 84DBEA42265E80DD00FDF73C /* LearnMoreViewModel.swift in Sources */, @@ -26811,13 +27500,16 @@ 84EDF66D29C4F41C002173E6 /* EvmBalanceMessage.swift in Sources */, 881CA22529E3F0D100159C5B /* EquilibriumAssetBalanceUpdatingService.swift in Sources */, 845F49ED2928F31100BD6D11 /* ChainModel+Evm.swift in Sources */, + 0C11D8582CC9FED7003EC46D /* AssetsHydraStableswapExchange.swift in Sources */, 848F8B292864503A00204BC4 /* TransferSetupWireframe.swift in Sources */, 84C1E7C929EE990800D37668 /* Web3NameProvider.swift in Sources */, 2DA85A822C80775F00591900 /* ReferendumsPresenter+VoteChildPresenter.swift in Sources */, + 0CA81D522CE50BDB00166969 /* HydraExchangeFeeSupportFetcher.swift in Sources */, 84F18D4C27A1874000CA7554 /* TokenSubscriptionFactory.swift in Sources */, 8846F72B29D6BC1300B8B776 /* Data+base32.swift in Sources */, 4448B591D4A193DBC9E2E3BF /* AccountCreateInteractor.swift in Sources */, 84E6D57C262E2CE8000EA3F5 /* OperationCombiningService.swift in Sources */, + 0CCA70782CD0B4220082A9C8 /* SigningWrapperFactory+Async.swift in Sources */, 8407716228CE09DA007DBD24 /* ParaStkYieldBoostTaskInitFee.swift in Sources */, 0CEB6B492CA561AC00609DC2 /* ParachainResolver.swift in Sources */, 88DC3E25292CAA9300DBCE4D /* IconDetailsView+Style.swift in Sources */, @@ -26830,6 +27522,7 @@ 8460E718284CF124002896E9 /* ParachainStakingValidatorFactoryProtocol.swift in Sources */, 846CDECD258D212D009F3E75 /* AlertImageWithTitleView.swift in Sources */, 84D8F15B24D8136700AF43E9 /* ModalPickerCellProtocol.swift in Sources */, + 0C11D8562CC9F9E6003EC46D /* HydraXYKExchangeEdge.swift in Sources */, 847999A9288862A500D1BAD2 /* MetaAccountOperationFactory+Secrets.swift in Sources */, 8496ADD7276AFEED00306B24 /* DAppBrowserModel.swift in Sources */, 8473B4782A207FE0003DE213 /* StakingDashboardOffchainMapper.swift in Sources */, @@ -26887,6 +27580,7 @@ 848CC94428D9FBDA009EB4B0 /* ConvictionVoting.swift in Sources */, 7D281FEA78E2E5F44990C184 /* AccountImportPresenter.swift in Sources */, 6C56AB4AE63AB2DC73DE98E0 /* AccountImportInteractor.swift in Sources */, + 0C32729E2CD2975400FC1B42 /* HydraExchangeAtomicOperation.swift in Sources */, F4EAC7972642E0D800FBDDC3 /* ControllerAccountViewModelFactory.swift in Sources */, 0C0CB3852AC561FB00EAC516 /* AssetHubSwapOperationFactory.swift in Sources */, 0CB6B2842C57857C00FFE475 /* TriangularedButton+Title.swift in Sources */, @@ -26941,12 +27635,14 @@ 2D5493312C0490A900607BF7 /* GridUnitTransitionCoordinator.swift in Sources */, 2DE668692C9D5839007883AF /* ReferendumTracksVotingDistribution+Diffable.swift in Sources */, 8428228A289B1E5C00163031 /* TableHeaderLayoutUpdatable.swift in Sources */, + 0C6108452CD8827200909928 /* HydraSwapFeeCurrencyService.swift in Sources */, 84EE2FAF2891215200A98816 /* WalletManageTableViewCell.swift in Sources */, 0CA26D8B2BBBB4450086748F /* CloudBackupServiceFactory.swift in Sources */, 8858917C29A707F600320896 /* ReferendumVotersViewFactory.swift in Sources */, 778210862A6588D100256E78 /* DiffableDataStore.swift in Sources */, 0CCDB2DC2B7128A7007BC5D6 /* HydraOmnipoolTokensFactory.swift in Sources */, 849B563527A70DDE007D5528 /* ExtrinsicProcessor+Matching.swift in Sources */, + 0C12BC352CE952EF00AB919D /* SwapRouteViewCell.swift in Sources */, 8804AD8D295B7735001C4E09 /* DAppGlobalSettingsMapper.swift in Sources */, 94B0F0C84AF74B3CD7223C3A /* AccountConfirmPresenter.swift in Sources */, 84B8AA7929F905C800347A37 /* WalletConnectStateInitiating.swift in Sources */, @@ -26957,7 +27653,6 @@ 8863C7AC29D499D30068AD54 /* Web3NameService.swift in Sources */, 8892284828F353A5003F8B9E /* ReferendumsModelFactory.swift in Sources */, 88D02FE32942EA2200E26390 /* PayButtonsRow.swift in Sources */, - 0CD3A67E2AEAAB670059BBEC /* AssetHubFeeService.swift in Sources */, 84B018AC26E01A4100C75E28 /* StakingStateView.swift in Sources */, 842A737C27DCC489006EE1EA /* OperationDetailsTransferView.swift in Sources */, 84880C4429026C3E00CADB06 /* ReferendumDelegatingLocal.swift in Sources */, @@ -27005,6 +27700,7 @@ 77DF404B2B7FFBA400ABDB53 /* ChainNotificationsSettingsProtocols.swift in Sources */, 84644A0C256713D5004EAA4B /* TriangularedBlurView+Inspectable.swift in Sources */, 849A4EF6279A7AEF00AB6709 /* StateminAssetExtras.swift in Sources */, + 0C6108432CD755A300909928 /* AssetExchangeGraphProxy.swift in Sources */, 849E17E627914394002D1744 /* NavigationBarSettings.swift in Sources */, 84B8AA7329F8EFC700347A37 /* DAppInteractionError.swift in Sources */, 8485035029FBC84300AE6909 /* WalletConnectSignModelFactory.swift in Sources */, @@ -27153,11 +27849,13 @@ 888797B229F11EBB0078633F /* SwitchSettingsTableViewCell.swift in Sources */, 84735C6327E05FE6001DD1E6 /* OnChainTransferInteractor.swift in Sources */, 77D326E82B98DF350055FE88 /* NotificationMessage.swift in Sources */, + 0C9BCC582CBD6B4000CBE21B /* AssetsExchangeProtocol.swift in Sources */, 8436E94426C853E4003D4EA7 /* RuntimeSnapshotOperationFactory.swift in Sources */, 885547EB29C8915A008782C1 /* KiltWeb3n.swift in Sources */, 84DD49F428EE91ED00B804F3 /* Gov2LocalMappingFactory.swift in Sources */, 84C1706D2996424800CBE531 /* GovernanceYourDelegationCell.swift in Sources */, 84E0EE04292D336C008B2953 /* GovernanceType.swift in Sources */, + 0C6353452CE463BD00EAB200 /* AssetExchangePathFilter.swift in Sources */, 849B036D2A15E90A009624D9 /* CoingeckoPriceHistorySource.swift in Sources */, 0CB3136A2C0F0EC400353724 /* CloudBackupServiceFacade+Create.swift in Sources */, 2D32BE222C6D247C0047F520 /* FeeAssetSelectionPresentable.swift in Sources */, @@ -27166,6 +27864,7 @@ F41CEB88272FFCB700C06154 /* CrowdloanContributionViewModel.swift in Sources */, 8466781327EC5446007935D3 /* MultilineBalanceView.swift in Sources */, 845B822926F0BB6700D25C72 /* CrowdloanChainSettings.swift in Sources */, + 0CF5F68C2CC7B1B6007BAAC5 /* AssetsExchageGraphReachability.swift in Sources */, 0C13D2FA2A7D473B0054BB6F /* DirectStakingRestrictionsBuilder.swift in Sources */, 0CE933922C043F00000F3EFE /* CloudBackupSyncMediatorFactory.swift in Sources */, 0C7104772C2AC00000487E64 /* CommonMetadataShortenerError.swift in Sources */, @@ -27192,6 +27891,7 @@ AE6F7FDF2685E812002BBC3E /* ValidatorListFilterPresenter.swift in Sources */, 84A5915A292AA83500BCCF8F /* EvmWebSocketOperationFactory.swift in Sources */, 8430AAF626023087005B1066 /* NominatorState.swift in Sources */, + 0CF4ADFD2CEC0A54009C51FA /* HydraExchangeMetaOperation.swift in Sources */, 842A735E27DB2EC4006EE1EA /* OperationDetailsModel.swift in Sources */, 844DAADD28ABC5E2008E11DA /* LedgerDevice.swift in Sources */, 8448221C26B1850D007F4492 /* TitleIconViewModel.swift in Sources */, @@ -27266,6 +27966,7 @@ 842BDB2C278C4FFE00AB4B5A /* DAppBrowserAuthorizedState.swift in Sources */, 849346AF2A1F7F7D00CB75B7 /* MultistakingServices.swift in Sources */, 848919DB26FB663D004DBAD5 /* CrowdloansChainViewModel.swift in Sources */, + 0CA7CEF42CE0D210004328F2 /* PalletAssets+EventCodingPath.swift in Sources */, 842A736D27DB7B5E006EE1EA /* OperationTransferViewModel.swift in Sources */, 842B18022864F9950014CC57 /* CrossChainTransferPresenter.swift in Sources */, 0CB64E672B01E174008F268F /* TransferNetworkSelectionViewModel.swift in Sources */, @@ -27286,6 +27987,7 @@ 84333BD7285682EC00C76A4F /* ValidatorsSelectionParams.swift in Sources */, 84C3F77B2601F08B00D47501 /* NominationViewModel.swift in Sources */, 846A2C4B2529F99400731018 /* AccountRepositoryFactory.swift in Sources */, + 0CF976812CFD04F8001D2801 /* AssetsExchangeStateMediator.swift in Sources */, 0CCE3AAC2BF4762000D55F03 /* CloudBackupDiff+Helper.swift in Sources */, 0C79C89E2A7BEF1200B171E3 /* NominationPoolRecommendationFactory.swift in Sources */, 0C85FF2A2B6D230100FC0014 /* AssetHubReQuoteService.swift in Sources */, @@ -27296,12 +27998,15 @@ 84FBDBDD28C87DA800CC1037 /* ParaStkYieldBoostTasksSource.swift in Sources */, 0CA5BDF12C438A45000A4CDD /* TransactionSubscriptionFactory.swift in Sources */, F4C086C726D1159E00716AEC /* SubqueryEraStakersInfoSource.swift in Sources */, + 0C6108492CD889A000909928 /* AssetsExchange.swift in Sources */, 84884B5F27A1336500FAC549 /* OrmlAssetExtras.swift in Sources */, 849976C827B2B80700B14A6C /* DAppMetamaskWaitingAuthState.swift in Sources */, AEF5071E262369C00098574D /* PurchaseProviderProtocol.swift in Sources */, 848F8B172863584B00204BC4 /* OnChainTransferSetupProtocols.swift in Sources */, + 0CBABFED2CED438A0047F29E /* AssetExchangeOperationPrototype.swift in Sources */, 84F47D532666F13C00F7647A /* KaruraVerifyInfo.swift in Sources */, F4D96B672637E89300B23D3D /* UIView+Separator.swift in Sources */, + 0CB4334C2CC10C3C00F2CB59 /* AssetsHydraExchangeProvider.swift in Sources */, 77A6F5AB2A2E0B31004AFD1A /* ReceiveAssetOperationPresenter.swift in Sources */, 845B891C295960AD00EE25B0 /* SecuredPresentable.swift in Sources */, 0CEB6B552CA70CB400609DC2 /* AssetBalance+OpenGov.swift in Sources */, @@ -27340,6 +28045,7 @@ 843F9ABC29DDAF8F004F1737 /* JSONRPCTimeout.swift in Sources */, 841E5540282D524800C8438F /* ParastakingLocalStorageSubscriber.swift in Sources */, 84350AD228457CC30031EF24 /* BaseValidatorInfoViewModelFactory.swift in Sources */, + 0CF976792CF09076001D2801 /* SwapExecutionView.swift in Sources */, 84C11F1F2842D5D5007F7C05 /* ParaStkCollatorsSearchViewModel.swift in Sources */, 1BFC90E1D8646F7429FFD5E6 /* ExportMnemonicProtocols.swift in Sources */, 848B59BA28BCB3CA0009543C /* LedgerBaseAccountConfirmationWireframe.swift in Sources */, @@ -27394,6 +28100,7 @@ 8499FECC27BF8F4A00712589 /* NftModelMapper.swift in Sources */, 842E9E962A2A279800759972 /* StakingDashboardLocalStorageHandler.swift in Sources */, C89D156BA8B690E8E4DE19ED /* ExportSeedProtocols.swift in Sources */, + 0C2DA8AA2CC2A2E2001F79C8 /* AnyAssetExchangeEdge.swift in Sources */, 848F5FE52989130B0058CD74 /* GovernanceOffchainDelegations.swift in Sources */, 84F1CB3327CE575E0095D523 /* NftListUniquesViewModel.swift in Sources */, AEBE173B262F3E6600DF257C /* StakingPayoutConfirmViewModelFactory.swift in Sources */, @@ -27422,16 +28129,18 @@ 84FB298C2639ABA500BE0FCD /* YourValidatorList.swift in Sources */, 8453DE5B28FD32B50055345C /* StorageSubscriptionObserver.swift in Sources */, 0CA719872B78AA71000B086E /* HydraRouter.swift in Sources */, - 0C85FF262B6CB9B400FC0014 /* HydraCallPathFactory.swift in Sources */, + 0C2DA89A2CC21419001F79C8 /* GraphQuotableEdge.swift in Sources */, 84873AFF26028E2B000A83EE /* StakingStateMachine.swift in Sources */, 8442002528E6FEEE00C49C4A /* ReferendumsProtocols.swift in Sources */, 3D1FB0EF87D42F08D9250552 /* PurchasePresenter.swift in Sources */, + 0CA7CEF22CE0D14F004328F2 /* PalletAssetsTokenDepositEventMatcher.swift in Sources */, 0C12A2492AA35AE300C7FA49 /* RelaychainStakingValidatorFacade.swift in Sources */, 2DA85A842C80780600591900 /* ReferendumsPresenter+ViewUpdate.swift in Sources */, F4F22976260DBF3F00ACFDB8 /* StakingPayoutRewardTableCell.swift in Sources */, 8428229A289BC8E400163031 /* AddAccount+ParitySignerWelcomeWireframe.swift in Sources */, 8471577D2910F18300D7D003 /* GovernanceUnlockProtocols.swift in Sources */, 0C500B1F2B04EA9100ABEE70 /* AssetConversionPallet+Event.swift in Sources */, + 0CBABFF62CEEACB60047F29E /* RoundedButton+Set.swift in Sources */, 2CF2F93AF862CF54FC46B560 /* PurchaseInteractor.swift in Sources */, 0C9951D32AE2DB0200B65615 /* PromotionViewModelFactory.swift in Sources */, 77EFFC912A7276F1009E28F8 /* StakingTypeAccountView.swift in Sources */, @@ -27464,6 +28173,7 @@ 0CCA245D2AC6918800AEF23D /* XcmV3Junction.swift in Sources */, 8412AF992789AB76008A6C22 /* PolkadotExtensionMetadata.swift in Sources */, 847999AF2888A45700D1BAD2 /* AddAccount+CreateWatchOnlyWireframe.swift in Sources */, + 0CB433462CC0C3F200F2CB59 /* CrosschainAssetsExchangeProvider.swift in Sources */, 2D6287492C94395600060814 /* SwipeGovVotingListViewModel.swift in Sources */, 846E5010277998040049B659 /* DAppAuthRequest.swift in Sources */, 0C59E8F82AA76833001E11F3 /* OperationDetailsContractProvider.swift in Sources */, @@ -27544,6 +28254,7 @@ B02EAF42C91E069FE6872EE0 /* SelectValidatorsConfirmWireframe.swift in Sources */, 8407716628CE6B2A007DBD24 /* ParaStkYieldBoostConfirmModel.swift in Sources */, AEA0C8B4267BA40C00F9666F /* SelectedValidatorListViewModelFactory.swift in Sources */, + 0CF976772CF08EF9001D2801 /* OperationExecutionProgressView.swift in Sources */, 2D4F55032CCA60F300B65B76 /* AssetsSearchCollectionViewDataSource.swift in Sources */, 6ECD0116CD39D8F55D246864 /* SelectValidatorsConfirmPresenter.swift in Sources */, 84906FEA28AFCC3F0049B57D /* LoadableStackCellView.swift in Sources */, @@ -27557,6 +28268,7 @@ 0678271BE1BA5BBC084F478A /* RecommendedValidatorListWireframe.swift in Sources */, F441BE0E263984DD0096B67B /* BondExtraCall.swift in Sources */, 8824D42829032BF60022D778 /* ReferendumStateLocal+Presenter.swift in Sources */, + 0CF3A6CB2CE949ED00F93C49 /* SwapRouteView.swift in Sources */, 0C13D2FC2A7D4B220054BB6F /* RelaychainStakingRestrictionsBuilder.swift in Sources */, 84FB9E20285C5C9E00B42FC0 /* XcmJunction.swift in Sources */, 84EBA4F027AD26A5000AEEAD /* AssetBalanceId.swift in Sources */, @@ -27567,6 +28279,7 @@ 88AC5AD82948A8A20056DD40 /* TransactionHistoryDataSource.swift in Sources */, 0C7104892C2BBF8700487E64 /* GenericLedgerDiscoverInteractor.swift in Sources */, 8472C5B3265CF9C500E2481B /* StakingRewardDestConfirmViewController.swift in Sources */, + 0C883A762CE25E6800CAB4C8 /* HydraSwapEventParser.swift in Sources */, 0C37AFB92B5562B500009ECA /* StakingEraStakersExposureFactory.swift in Sources */, 0C59E8F62AA76772001E11F3 /* OperationDetailsExtrinsicProvider.swift in Sources */, B071927DF8DD5C3CA84494BA /* RecommendedValidatorListViewController.swift in Sources */, @@ -27635,7 +28348,6 @@ 77E304B72AEFC348006FD6F0 /* FeeAssetSelectSheetViewModel.swift in Sources */, 0CB0646A2A40572C00BFBA3F /* AmountInputViewModel.swift in Sources */, 773A37542B398CEB006AC4AA /* WalletNotificationService.swift in Sources */, - 0C85FF282B6CBA9D00FC0014 /* AssetHubCallPathFactory.swift in Sources */, 847A25CA28D85204006AC9F5 /* ReferendumInfo.swift in Sources */, 5869563D0EA593FBD02C169C /* StakingPayoutConfirmationProtocols.swift in Sources */, 846B749828B4BA0500C39B93 /* LedgerAccountsStore.swift in Sources */, @@ -27646,9 +28358,9 @@ 880E40FF298CF1ED0077B18B /* VotesProtocols.swift in Sources */, 774A980D2B0D0873009146CA /* OpenScreenUrlParsingService.swift in Sources */, 84FEF3E528089FFB0042CBE7 /* TextInputField.swift in Sources */, - 0CEB4ED32AF1689D0048FD84 /* AssetConversionAggregationFactory.swift in Sources */, 84D17EE12805A62600F7BAFF /* DAppAlertPresentable.swift in Sources */, A871B6ABACAE8A811010F792 /* StakingPayoutConfirmationWireframe.swift in Sources */, + 0CBABFE92CED31C80047F29E /* AssetHubExchangeMetaOperation.swift in Sources */, 1795E946F1E386442E96E2BC /* StakingPayoutConfirmationPresenter.swift in Sources */, AEFA82BC4285117096BCBB16 /* StakingPayoutConfirmationInteractor.swift in Sources */, 84DBBE13274BE955009F52BB /* HintView.swift in Sources */, @@ -27677,6 +28389,7 @@ 5FE687B860FC10AB08518A6E /* WalletHistoryFilterPresenter.swift in Sources */, 640A79BD1335394818E70366 /* WalletHistoryFilterViewController.swift in Sources */, DD090C2ED91726FF7779F6C7 /* WalletHistoryFilterViewFactory.swift in Sources */, + 0CF976722CEFF01D001D2801 /* SwapExecutionModel.swift in Sources */, 2D65F6D92CD0067500CC6F72 /* AppearanceFacade.swift in Sources */, 84ED6BE0286900B600B3C558 /* TransferNetworkContainerViewModel.swift in Sources */, 845B0819291905CA005785D3 /* Gov1LockStateFactory.swift in Sources */, @@ -27727,7 +28440,6 @@ 84AEEE6C27A92AEF005EBA77 /* AssetListFlowLayout.swift in Sources */, 8410D6972815DE3300B83CF7 /* IconDetails+Hint.swift in Sources */, 4FBD73DD27CAA339727616B5 /* StakingUnbondSetupPresenter.swift in Sources */, - 0C363E9A2B6B69D60065AFA6 /* HydraFeeService.swift in Sources */, 84CFF1F026526FBC00DB7CF7 /* StakingBondMoreConfirmationViewLayout.swift in Sources */, F83EA1F4ECE14C390C0B287F /* StakingUnbondSetupInteractor.swift in Sources */, 2DAF539B2C1842D00076B4B6 /* NetworkDetailsNodeView.swift in Sources */, @@ -27755,7 +28467,6 @@ 0C37AFC12B56EBC000009ECA /* MaxNominationsOperationFactory.swift in Sources */, 0C0E0A952B3E89D200865F10 /* ExtrinsicWrapperResult.swift in Sources */, F0C3DB0CEE1975626B0014A8 /* StakingUnbondConfirmInteractor.swift in Sources */, - 0C0CB3822AC545A800EAC516 /* AssetConversionExtrinsicService.swift in Sources */, 2D6287372C917E3900060814 /* VotingPowerMapper.swift in Sources */, 2D7E4A192C406FED00455509 /* CustomNetworkDTO.swift in Sources */, 77A4F4032B036615006294BC /* Optional+Result.swift in Sources */, @@ -27764,6 +28475,7 @@ 842AEB81292F34B600C61B0C /* RemoteChainExternalApi.swift in Sources */, 8472C5AE265CF9C500E2481B /* StakingRewardDestConfirmViewModel.swift in Sources */, 88BB0B4729C2FB5300D041C1 /* Caip19.swift in Sources */, + 0CA7CEEE2CE0CA75004328F2 /* TokenDepositEventMatching.swift in Sources */, 0C13D3022A7D53C10054BB6F /* HybridStakingRecommendationMediator.swift in Sources */, 842A736227DB3032006EE1EA /* OperationExtrinsicModel.swift in Sources */, 843E9B2827C83985009C143A /* AssetListNftsCell.swift in Sources */, @@ -27798,7 +28510,6 @@ 77A6F5AE2A2E0C7C004AFD1A /* ReceiveAssetOperationWireframe.swift in Sources */, 0C543E972AAB1B350035F45F /* ElectedAndPrefValidators.swift in Sources */, 8489A6CE27FC5C5E0040C066 /* StakingActionsView.swift in Sources */, - 0C85FF372B6E0BB300FC0014 /* AssetConversionAggregationFactory+AssetHub.swift in Sources */, 8455F19E2A1E4956003F072D /* RelaychainMultistakingUpdateService.swift in Sources */, 8455F1A42A1F606B003F072D /* OnchainStorage.swift in Sources */, 7754BD582B50232C0099C13E /* ProxyDepositCalculator.swift in Sources */, @@ -27813,6 +28524,7 @@ 841E553C282D44BA00C8438F /* ParachainStakingAccountSubscriptionService.swift in Sources */, 84C479C129309E58003DF82B /* BasePolkassemblyOperationFactory.swift in Sources */, 0CA5BDF32C4476A3000A4CDD /* ConnectionCreationParams.swift in Sources */, + 0C03877F2D0A1043000A2F24 /* SwapRouteDetailsItemView.swift in Sources */, 770ABB8C2B85587200132465 /* PushNotificationTopicSettings.swift in Sources */, 0C846B892BE50BF2000EBFC2 /* VaraRewardEngine.swift in Sources */, 84300B2C26C10C9B00D64514 /* ConnectionStateReporting.swift in Sources */, @@ -27848,8 +28560,10 @@ C6E5671768DA68535DA5B1C7 /* ControllerAccountConfirmationViewFactory.swift in Sources */, 84E8BA0D29FF898600FD9F40 /* BaseExtrinsicOperationFactory.swift in Sources */, 270C21973CB61F0BF3D2D1E3 /* CrowdloanListProtocols.swift in Sources */, + 0C11D85E2CCA470D003EC46D /* AssetsExchangeRouteManager.swift in Sources */, 6D61E43A79BDF5EA6CA9E85D /* CrowdloanListWireframe.swift in Sources */, A090FF206B56A0E465C62072 /* CrowdloanListPresenter.swift in Sources */, + 0CF5F68A2CC7AF84007BAAC5 /* AssetExchangeGraphPath.swift in Sources */, 2D6F51B32CE120AB00564C00 /* SpendAssetOperationNetworkListInteractor.swift in Sources */, 846CA7802709A41E0011124C /* StakingAnalyticsLocalSubscriptionHandler.swift in Sources */, 849DF02D26C40B7900B702F4 /* RuntimeFilesOperationFactory.swift in Sources */, @@ -27962,6 +28676,7 @@ 0C8FDBD72CB39D7000775D7F /* AssetListTotalBalanceView.swift in Sources */, 8407715628CB7D6B007DBD24 /* ParaStkYieldBoostScheduleInteractor.swift in Sources */, 84E2ABCA2992891300A5D3C1 /* GovernanceDelegateInteractor.swift in Sources */, + 0CBABFE72CED31690047F29E /* CrosschainExchangeMetaOperation.swift in Sources */, 84D1ABDC27E1B4FE0073C631 /* ChainAssetViewModel.swift in Sources */, 0CD352952ACAF59900B3E446 /* BigRational.swift in Sources */, AEE0C442272A9AF7009F9AD5 /* BaseChainAccountConfirmInteractor.swift in Sources */, @@ -27986,7 +28701,6 @@ 880CC0A829E7F13B008C7F65 /* EquilibriumAccountBalancesSubscription.swift in Sources */, 849976D227B3AC3000B14A6C /* MetamaskChain.swift in Sources */, 84468A112867BCD400BCBE00 /* XcmTransferService+Protocol.swift in Sources */, - 0C85FF392B6E0C0600FC0014 /* AssetConversionAggregationFactory+HydraDx.swift in Sources */, 880855F028D099F2004255E7 /* CrowdloanOnChainSyncService.swift in Sources */, 7736186F2B874C3B00C922FB /* SelectTracksPresenter.swift in Sources */, 843CE3A627D2098100436F4E /* NftDetailsLabel.swift in Sources */, @@ -28005,6 +28719,7 @@ 8499FEE427C1059700712589 /* RMRKV1SyncService.swift in Sources */, 8882FC6529E5D09300DDA90B /* EquillibriumAssetsBalanceUpdater.swift in Sources */, 842A737527DB8338006EE1EA /* OperationDetailsViewModelFactory.swift in Sources */, + 0C32728F2CD203F000FC1B42 /* XcmTotalFeeModel.swift in Sources */, 844C3E712A09126F00C4305F /* WalletsChooseViewFactory.swift in Sources */, 93434E8E407A6C63D8862A21 /* ChainAssetSelectionProtocols.swift in Sources */, 84E25BEC27E87D5400290BF1 /* TransferDataValidatorFactory.swift in Sources */, @@ -28025,6 +28740,7 @@ 2932BD7922FDCD64F4E9A57D /* AssetListPresenter.swift in Sources */, 2D5A61CD2C0F58DD006D58E3 /* NetworksEmptyPlaceholderView.swift in Sources */, 2D65F6CF2CCF8DA100CC6F72 /* AppearanceIconsOptions.swift in Sources */, + 0C423ABB2CDBAE9B00A64941 /* AssetExchangeFacade.swift in Sources */, 84355CF628B63D19004E5C5E /* LedgerCrypto+Conversion.swift in Sources */, 8463781F2979DACB0034162B /* ReferendumsActivityViewModelFactory.swift in Sources */, 84B8AA8B29FA3B0500347A37 /* WalletConnectBaseState.swift in Sources */, @@ -28044,6 +28760,7 @@ 7D707DDD180999C63FD0C4ED /* AssetListViewController.swift in Sources */, 75E689BC8D16786DF2674171 /* AssetListViewLayout.swift in Sources */, 88FAE78828FCF8E200130B47 /* ReferendumDetailsTitleView.swift in Sources */, + 0C883A742CE241CB00CAB4C8 /* AssetConversionEventsMatching.swift in Sources */, 847EA1D42A1CA43A00F1CBD8 /* SubqueryNodes.swift in Sources */, 8483B15328F940080048B295 /* ReferendumVotersModel.swift in Sources */, 2736BAABAE1389260A0B28D6 /* AssetListViewFactory.swift in Sources */, @@ -28056,6 +28773,7 @@ 8452585527ABF270004F9082 /* AssetListEmptyCell.swift in Sources */, D72773CE9AF638387DA9BA77 /* MoonbeamTermsInteractor.swift in Sources */, 888B8E95291D274B00AA78E9 /* MercuryoProvider.swift in Sources */, + 0C11D85C2CCA3CCC003EC46D /* AssetsHydraXYKExchange.swift in Sources */, 2D95DF5F2C6FAFF6009BB063 /* ExtrinsicFeeEstimatingWrapperFactory.swift in Sources */, 2B1E63AF98584341D670FB40 /* MoonbeamTermsViewController.swift in Sources */, B1B0F4818510EB082ACA83AB /* MoonbeamTermsViewLayout.swift in Sources */, @@ -28076,6 +28794,7 @@ 0CCCDF802B64BE7C00473D42 /* HydraDx+Constants.swift in Sources */, 716F0819BAB14322E34E416C /* CrowdloanYourContributionsPresenter.swift in Sources */, F0675F495766D07473B065F7 /* CrowdloanYourContributionsInteractor.swift in Sources */, + 0C6108412CD74DD200909928 /* AssetExchangeFeeEstimatingFactory.swift in Sources */, 845B080D2918D4F8005785D3 /* Democracy+Call.swift in Sources */, 8425D0EE28FE9BF1003B782A /* ReferendumVoteAction.swift in Sources */, 84E8BA1A29FFB38600FD9F40 /* EthereumBlockObject.swift in Sources */, @@ -28084,6 +28803,8 @@ 0C3205C82A877E5A002EB914 /* EvmGasPriceProviderFactory.swift in Sources */, 487A912B697604FE3367FAEC /* CrowdloanYourContributionsViewLayout.swift in Sources */, 3F7F10D0E1BDE09CBE64BD2D /* CrowdloanYourContributionsViewFactory.swift in Sources */, + 0CA7CEE62CE0C23B004328F2 /* SystemPallet+Events.swift in Sources */, + 0CA7CEE72CE0C23B004328F2 /* SystemPallet+StoragePath.swift in Sources */, E37BB7A393FFEFC350B4EA3D /* AdvancedWalletProtocols.swift in Sources */, 0CA957222B6A507A009AD757 /* HydraSwapParamsService.swift in Sources */, 84D6E2FA283AE6590031D6FD /* ExtrinsicSubmissionPresenting.swift in Sources */, @@ -28140,6 +28861,7 @@ 0C7104A82C2D11EB00487E64 /* GenericLedgerTxConfirmInteractor.swift in Sources */, 40087CC8E6A91976807F7D44 /* DAppBrowserWireframe.swift in Sources */, 88FF5C7C29C8360400D1CB5D /* Caip19+ParseError.swift in Sources */, + 0C19CC9F2CC6EBBC007F8ED8 /* CrosschainAssetsExchange.swift in Sources */, 37E229641DCDF64AC5AF1DCD /* DAppBrowserPresenter.swift in Sources */, 91530F7301CA39654E008580 /* DAppBrowserInteractor.swift in Sources */, 8451720F298C495500489EF1 /* BorderedIconLabelView+TrackStyle.swift in Sources */, @@ -28199,7 +28921,7 @@ B409644ED1E20062A3EA0316 /* DAppTxDetailsViewController.swift in Sources */, DAD46B2B29A446C19A6ABF2D /* DAppTxDetailsViewLayout.swift in Sources */, 2D442CEF2CD0F7DF00A0380F /* AppearanceDepending.swift in Sources */, - 0CD352972ACAFADA00B3E446 /* AssetConversionOperationFactory.swift in Sources */, + 0CD352972ACAFADA00B3E446 /* AssetConversionOperationError.swift in Sources */, 88AC186328CA3F0000892A9B /* GenericCollectionViewLayout.swift in Sources */, A97F32D057BFEFBCC478A09C /* DAppTxDetailsViewFactory.swift in Sources */, D567BAAF620EDB9F4975C800 /* DAppAuthConfirmProtocols.swift in Sources */, @@ -28219,6 +28941,7 @@ 841E5565282EA76C00C8438F /* ParachainStakingBaseState.swift in Sources */, 77CB33D72A3998FD00B6709A /* Array+Sort.swift in Sources */, 84B8AA8129F9097D00347A37 /* WalletConnectTransport.swift in Sources */, + 0C3272932CD246DD00FC1B42 /* AssetExchangeOperationFee+Crosschain.swift in Sources */, 04B85867D67D56994D99FF14 /* NftListProtocols.swift in Sources */, E9625CE429290F5504728D62 /* NftListWireframe.swift in Sources */, 841816712824F4F30007684A /* ParachainStakingCollatorSnapshot.swift in Sources */, @@ -28228,6 +28951,7 @@ 845B811528F43C350040CE84 /* Treasury.swift in Sources */, 0C13DFD72AFA50A200E5F355 /* SwapIssueViewModelFactory.swift in Sources */, 0C7C886C2A9622F800DD96A1 /* StakingSelectedEntityViewModel.swift in Sources */, + 0CA81D592CE60AA700166969 /* AssetExchangeEdgeType.swift in Sources */, 2D4F55262CCBFC4700B65B76 /* AssetOperationNetworkListCell.swift in Sources */, 84C3420B283187D800156569 /* BlockTimeEstimationService.swift in Sources */, EB376E61CD1C39AC148DE80C /* NftListViewController.swift in Sources */, @@ -28299,6 +29023,7 @@ 0C9525E72A7AFA2C00BD724D /* ValueResolver.swift in Sources */, 84DD261429ACA9030032A598 /* VotersInfoOperationFactory.swift in Sources */, 0C63909B2BF23E3A0015D467 /* NSPredicate+CloudBackup.swift in Sources */, + 0C11D8542CC9F82C003EC46D /* HydraStableswapExchangeEdge.swift in Sources */, 11C6F4CD5B167DE4E9E7F654 /* DAppPhishingWireframe.swift in Sources */, 77DF40492B7FE7B700ABDB53 /* ChainNotificationSettingsViewLayout.swift in Sources */, 4014ED301AA53DA0B07B4221 /* DAppPhishingPresenter.swift in Sources */, @@ -28314,7 +29039,6 @@ F5CA222FA684AAC8B556E667 /* DAppAddFavoritePresenter.swift in Sources */, 77C9F7612B8455E10099A465 /* GovernanceNotificationsModel.swift in Sources */, AC904E313DC15AE40C927946 /* DAppAddFavoriteInteractor.swift in Sources */, - 0CD352932ACAD7A500B3E446 /* AssetHubExtrinsicService.swift in Sources */, 006BEDBD2F98FF54DB993D8C /* DAppAddFavoriteViewController.swift in Sources */, 84FFE45D28620833002432BB /* XcmTransferResolutionService.swift in Sources */, D3F199376DAEBF380C5FFD9D /* DAppAddFavoriteViewLayout.swift in Sources */, @@ -28338,7 +29062,7 @@ 0CE9338A2C00538D000F3EFE /* RuntimeTypeRegistryFactory.swift in Sources */, 148748ACAE23B7D15144015B /* DAppAuthSettingsViewFactory.swift in Sources */, B1F86CA723BB4D69C5EF989D /* ParaStkStakeSetupProtocols.swift in Sources */, - 771901AB2AE9581400D9C918 /* SwapConfirmViewModelFactory.swift in Sources */, + 771901AB2AE9581400D9C918 /* SwapDetailsViewModelFactory.swift in Sources */, 623474C49445578F030291B0 /* ParaStkStakeSetupWireframe.swift in Sources */, 8490110B29E5A491005D688B /* URIScanViewFactory.swift in Sources */, 840B3D672899BFD200DA1DA9 /* QRScannerViewSettings.swift in Sources */, @@ -28381,6 +29105,7 @@ F85E4E18D7D535538D52B950 /* ParaStkSelectCollatorsViewLayout.swift in Sources */, C5B6C00F8B0E3D89CBF1A8DB /* ParaStkSelectCollatorsViewFactory.swift in Sources */, 2A3000D01CD4F8E517EE1BA2 /* ParaStkCollatorFiltersProtocols.swift in Sources */, + 0CA7CEE22CE0BBE9004328F2 /* SubstrateEventsRepository.swift in Sources */, 88B1C34629BA0CEE00DCA101 /* BagListBounds.swift in Sources */, F75C2AAA76C49EDB8174B590 /* ParaStkCollatorFiltersWireframe.swift in Sources */, 7D7D40581C276D60713822E9 /* ParaStkCollatorFiltersPresenter.swift in Sources */, @@ -28389,6 +29114,7 @@ 0DAD4FE2F8B905CEBADAB3EA /* ParaStkCollatorFiltersViewController.swift in Sources */, 6A776A53FEC109C875113B38 /* ParaStkCollatorFiltersViewFactory.swift in Sources */, 3B6F50061AD9FC31D6712D9F /* ParaStkCollatorsSearchProtocols.swift in Sources */, + 0C61084F2CD92C6F00909928 /* GraphEdgeFiltering.swift in Sources */, 52326E49B049C54434C95132 /* ParaStkCollatorsSearchWireframe.swift in Sources */, 84117078285B1050006F4DFB /* XcmAssetTransfer.swift in Sources */, 0C3205D82A896009002EB914 /* EvmDefaultNonceProvider.swift in Sources */, @@ -28417,6 +29143,7 @@ 848F8B2B2864824200204BC4 /* AssetListChainControlView.swift in Sources */, 4F03B1138E7160AEA00B7793 /* ParaStkCollatorInfoViewFactory.swift in Sources */, 0119531FAE0D22EA9464F84D /* ParaStkYourCollatorsProtocols.swift in Sources */, + 0C6108342CD5F77E00909928 /* ExtrinsicFeeEstimatorHost.swift in Sources */, 77FE76402B67682A00ADA73F /* ProxyAccountUpdatingService.swift in Sources */, 542588DA751A44C993BC1F27 /* ParaStkYourCollatorsWireframe.swift in Sources */, D6D9D16440AB588F581AF5BA /* ParaStkYourCollatorsPresenter.swift in Sources */, @@ -28516,6 +29243,7 @@ 777BD86229F97322004969A2 /* ReferendumsFilter.swift in Sources */, 773090512B559196002577AE /* ProxyDataValidatorFactory.swift in Sources */, 2D1C5D1C2C25ACA000E2DBDD /* CustomNetworkBaseInteractor.swift in Sources */, + 0CBABFF92CEF33BE0047F29E /* SwapExecutionDetailsView.swift in Sources */, 77FF02692B21ECB900B655BC /* ProxyTableViewCell.swift in Sources */, 84282288289AC80600163031 /* MultiValueView+Factory.swift in Sources */, 0CA5BDEB2C4171D9000A4CDD /* BalanceRemoteSubscriptionHandlingProxy.swift in Sources */, @@ -28528,6 +29256,7 @@ 2450083471CD071346371995 /* MessageSheetWireframe.swift in Sources */, 0CB3136C2C0F34CB00353724 /* CloudBackupEnterPasswordSetWireframe.swift in Sources */, 411E74233593A329298C6405 /* MessageSheetPresenter.swift in Sources */, + 0C2DA89C2CC215D0001F79C8 /* AssetExchangeGraphEdge.swift in Sources */, 843461EC290D0D9400379936 /* GovernanceUnlockSchedule.swift in Sources */, 3FC436AED4098456EDEAF484 /* MessageSheetViewController.swift in Sources */, 0C1338102AB832B30036BCD6 /* QRImageViewModel.swift in Sources */, @@ -28554,6 +29283,7 @@ 99781C3020E07C022DBF5280 /* ChangeWatchOnlyViewLayout.swift in Sources */, 832B616B89972C96D98023DB /* ChangeWatchOnlyViewFactory.swift in Sources */, 2D7C8A932C2E9D9F0044E601 /* NetworkNodeCorrespondingTrait.swift in Sources */, + 0C32728D2CD1F90100FC1B42 /* AssetExchangeSwapLimit.swift in Sources */, 884A7532299CB22F00FEFC30 /* TrackTableViewCell.swift in Sources */, B310C3D126C304851A40CFA9 /* ChainAddressDetailsProtocols.swift in Sources */, FB405A41F9B89097016D4C78 /* ChainAddressDetailsWireframe.swift in Sources */, @@ -28688,6 +29418,7 @@ 77E304B22AEFC0F3006FD6F0 /* FeeAssetSelectSheetViewController.swift in Sources */, 2D039DE82BFCA83600A928E5 /* BindableGestureRecognizer.swift in Sources */, 84715783291132B400D7D003 /* GovernanceUnlockTableViewCell.swift in Sources */, + 0C32729C2CD28C4000FC1B42 /* AssetExchangeOperationFee+AssetHub.swift in Sources */, 84A5915E292B3C3D00BCCF8F /* EvmTransactionBuilder+Transfer.swift in Sources */, CD4240B756F20C338A8B3589 /* LedgerInstructionsWireframe.swift in Sources */, 91A1286763617DE022BD495F /* LedgerInstructionsPresenter.swift in Sources */, @@ -28704,6 +29435,7 @@ 964DE461970AF6B89D0968C5 /* YourWalletsViewFactory.swift in Sources */, 9E40464B7687006B1EE75C72 /* LocksProtocols.swift in Sources */, 30413A3C5ADB96B7D663F94D /* LocksWireframe.swift in Sources */, + 0C0387982D0CE605000A2F24 /* SwapFeeDetailsViewModel.swift in Sources */, 2DE302ED2C32E4D10082FB5C /* CustomNetworkBaseInteractorError.swift in Sources */, BE301A0F2286CCEF6A02D341 /* LocksPresenter.swift in Sources */, 0C77B5612A8371AA00B5AE08 /* StaticValidatorListProtocols.swift in Sources */, @@ -28756,7 +29488,9 @@ 5E3B1E6B9E94848B186FD4D1 /* ReferendumDetailsInteractor.swift in Sources */, 77740BC02AD4A80D00E8C06F /* SwapInfoView.swift in Sources */, 0C495B9B2B452CD600B8D339 /* ProxyResolutionGraph.swift in Sources */, + 0C3272952CD24F9600FC1B42 /* AssetHubExchangeAtomicOperation.swift in Sources */, 488E4467895040EA85FDCC79 /* ReferendumDetailsViewController.swift in Sources */, + 0C2DA8A22CC21E5F001F79C8 /* AssetHubExchangeEdge.swift in Sources */, 0C992C4A2B5530A000ACC129 /* StakingClaimedRewardsOperationFactory.swift in Sources */, 845B811D28F44A700040CE84 /* ReferendumActionLocal.swift in Sources */, 77E255692A16148000B644C3 /* StakingRewardsFilter.swift in Sources */, @@ -28794,6 +29528,7 @@ F35B520D7955A70588AB593C /* ReferendumVoteConfirmWireframe.swift in Sources */, F0ADB63765A8EAA19D85C30B /* ReferendumVoteConfirmPresenter.swift in Sources */, 84C9CF3D291AF1B1002BF328 /* GovernanceOffchainApi.swift in Sources */, + 0CFFB9D32D11A67C00172E8C /* XcmTokensArrivalDetector.swift in Sources */, 77C3D7D52B5E7E7800997BA0 /* StakingRemoveProxyInteractor.swift in Sources */, 84BAD21A293B18EC00C55C49 /* RemoteAssetModel.swift in Sources */, DE52F23521D54A07F558EB1B /* ReferendumVoteConfirmInteractor.swift in Sources */, @@ -28823,6 +29558,7 @@ 62B2298F132DB0CE0794DD7A /* MarkdownDescriptionWireframe.swift in Sources */, A05C2B3458F12EFE1633D917 /* MarkdownDescriptionPresenter.swift in Sources */, 91201789084DD9A419CA8CD3 /* MarkdownDescriptionViewController.swift in Sources */, + 0C2A3C982CDCCECC00A0E2B3 /* SwapTokensFlowState.swift in Sources */, 84A0166929A4E6AC00C9D415 /* MarkdownDescriptionModel.swift in Sources */, 3A4743C7C74BE4F74F6390F6 /* MarkdownDescriptionViewLayout.swift in Sources */, 0CD846212BDCA9F60026B5CA /* WalletImportOptionPrimaryView.swift in Sources */, @@ -28834,6 +29570,7 @@ 0CC2E56A2A6E6EBB004092E7 /* LocalStorageProviderObserving.swift in Sources */, 2D389B172C875B3200256C7B /* SwipeGovInteractor.swift in Sources */, A3BDFA01A32B6C7463E6EFFA /* GovernanceUnlockConfirmPresenter.swift in Sources */, + 0CA7CEF02CE0CC13004328F2 /* BalancesPallet+EventCodingPath.swift in Sources */, 62649D3FB6AACB508872C67A /* GovernanceUnlockConfirmInteractor.swift in Sources */, 6D603098CCF0B65AA726AD38 /* GovernanceUnlockConfirmViewController.swift in Sources */, 7DB7E81CC2F880E4736DE062 /* GovernanceUnlockConfirmViewLayout.swift in Sources */, @@ -28922,6 +29659,7 @@ 5510625BDA756B939ED7C586 /* AddDelegationPresenter.swift in Sources */, 0C7C88612A94A54E00DD96A1 /* StakingNPoolsViewModelFactory+Alerts.swift in Sources */, C6E220DA6AD9A938083179CB /* AddDelegationInteractor.swift in Sources */, + 0CA7CEF82CE0D575004328F2 /* TokensPalletDepositEventMatcher.swift in Sources */, 8E74A13BA73160F88B2B0948 /* AddDelegationViewController.swift in Sources */, CEED39FF1C586C00B56B1F0C /* AddDelegationViewLayout.swift in Sources */, 2D389B0E2C86E94A00256C7B /* SwipeGovEmptyStateView.swift in Sources */, @@ -28965,12 +29703,14 @@ AA3AE7900853DFB4D6FC3F96 /* DelegateVotedReferendaViewFactory.swift in Sources */, 179DDEB4F2431F02D60117BD /* GovernanceSelectTracksProtocols.swift in Sources */, A1FA23B8A833B6896104ABA6 /* GovernanceSelectTracksWireframe.swift in Sources */, + 0CF976752CF082BF001D2801 /* SwapExecutionViewModel.swift in Sources */, A6E762549FF85393D1B69007 /* GovernanceSelectTracksPresenter.swift in Sources */, 4B83231E151422897F71408F /* GovernanceSelectTracksInteractor.swift in Sources */, EAAB9E53189BC6394C5900D2 /* GovernanceSelectTracksViewController.swift in Sources */, 2D81FAEB2CC65CF800BA0E46 /* TokensManageSearchView.swift in Sources */, 347BBBBCC84CA155006FDCDB /* GovernanceSelectTracksViewLayout.swift in Sources */, 0C59E8CD2AA5D673001E11F3 /* PooledBalanceUpdatingService.swift in Sources */, + 0C2DA8A42CC223D9001F79C8 /* AssetsHydraExchangeEdge.swift in Sources */, 0C71049C2C2C422500487E64 /* BaseLedgerWalletConfirmInteractor.swift in Sources */, 60B66AA63089FC2A3A701CF2 /* GovernanceSelectTracksViewFactory.swift in Sources */, 1F45D221E855D5340572C243 /* GovernanceUnavailableTracksProtocols.swift in Sources */, @@ -28995,6 +29735,7 @@ 845B823629C8FF2900D187CB /* EtherscanBaseOperationFactory.swift in Sources */, 473EDA64B1A18BA80189142D /* GovernanceRemoveVotesConfirmProtocols.swift in Sources */, 590717389ECBA34FA08F247B /* GovernanceRemoveVotesConfirmWireframe.swift in Sources */, + 0C6353472CE46FB200EAB200 /* AssetExchangeSufficiencyProvider.swift in Sources */, 544C8EB3D71227FAF2FD4658 /* GovernanceRemoveVotesConfirmPresenter.swift in Sources */, C1E74FC31BFB401B1745C2E8 /* GovernanceRemoveVotesConfirmInteractor.swift in Sources */, AD1920A8DE1C9A4D6502473F /* GovernanceRemoveVotesConfirmViewController.swift in Sources */, @@ -29011,6 +29752,7 @@ 4C4142B4CB2DBCA1F06DC046 /* GovernanceDelegateSetupViewLayout.swift in Sources */, 6789ED94EFF73E5B47956462 /* GovernanceDelegateSetupViewFactory.swift in Sources */, 4F406ED92AAE2C77E358F49C /* GovernanceDelegateConfirmProtocols.swift in Sources */, + 0CA7CEF62CE0D35B004328F2 /* PalletAssets+Events.swift in Sources */, 0C3205C42A877172002EB914 /* EthereumReducedBlockObject.swift in Sources */, 1C9EA26D4E4BA6BAE147B374 /* GovernanceDelegateConfirmWireframe.swift in Sources */, 6FE660C98518CEB28AD9CDA3 /* GovernanceDelegateConfirmPresenter.swift in Sources */, @@ -29021,6 +29763,7 @@ 1232A714A96F937330FC0AFA /* GovernanceDelegateConfirmViewFactory.swift in Sources */, 4B1FA597B618713C75917816 /* GovernanceYourDelegationsProtocols.swift in Sources */, 15B079FA97C96327FD4A2E16 /* GovernanceYourDelegationsWireframe.swift in Sources */, + 0C0387832D0A1878000A2F24 /* AssetAmountRouteItemView.swift in Sources */, 77740BC42AD8145500E8C06F /* PercentInputView.swift in Sources */, 75249684C6F3EE4E553DABA1 /* GovernanceYourDelegationsPresenter.swift in Sources */, EB20C6B406155664B981BA94 /* GovernanceYourDelegationsInteractor.swift in Sources */, @@ -29036,6 +29779,7 @@ FF985AA0ABA33118AC02A761 /* GovernanceEditDelegationTracksViewFactory.swift in Sources */, 33431341505ABC30172D34E3 /* GovernanceRevokeDelegationTracksWireframe.swift in Sources */, C7E96E4185084C3E8F226E97 /* GovernanceRevokeDelegationTracksPresenter.swift in Sources */, + 0C2A3C9A2CDCEB8A00A0E2B3 /* SwapAssetSelectionClosure.swift in Sources */, 0CD54E4D2BCE3B89007B58E7 /* CloudBackupSecretsImporting.swift in Sources */, 3592E885646B3ED9F2717412 /* GovernanceRevokeDelegationTracksViewController.swift in Sources */, 1F205FE059A7099A1A7391EC /* GovernanceRevokeDelegationTracksViewFactory.swift in Sources */, @@ -29087,6 +29831,7 @@ 0C59E8DA2AA6085B001E11F3 /* ExternalAssetBalanceLocalSubscriptionFactory.swift in Sources */, F5AADF461ED043EAF04A50DE /* ReferendumsFiltersProtocols.swift in Sources */, A44CA3E6BB506841948AB2D1 /* ReferendumsFiltersWireframe.swift in Sources */, + 0CE94FF82D050EE300E31D0C /* AssetExchangeOperationPrototypeFactory.swift in Sources */, 0C8FDBC42CB101E700775D7F /* MercuryoCardHookFactory+ResponseIntercept.swift in Sources */, 77DF40422B7FE07D00ABDB53 /* SwitchTitleIconViewModel.swift in Sources */, 32428875321BF68F5DC47D52 /* ReferendumsFiltersPresenter.swift in Sources */, @@ -29131,6 +29876,7 @@ F332FA8C330A16C3894B6542 /* WalletConnectSessionDetailsProtocols.swift in Sources */, D0AD3C44BBFD6A9F9FDEC933 /* WalletConnectSessionDetailsWireframe.swift in Sources */, 0CFA161C2B0CE851007AF885 /* Utility+Calls.swift in Sources */, + 0C2A3C952CDC8ADB00A0E2B3 /* SwapAssetSelectionModel.swift in Sources */, 0A44D28DF4BCF56131752F35 /* WalletConnectSessionDetailsPresenter.swift in Sources */, 0CA719942B79CA01000B086E /* HydraOmnipool+Storages.swift in Sources */, F92E73C24AB577F37B35649E /* WalletConnectSessionDetailsInteractor.swift in Sources */, @@ -29177,6 +29923,7 @@ A83ED3518F2FD1C7B1C48D9E /* StartStakingInfoViewLayout.swift in Sources */, DC02C1C18DC5C03F5A006C81 /* StartStakingInfoViewFactory.swift in Sources */, 62D28F231DED3F39B2C53F1F /* StakingSetupAmountProtocols.swift in Sources */, + 0C3272972CD2821500FC1B42 /* ChainRegistry+Get.swift in Sources */, 3B1D2A0FCDDF1AAC32BFEE58 /* StakingSetupAmountWireframe.swift in Sources */, FA0E6F6A12CA290C7079AC6C /* StakingSetupAmountPresenter.swift in Sources */, 1ECC51FC47422BF1450E0575 /* StakingSetupAmountInteractor.swift in Sources */, @@ -29198,7 +29945,6 @@ DB8AD60FE397F764623A566F /* StartStakingConfirmViewLayout.swift in Sources */, 772540382AC45D22002B3FD4 /* PurchasePresentable.swift in Sources */, A428E022070EF536D4B0B5EC /* StartStakingConfirmViewFactory.swift in Sources */, - 2D95DF5B2C6F7D83009BB063 /* HydraExtrinsicAssetsCustomFeeEstimator.swift in Sources */, 774A980F2B0D08A6009146CA /* OpenScreenUrlParsingServiceFactory.swift in Sources */, AD36A601830C69DA003B3B01 /* StakingSelectPoolProtocols.swift in Sources */, 0C8FDBAB2CAE58DF00775D7F /* SwipeGovService.swift in Sources */, @@ -29226,7 +29972,6 @@ BD556407702A75D66B73A55C /* NominationPoolBondMoreSetupViewFactory.swift in Sources */, 19B35C4E96708405754B8EC5 /* NPoolsUnstakeSetupProtocols.swift in Sources */, 5103B0A6919721E7E1284829 /* NPoolsUnstakeSetupWireframe.swift in Sources */, - 0C85FF322B6D4A3500FC0014 /* AssetConversionFlowState.swift in Sources */, 2DA85A772C7FCCCE00591900 /* SwipeGovViewModelFactory.swift in Sources */, 594EA36463252924AB73475B /* NPoolsUnstakeSetupPresenter.swift in Sources */, 1E1B60AC1FBF11673A70955C /* NPoolsUnstakeSetupInteractor.swift in Sources */, @@ -29305,6 +30050,7 @@ 68E01098C46BFEA570B95ED1 /* ProxiedsUpdatePresenter.swift in Sources */, F19BA6E8A9A7FDA269CA57DE /* ProxiedsUpdateInteractor.swift in Sources */, 7136E160BD5C6A19F9C20A5B /* ProxiedsUpdateViewController.swift in Sources */, + 0C11F0282CEFD99C008D19D2 /* CountdownLoadingView.swift in Sources */, 5DCEEE88DD1E09DC8110BF0C /* ProxiedsUpdateViewLayout.swift in Sources */, 93E203248735DCCEC1BA96AF /* ProxiedsUpdateViewFactory.swift in Sources */, A8515273213D048B0F9AFD15 /* StakingSetupProxyProtocols.swift in Sources */, @@ -29312,8 +30058,10 @@ B1A789A7E8D60223F18AA407 /* StakingSetupProxyPresenter.swift in Sources */, 886E599110019177E0489999 /* StakingSetupProxyInteractor.swift in Sources */, 450BF207003E4DD3F5FE4D9B /* StakingSetupProxyViewController.swift in Sources */, + 0CBABFEB2CED37E30047F29E /* AssetExchangeQuote.swift in Sources */, 5628E716AB0D71B2DC6D3647 /* StakingSetupProxyViewLayout.swift in Sources */, FC963F2AFEA3984B2CB14A34 /* StakingSetupProxyViewFactory.swift in Sources */, + 0C3272A02CD34CB200FC1B42 /* HydraExchangeExtrinsicParamsFactory.swift in Sources */, C641C8A414F7F4E3B96B16CE /* ProxySignValidationProtocols.swift in Sources */, 1DA7B852B65ED2DC69E8A906 /* ProxySignValidationWireframe.swift in Sources */, 9AA018D4BDEEECF00018F049 /* ProxySignValidationPresenter.swift in Sources */, @@ -29330,6 +30078,7 @@ C4D962BDC211A38CA3536425 /* StakingProxyManagementProtocols.swift in Sources */, D5F1C910930BA08E5F7681B2 /* StakingProxyManagementWireframe.swift in Sources */, 5AEA54E0691BD6DC6B570ACF /* StakingProxyManagementPresenter.swift in Sources */, + 0CE94FF22D046A8700E31D0C /* AssetExchangeFeePayerMatcher.swift in Sources */, 6362476898582D2118E96447 /* StakingProxyManagementInteractor.swift in Sources */, ACF27885E3B3D01B6C3BBB4B /* StakingProxyManagementViewController.swift in Sources */, 78E67009E5A7D4564172FD0B /* StakingProxyManagementViewLayout.swift in Sources */, @@ -29374,6 +30123,12 @@ 4520C59F8C8CCB7669DDA4DF /* NotificationWalletListPresenter.swift in Sources */, AD87A679F6E61D7AA20DF60C /* NotificationWalletListInteractor.swift in Sources */, 6B1BDD1421916CCF59DF6F5F /* NotificationWalletListViewController.swift in Sources */, + 0CA8AD622CE08FEE00ED9746 /* BlockEventsQueryFactory.swift in Sources */, + 0CA8AD632CE08FEE00ED9746 /* ExtrinsicStatusService.swift in Sources */, + 0CA8AD642CE08FEE00ED9746 /* SubstrateExtrinsicEvents.swift in Sources */, + 0CA8AD652CE08FEE00ED9746 /* ExtrinsicSubmissionMonitor.swift in Sources */, + 0CA8AD662CE08FEE00ED9746 /* ExtrinsicEventsMatching.swift in Sources */, + 0CA8AD672CE08FEE00ED9746 /* SubstrateExtrinsicStatus.swift in Sources */, 0566845B5E1D65E3632C54CE /* NotificationWalletListViewLayout.swift in Sources */, C802F43E2481C2A091F86CFF /* NotificationWalletListViewFactory.swift in Sources */, A8B288AA4F50470D691C72CE /* OnboardingWalletReadyProtocols.swift in Sources */, @@ -29383,21 +30138,25 @@ 770B26D96689F1A253B1348D /* OnboardingWalletReadyViewController.swift in Sources */, EBDC7FDF98C20154FA7ED595 /* OnboardingWalletReadyViewLayout.swift in Sources */, 4AA446B2764D922E91641C03 /* OnboardingWalletReadyViewFactory.swift in Sources */, + 0C2DA8A62CC265F0001F79C8 /* AssetsExchangeGraphProvider.swift in Sources */, DA46697F2BA60F98B01AE6FA /* CloudBackupCreateProtocols.swift in Sources */, 2F817BEA33B48EDDCBA8CB53 /* CloudBackupCreateWireframe.swift in Sources */, A10BC98A621E20E2DF59D747 /* CloudBackupCreateInteractor.swift in Sources */, 979B8AFDD578C8B589BE6AC8 /* CloudBackupCreateViewController.swift in Sources */, DB099FF1914E6A197E5F1D95 /* CloudBackupCreateViewLayout.swift in Sources */, DD7554F36497B3CFA4D4E6AA /* CloudBackupCreateViewFactory.swift in Sources */, + 0C6108302CD5ED1400909928 /* ExtrinsicCustomFeeEstimatingFactoryProtocol.swift in Sources */, 47F3A61D12849B9A408D74D7 /* WalletImportOptionsProtocols.swift in Sources */, 5EA3B42C38C29448DCE093A6 /* OnboardingImportOptionsWireframe.swift in Sources */, 292B3DBF9B34B2DBD9763655 /* WalletImportOptionsPresenter.swift in Sources */, B823DEE94E0979161EE96F59 /* OnboardingImportOptionsInteractor.swift in Sources */, 6AC4087E34E90E31DACE83EE /* WalletImportOptionsViewController.swift in Sources */, 625027A97EC1C01FEF2AB6BE /* WalletImportOptionsViewLayout.swift in Sources */, + 0CE94FF62D04A3C100E31D0C /* AssetsExchangePathCostEstimator.swift in Sources */, 2D7E4A152C4028E300455509 /* CustomNetworkSetupFinishStrategy.swift in Sources */, FD9855AC31967F470C7F9679 /* WalletImportOptionsViewFactory.swift in Sources */, DB1FB213FCB39BE3A5780D82 /* ImportCloudPasswordProtocols.swift in Sources */, + 0C0387512D066474000A2F24 /* AssetExchageUsdtConverter.swift in Sources */, F652C0081A957CCE6266D204 /* ImportCloudPasswordWireframe.swift in Sources */, 6D55D47510FAA4D33E1F1D7C /* ImportCloudPasswordPresenter.swift in Sources */, 51B9CE9BD264AB3B83BFB3D0 /* ImportCloudPasswordInteractor.swift in Sources */, @@ -29474,7 +30233,9 @@ B7AA65C6A2A013A46B99B9D2 /* NetworksListViewController.swift in Sources */, A09631A9C63D7099075D3C8F /* NetworksListViewLayout.swift in Sources */, 782C073E35DE4CEA38B610E0 /* NetworksListViewFactory.swift in Sources */, + 0C19CCA52CC6EDAE007F8ED8 /* AssetsExchangeBaseProvider.swift in Sources */, DCC44FA06F0EFD6D41277098 /* NetworkDetailsProtocols.swift in Sources */, + 0C2DA8A02CC21B09001F79C8 /* IndexedChainModels.swift in Sources */, F8617BB83CE091445E5087AD /* NetworkDetailsWireframe.swift in Sources */, 5A606743183D76BF10DDFACB /* NetworkDetailsPresenter.swift in Sources */, 9E757C72D330AE8ECC00B3F0 /* NetworkDetailsInteractor.swift in Sources */, @@ -29492,12 +30253,14 @@ A640F0905D22C22F946278C6 /* NetworkManageNodeViewController.swift in Sources */, A76FAB4B859306CE12958CDF /* NetworkManageNodeViewLayout.swift in Sources */, 4CD9B187647A37B4F6343DD7 /* NetworkManageNodeViewFactory.swift in Sources */, + 0CE94FF02D03AF9800E31D0C /* SwapPreferredFeeAssetModel.swift in Sources */, EECFBA0B881DBD2AE2BC73B7 /* CustomNetworkProtocols.swift in Sources */, 440436412294F3569F426552 /* CustomNetworkWireframe.swift in Sources */, F08729B3361332AC9E017BED /* CustomNetworkViewController.swift in Sources */, A83FCCEF54DDA2C8CDA98AF5 /* CustomNetworkViewLayout.swift in Sources */, D71D861DF5798FF122845B86 /* CustomNetworkViewFactory.swift in Sources */, BDC90BC133D963F0E7360386 /* KnownNetworksListProtocols.swift in Sources */, + 0C6108472CD882F900909928 /* SwapFeeCurrencyState.swift in Sources */, FCCC66B228B51655D109C006 /* KnownNetworksListWireframe.swift in Sources */, D75DFCEA7BCB9615075EF82B /* KnownNetworksListPresenter.swift in Sources */, 433CA9D0C219954508DCBCCD /* KnownNetworksListInteractor.swift in Sources */, @@ -29523,6 +30286,7 @@ 49E9D296222D2E8418E0A4EA /* SwipeGovProtocols.swift in Sources */, 56C840097B6B2E3D7133EE21 /* SwipeGovWireframe.swift in Sources */, 59E3304ED1BBA3CB2699C957 /* SwipeGovPresenter.swift in Sources */, + 0C12BC372CEA7DCB00AB919D /* AssetExchangePrice.swift in Sources */, 7C54DE318B0675170102B7AD /* SwipeGovViewController.swift in Sources */, 735E83BE57E30FBFD1382474 /* SwipeGovViewLayout.swift in Sources */, EA5757C027BC51F64C28A4D3 /* SwipeGovViewFactory.swift in Sources */, @@ -29566,6 +30330,23 @@ 14003DEC47A69E473B97B3E5 /* AppearanceSettingsViewController.swift in Sources */, 36EA1F22A0E7DDD3C987CF98 /* AppearanceSettingsViewLayout.swift in Sources */, BC927FA9FDA6F1AC16E0AAAE /* AppearanceSettingsViewFactory.swift in Sources */, + 076E4DC5984B431018CB9F65 /* SwapExecutionProtocols.swift in Sources */, + 7BF57D772C522E7728585166 /* SwapExecutionWireframe.swift in Sources */, + 98815A40D614BCB2BEA01791 /* SwapExecutionPresenter.swift in Sources */, + D655ED75A8D7BD4380FCF3A8 /* SwapExecutionInteractor.swift in Sources */, + D1D2709811B89D89F8A08FAF /* SwapExecutionViewController.swift in Sources */, + 8BB2AB5B4E97FAE0D27A4B60 /* SwapExecutionViewLayout.swift in Sources */, + D010C2D055F79587881692F3 /* SwapExecutionViewFactory.swift in Sources */, + 1116E062DCFC5E1353B9B4F8 /* SwapRouteDetailsProtocols.swift in Sources */, + F007ED524D9B187C5159C5DA /* SwapRouteDetailsPresenter.swift in Sources */, + 04D6122369889C927BB3D13F /* SwapRouteDetailsViewController.swift in Sources */, + 6962A0D96A2F40C7D50FFFA7 /* SwapRouteDetailsViewLayout.swift in Sources */, + D381A3467D7C07331DBEB68F /* SwapRouteDetailsViewFactory.swift in Sources */, + 68CF0931493D315D0B315648 /* SwapFeeDetailsProtocols.swift in Sources */, + EB1102249D1DE711E723D7BA /* SwapFeeDetailsPresenter.swift in Sources */, + 3C219894A8E0598486B2D285 /* SwapFeeDetailsViewController.swift in Sources */, + 8364E4802CC617E523EB87A7 /* SwapFeeDetailsViewLayout.swift in Sources */, + AFFAC5B4085560356A9FDC7C /* SwapFeeDetailsViewFactory.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -29710,12 +30491,10 @@ 84B7C731289BFA79001A3566 /* ValidatorInfoTests.swift in Sources */, 0C41D2102BF7814A000950EE /* MockCloudBackupServiceFactory.swift in Sources */, 845B822326EFDF3F00D25C72 /* SelectedAccountSettingsTests.swift in Sources */, - 84B7C71C289BFA79001A3566 /* DAppTxDetailsTests.swift in Sources */, 0C7104692C2966C000487E64 /* LedgerConversionTests.swift in Sources */, 84B66A1426FDF29A0038B963 /* WalletLocalSubscriptionFactoryStub.swift in Sources */, 887248082924F54900B0D2CC /* URL+Matchable.swift in Sources */, 8440F4A5295AB4E300CAFBF9 /* SecurityLayerTests.swift in Sources */, - 771901902AE2424B00D9C918 /* SwapsValidationTests.swift in Sources */, 84B7C718289BFA79001A3566 /* DAppOperationConfirmTests.swift in Sources */, 844D229E25EE79EA00C022F7 /* ExtrinsicServiceStub.swift in Sources */, 84F4A910255001D2000CF0A3 /* KeystoreExportWrapperTests.swift in Sources */, @@ -29744,7 +30523,6 @@ 84E25BF427E8FEFD00290BF1 /* RewardDataSourceTests.swift in Sources */, 84AA004326C5DFD800BCB4DC /* RuntimeSyncServiceTests.swift in Sources */, 8413B27A29507F3A00F8E2E4 /* DataProviderRepositoryStub.swift in Sources */, - 84B7C748289BFA79001A3566 /* WalletListTests.swift in Sources */, 84B7C720289BFA79001A3566 /* ReferralCrowdloanTests.swift in Sources */, F4897BB126AED13D0075F291 /* EraCountdownOperationFactoryStub.swift in Sources */, ); diff --git a/novawallet.xcodeproj/xcshareddata/xcbaselines/8438E1CE24BFAAD2001BDB13.xcbaseline/0377CEA4-94EB-4077-B7D5-C214E105C89F.plist b/novawallet.xcodeproj/xcshareddata/xcbaselines/8438E1CE24BFAAD2001BDB13.xcbaseline/0377CEA4-94EB-4077-B7D5-C214E105C89F.plist new file mode 100644 index 0000000000..812d748d62 --- /dev/null +++ b/novawallet.xcodeproj/xcshareddata/xcbaselines/8438E1CE24BFAAD2001BDB13.xcbaseline/0377CEA4-94EB-4077-B7D5-C214E105C89F.plist @@ -0,0 +1,42 @@ + + + + + classNames + + AssetsExchangeTests + + testFindAvailablePairs() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.926857 + baselineIntegrationDisplayName + Local Baseline + + + testMeasureRouteSearch() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 1.923227 + baselineIntegrationDisplayName + Local Baseline + + + testNoRoute() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.001498 + baselineIntegrationDisplayName + Local Baseline + + + + + + diff --git a/novawallet.xcodeproj/xcshareddata/xcbaselines/8438E1CE24BFAAD2001BDB13.xcbaseline/0CBA0634-E260-422A-A54D-0326AA5D9BBC.plist b/novawallet.xcodeproj/xcshareddata/xcbaselines/8438E1CE24BFAAD2001BDB13.xcbaseline/0CBA0634-E260-422A-A54D-0326AA5D9BBC.plist index 23e5cea938..0d878f22c3 100644 --- a/novawallet.xcodeproj/xcshareddata/xcbaselines/8438E1CE24BFAAD2001BDB13.xcbaseline/0CBA0634-E260-422A-A54D-0326AA5D9BBC.plist +++ b/novawallet.xcodeproj/xcshareddata/xcbaselines/8438E1CE24BFAAD2001BDB13.xcbaseline/0CBA0634-E260-422A-A54D-0326AA5D9BBC.plist @@ -11,7 +11,7 @@ com.apple.XCTPerformanceMetric_WallClockTime baselineAverage - 1.0603 + 1.060300 baselineIntegrationDisplayName Local Baseline @@ -31,7 +31,7 @@ com.apple.XCTPerformanceMetric_WallClockTime baselineAverage - 3.92 + 3.920000 baselineIntegrationDisplayName Local Baseline @@ -41,7 +41,7 @@ com.apple.XCTPerformanceMetric_WallClockTime baselineAverage - 7.98 + 7.980000 baselineIntegrationDisplayName Local Baseline @@ -54,7 +54,7 @@ com.apple.XCTPerformanceMetric_WallClockTime baselineAverage - 6.9026 + 6.902600 baselineIntegrationDisplayName Local Baseline @@ -67,7 +67,7 @@ com.apple.XCTPerformanceMetric_WallClockTime baselineAverage - 0.65465 + 0.654650 baselineIntegrationDisplayName Local Baseline diff --git a/novawallet.xcodeproj/xcshareddata/xcbaselines/8438E1CE24BFAAD2001BDB13.xcbaseline/24255ADB-1167-4055-A89C-15D155BEDA4F.plist b/novawallet.xcodeproj/xcshareddata/xcbaselines/8438E1CE24BFAAD2001BDB13.xcbaseline/24255ADB-1167-4055-A89C-15D155BEDA4F.plist index d43877cbbb..19e00953b5 100644 --- a/novawallet.xcodeproj/xcshareddata/xcbaselines/8438E1CE24BFAAD2001BDB13.xcbaseline/24255ADB-1167-4055-A89C-15D155BEDA4F.plist +++ b/novawallet.xcodeproj/xcshareddata/xcbaselines/8438E1CE24BFAAD2001BDB13.xcbaseline/24255ADB-1167-4055-A89C-15D155BEDA4F.plist @@ -11,7 +11,7 @@ com.apple.XCTPerformanceMetric_WallClockTime baselineAverage - 4.0435 + 4.043500 baselineIntegrationDisplayName Local Baseline diff --git a/novawallet.xcodeproj/xcshareddata/xcbaselines/8438E1CE24BFAAD2001BDB13.xcbaseline/Info.plist b/novawallet.xcodeproj/xcshareddata/xcbaselines/8438E1CE24BFAAD2001BDB13.xcbaseline/Info.plist index e419722a9f..916f90c064 100644 --- a/novawallet.xcodeproj/xcshareddata/xcbaselines/8438E1CE24BFAAD2001BDB13.xcbaseline/Info.plist +++ b/novawallet.xcodeproj/xcshareddata/xcbaselines/8438E1CE24BFAAD2001BDB13.xcbaseline/Info.plist @@ -4,6 +4,37 @@ runDestinationsByUUID + 0377CEA4-94EB-4077-B7D5-C214E105C89F + + localComputer + + busSpeedInMHz + 0 + cpuCount + 1 + cpuKind + Apple M2 Max + cpuSpeedInMHz + 0 + logicalCPUCoresPerPackage + 12 + modelCode + Mac14,6 + physicalCPUCoresPerPackage + 12 + platformIdentifier + com.apple.platform.macosx + + targetArchitecture + x86_64 + targetDevice + + modelCode + iPhone14,6 + platformIdentifier + com.apple.platform.iphonesimulator + + 0CBA0634-E260-422A-A54D-0326AA5D9BBC localComputer diff --git a/novawallet/Assets.xcassets/colors/background/colorRouteNumberBackground.colorset/Contents.json b/novawallet/Assets.xcassets/colors/background/colorRouteNumberBackground.colorset/Contents.json new file mode 100644 index 0000000000..c14dc7cc8b --- /dev/null +++ b/novawallet/Assets.xcassets/colors/background/colorRouteNumberBackground.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x3B", + "green" : "0x2D", + "red" : "0x2B" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/countdownTimerImage.imageset/Contents.json b/novawallet/Assets.xcassets/countdownTimerImage.imageset/Contents.json new file mode 100644 index 0000000000..344f0ad74c --- /dev/null +++ b/novawallet/Assets.xcassets/countdownTimerImage.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "countdownTimerImage.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/countdownTimerImage.imageset/countdownTimerImage.pdf b/novawallet/Assets.xcassets/countdownTimerImage.imageset/countdownTimerImage.pdf new file mode 100644 index 0000000000..8d837a0505 Binary files /dev/null and b/novawallet/Assets.xcassets/countdownTimerImage.imageset/countdownTimerImage.pdf differ diff --git a/novawallet/Assets.xcassets/iconSwapExecutionComplete.imageset/Contents.json b/novawallet/Assets.xcassets/iconSwapExecutionComplete.imageset/Contents.json new file mode 100644 index 0000000000..556fefbd32 --- /dev/null +++ b/novawallet/Assets.xcassets/iconSwapExecutionComplete.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "iconSwapExecutionComplete.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconSwapExecutionComplete.imageset/iconSwapExecutionComplete.pdf b/novawallet/Assets.xcassets/iconSwapExecutionComplete.imageset/iconSwapExecutionComplete.pdf new file mode 100644 index 0000000000..7935683725 Binary files /dev/null and b/novawallet/Assets.xcassets/iconSwapExecutionComplete.imageset/iconSwapExecutionComplete.pdf differ diff --git a/novawallet/Assets.xcassets/iconSwapExecutionFailed.imageset/Contents.json b/novawallet/Assets.xcassets/iconSwapExecutionFailed.imageset/Contents.json new file mode 100644 index 0000000000..409f2a2e75 --- /dev/null +++ b/novawallet/Assets.xcassets/iconSwapExecutionFailed.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "iconSwapExecutionFailed.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconSwapExecutionFailed.imageset/iconSwapExecutionFailed.pdf b/novawallet/Assets.xcassets/iconSwapExecutionFailed.imageset/iconSwapExecutionFailed.pdf new file mode 100644 index 0000000000..97192e2326 Binary files /dev/null and b/novawallet/Assets.xcassets/iconSwapExecutionFailed.imageset/iconSwapExecutionFailed.pdf differ diff --git a/novawallet/Common/Crypto/SigningWrapperFactory+Async.swift b/novawallet/Common/Crypto/SigningWrapperFactory+Async.swift new file mode 100644 index 0000000000..4d1570b408 --- /dev/null +++ b/novawallet/Common/Crypto/SigningWrapperFactory+Async.swift @@ -0,0 +1,22 @@ +import Foundation +import Operation_iOS + +extension SigningWrapperFactoryProtocol { + func createSigningOperationWrapper( + dependingOn accountClosure: @escaping () throws -> MetaChainAccountResponse, + operationQueue: OperationQueue + ) -> CompoundOperationWrapper { + OperationCombiningService.compoundNonOptionalWrapper( + operationManager: OperationManager(operationQueue: operationQueue) + ) { + let account = try accountClosure() + + let signingWrapper = self.createSigningWrapper( + for: account.metaId, + accountResponse: account.chainAccount + ) + + return CompoundOperationWrapper.createWithResult(signingWrapper) + } + } +} diff --git a/novawallet/Common/DataProvider/CrowdloanLocalSubscriptionFactory.swift b/novawallet/Common/DataProvider/CrowdloanLocalSubscriptionFactory.swift index 23f418ebd8..c117bf206d 100644 --- a/novawallet/Common/DataProvider/CrowdloanLocalSubscriptionFactory.swift +++ b/novawallet/Common/DataProvider/CrowdloanLocalSubscriptionFactory.swift @@ -13,7 +13,7 @@ protocol CrowdloanLocalSubscriptionFactoryProtocol { final class CrowdloanLocalSubscriptionFactory: SubstrateLocalSubscriptionFactory, CrowdloanLocalSubscriptionFactoryProtocol { func getBlockNumberProvider(for chainId: ChainModel.Id) throws -> AnyDataProvider { - let codingPath = StorageCodingPath.blockNumber + let codingPath = SystemPallet.blockNumberPath let localKey = try LocalStorageKeyFactory().createFromStoragePath(codingPath, chainId: chainId) return try getDataProvider( diff --git a/novawallet/Common/DataProvider/GeneralStorageSubscriptionFactory.swift b/novawallet/Common/DataProvider/GeneralStorageSubscriptionFactory.swift index 6f16196da5..0f214afd67 100644 --- a/novawallet/Common/DataProvider/GeneralStorageSubscriptionFactory.swift +++ b/novawallet/Common/DataProvider/GeneralStorageSubscriptionFactory.swift @@ -14,7 +14,7 @@ protocol GeneralStorageSubscriptionFactoryProtocol { final class GeneralStorageSubscriptionFactory: SubstrateLocalSubscriptionFactory, GeneralStorageSubscriptionFactoryProtocol { func getBlockNumberProvider(for chainId: ChainModel.Id) throws -> AnyDataProvider { - let codingPath = StorageCodingPath.blockNumber + let codingPath = SystemPallet.blockNumberPath let localKey = try LocalStorageKeyFactory().createFromStoragePath(codingPath, chainId: chainId) return try getDataProvider( @@ -29,7 +29,7 @@ final class GeneralStorageSubscriptionFactory: SubstrateLocalSubscriptionFactory for accountId: AccountId, chainId: ChainModel.Id ) throws -> AnyDataProvider { - let codingPath = StorageCodingPath.account + let codingPath = SystemPallet.accountPath let localKey = try LocalStorageKeyFactory().createFromStoragePath( codingPath, diff --git a/novawallet/Common/Extension/Error/Optional+Result.swift b/novawallet/Common/Extension/Error/Optional+Result.swift index 82bb3b12d8..c85b9b1c17 100644 --- a/novawallet/Common/Extension/Error/Optional+Result.swift +++ b/novawallet/Common/Extension/Error/Optional+Result.swift @@ -9,4 +9,12 @@ extension Optional { return true } } + + func mapOrThrow(_ error: Error) throws -> Wrapped { + guard let value = self else { + throw error + } + + return value + } } diff --git a/novawallet/Common/Extension/Foundation/BigUInt+Operation.swift b/novawallet/Common/Extension/Foundation/BigUInt+Operation.swift index 1d75d6a570..bd94aaf540 100644 --- a/novawallet/Common/Extension/Foundation/BigUInt+Operation.swift +++ b/novawallet/Common/Extension/Foundation/BigUInt+Operation.swift @@ -5,4 +5,10 @@ extension BigUInt { func subtractOrZero(_ value: BigUInt) -> BigUInt { self > value ? self - value : 0 } + + func divideByRoundingUp(_ value: BigUInt) -> BigUInt { + let (quotient, reminder) = quotientAndRemainder(dividingBy: value) + + return reminder > 0 ? quotient + 1 : quotient + } } diff --git a/novawallet/Common/Extension/UIKit/RoundedButton+Set.swift b/novawallet/Common/Extension/UIKit/RoundedButton+Set.swift new file mode 100644 index 0000000000..37df5bbfca --- /dev/null +++ b/novawallet/Common/Extension/UIKit/RoundedButton+Set.swift @@ -0,0 +1,9 @@ +import Foundation +import SoraUI + +extension RoundedButton { + func setTitle(_ title: String?) { + imageWithTitleView?.title = title + invalidateLayout() + } +} diff --git a/novawallet/Common/Extension/UIKit/RoundedView+Styles.swift b/novawallet/Common/Extension/UIKit/RoundedView+Styles.swift index 3972d57798..9cef3c4b06 100644 --- a/novawallet/Common/Extension/UIKit/RoundedView+Styles.swift +++ b/novawallet/Common/Extension/UIKit/RoundedView+Styles.swift @@ -48,6 +48,11 @@ extension RoundedView { highlightedFillColor = R.color.colorCellBackgroundPressed()! } + func applyErrorBlockBackgroundStyle() { + let color = R.color.colorErrorBlockBackground()! + applyFilledBackgroundStyle(for: color, highlighted: color) + } + func applyFilledBackgroundStyle() { shadowOpacity = 0.0 strokeWidth = 0.0 @@ -58,4 +63,13 @@ extension RoundedView { fillColor = .clear highlightedFillColor = .clear } + + func applyFilledBackgroundStyle(for color: UIColor, highlighted: UIColor) { + shadowOpacity = 0.0 + strokeWidth = 0.0 + strokeColor = .clear + highlightedStrokeColor = .clear + fillColor = color + highlightedFillColor = highlighted + } } diff --git a/novawallet/Common/Extension/UIKit/Style/UILabel+Style.swift b/novawallet/Common/Extension/UIKit/Style/UILabel+Style.swift index 160c121009..442ff1e4b2 100644 --- a/novawallet/Common/Extension/UIKit/Style/UILabel+Style.swift +++ b/novawallet/Common/Extension/UIKit/Style/UILabel+Style.swift @@ -51,11 +51,21 @@ extension UILabel.Style { font: .semiBoldSubheadline ) + static let semiboldBodyButtonAccent = UILabel.Style( + textColor: R.color.colorButtonTextAccent(), + font: .semiBoldBody + ) + static let semiboldBodyPrimary = UILabel.Style( textColor: R.color.colorTextPrimary(), font: .semiBoldBody ) + static let semiboldBodySecondary = UILabel.Style( + textColor: R.color.colorTextSecondary(), + font: .semiBoldBody + ) + static let semiboldChip = UILabel.Style( textColor: R.color.colorChipText(), font: .semiBoldFootnote @@ -201,6 +211,21 @@ extension UILabel.Style { font: .semiBoldTitle3 ) + static let boldTitle1Primary = UILabel.Style( + textColor: R.color.colorTextPrimary()!, + font: .boldTitle1 + ) + + static let boldTitle1Positive = UILabel.Style( + textColor: R.color.colorTextPositive()!, + font: .boldTitle1 + ) + + static let boldTitle1Negative = UILabel.Style( + textColor: R.color.colorTextNegative()!, + font: .boldTitle1 + ) + static let boldTitle2Primary = UILabel.Style( textColor: R.color.colorTextPrimary()!, font: .boldTitle2 diff --git a/novawallet/Common/Graphs/GraphEdgeFiltering.swift b/novawallet/Common/Graphs/GraphEdgeFiltering.swift new file mode 100644 index 0000000000..67db7c3a13 --- /dev/null +++ b/novawallet/Common/Graphs/GraphEdgeFiltering.swift @@ -0,0 +1,33 @@ +import Foundation + +protocol GraphEdgeFiltering { + associatedtype Edge + + func shouldVisit(edge: Edge, predecessor: Edge?) -> Bool +} + +class AnyGraphEdgeFilter { + typealias Edge = E + + private let shouldVisitClosure: (Edge, Edge?) -> Bool + + init(filter: F) where F.Edge == E { + shouldVisitClosure = filter.shouldVisit + } + + init(closure: @escaping (Edge, Edge?) -> Bool) { + shouldVisitClosure = closure + } +} + +extension AnyGraphEdgeFilter: GraphEdgeFiltering { + func shouldVisit(edge: Edge, predecessor: Edge?) -> Bool { + shouldVisitClosure(edge, predecessor) + } +} + +extension AnyGraphEdgeFilter { + static func allEdges() -> AnyGraphEdgeFilter { + AnyGraphEdgeFilter { _, _ in true } + } +} diff --git a/novawallet/Common/Graphs/GraphModel+BFS.swift b/novawallet/Common/Graphs/GraphModel+BFS.swift new file mode 100644 index 0000000000..bc4c15af06 --- /dev/null +++ b/novawallet/Common/Graphs/GraphModel+BFS.swift @@ -0,0 +1,39 @@ +import Foundation + +extension GraphModel { + func calculateReachableNodes( + for node: N, + filter: AnyGraphEdgeFilter + ) -> Set { + var queue = [E]() + var visitedEdges: Set = Set() + + connections[node]?.forEach { edge in + if filter.shouldVisit(edge: edge, predecessor: nil) { + visitedEdges.insert(edge) + queue.append(edge) + } + } + + var result: Set = [] + + while !queue.isEmpty { + let currentEdge = queue.removeFirst() + + if node != currentEdge.destination { + result.insert(currentEdge.destination) + } + + let neighbors = connections[currentEdge.destination] ?? [] + + for neighbor in neighbors where !visitedEdges.contains(neighbor) { + if filter.shouldVisit(edge: neighbor, predecessor: currentEdge) { + visitedEdges.insert(neighbor) + queue.append(neighbor) + } + } + } + + return result + } +} diff --git a/novawallet/Common/Graphs/GraphModel+Dijkstra.swift b/novawallet/Common/Graphs/GraphModel+Dijkstra.swift index 05642eb1a1..f7ea5cda74 100644 --- a/novawallet/Common/Graphs/GraphModel+Dijkstra.swift +++ b/novawallet/Common/Graphs/GraphModel+Dijkstra.swift @@ -1,39 +1,103 @@ import Foundation -extension GraphModel { - func calculateShortestPath(from nodeStart: N, nodeEnd: N, topN: Int) -> [[E]] { - var queue = PriorityQueue<(cost: Int, path: [E])>(sort: { $0.cost < $1.cost }) - - connections[nodeStart]?.forEach { - queue.push((cost: 1, path: [$0])) +extension GraphModel where E: GraphWeightableEdgeProtocol { + func calculateShortestPath( + from nodeStart: N, + nodeEnd: N, + topN: Int, + filter: AnyGraphEdgeFilter + ) -> [[E]] { + guard topN > 1 else { + if let path = calculateShortestPath(from: nodeStart, nodeEnd: nodeEnd, filter: filter) { + return [path] + } else { + return [] + } } + var queue = PriorityQueue<(cost: Int, path: [E])>(sort: { $0.cost < $1.cost }) var result: [[E]] = [] - var visitedPaths: Set<[E]> = Set() + var counter: [N: Int] = [:] + + connections[nodeStart]?.forEach { edge in + if filter.shouldVisit(edge: edge, predecessor: nil) { + queue.push((cost: edge.weight, path: [edge])) + } + } while !queue.isEmpty, result.count < topN { guard let (cost, path) = queue.pop() else { break } let currentEdge = path.last! + let newCounter = (counter[currentEdge.destination] ?? 0) + 1 + counter[currentEdge.destination] = newCounter + if currentEdge.destination == nodeEnd { result.append(path) - continue } let neighbors = connections[currentEdge.destination] ?? [] - for neighbor in neighbors { - var newPath = path - newPath.append(neighbor) + if newCounter <= topN { + for neighbor in neighbors { + if filter.shouldVisit(edge: neighbor, predecessor: currentEdge) { + var newPath = path + newPath.append(neighbor) - if !visitedPaths.contains(newPath) { - visitedPaths.insert(newPath) - queue.push((cost: cost + 1, path: newPath)) + queue.push((cost: cost + neighbor.weight, path: newPath)) + } } } } return result } + + func calculateShortestPath( + from nodeStart: N, + nodeEnd: N, + filter: AnyGraphEdgeFilter + ) -> [E]? { + var queue = PriorityQueue<(cost: Int, edge: E)>(sort: { $0.cost < $1.cost }) + var dist: [N: Int] = [nodeStart: 0] + var prev: [N: E] = [:] + + connections[nodeStart]?.forEach { edge in + if filter.shouldVisit(edge: edge, predecessor: nil) { + prev[edge.destination] = edge + dist[edge.destination] = edge.weight + queue.push((cost: edge.weight, edge: edge)) + } + } + + while !queue.isEmpty { + guard let (cost, edge) = queue.pop() else { break } + + let neighbors = connections[edge.destination] ?? [] + + for neighbor in neighbors { + let neighborDist = dist[neighbor.destination] ?? Int.max + let newCost = cost + neighbor.weight + + if newCost < neighborDist, filter.shouldVisit(edge: neighbor, predecessor: edge) { + dist[neighbor.destination] = newCost + prev[neighbor.destination] = neighbor + queue.push((cost: newCost, edge: neighbor)) + } + } + } + + guard let currentEdge = prev[nodeEnd] else { + return nil + } + + var result: [E] = [currentEdge] + + while let prevEdge = result.last, let edge = prev[prevEdge.origin] { + result.append(edge) + } + + return Array(result.reversed()) + } } diff --git a/novawallet/Common/Graphs/GraphModel.swift b/novawallet/Common/Graphs/GraphModel.swift index a16d041cfc..f7843566a6 100644 --- a/novawallet/Common/Graphs/GraphModel.swift +++ b/novawallet/Common/Graphs/GraphModel.swift @@ -4,37 +4,22 @@ import BigInt protocol GraphEdgeProtocol { associatedtype Node + var origin: Node { get } var destination: Node { get } } struct SimpleEdge: GraphEdgeProtocol, Hashable { typealias Node = N + let origin: N let destination: N } struct GraphModel where N == E.Node { let connections: [N: Set] - - private func reachableHandle(node: N, visited: Set) -> Set { - guard let edges = connections[node] else { - return visited - } - - let siblings = Set(edges.map(\.destination)) - let notVisited = siblings.subtracting(visited) - - return notVisited.reduce(visited.union(notVisited)) { reachableHandle(node: $1, visited: $0) } - } } extension GraphModel { - func reachableNodes(for node: N) -> Set { - let result = reachableHandle(node: node, visited: [node]) - - return result.subtracting([node]) - } - func merging(with other: GraphModel) -> GraphModel { let newConnections = other.connections.reduce(into: connections) { accum, keyValue in accum[keyValue.key] = (accum[keyValue.key] ?? []).union(keyValue.value) @@ -49,11 +34,22 @@ enum GraphModelFactory { _ connections: [[N: Set]] ) -> GraphModel> { connections.reduce(GraphModel>(connections: [:])) { graph, subcon in - let edges = subcon.mapValues { siblings in - Set(siblings.map { SimpleEdge(destination: $0) }) + let edges = subcon.reduce(into: [N: Set>]()) { accum, siblings in + let origin = siblings.key + let destinations = siblings.value + + accum[origin] = Set(destinations.map { SimpleEdge(origin: origin, destination: $0) }) } return graph.merging(with: GraphModel>(connections: edges)) } } + + static func createFromEdges(_ edges: [E]) -> GraphModel { + let connections = edges.reduce(into: [E.Node: Set]()) { accum, edge in + accum[edge.origin] = (accum[edge.origin] ?? []).union([edge]) + } + + return GraphModel(connections: connections) + } } diff --git a/novawallet/Common/Graphs/WeightableEdge.swift b/novawallet/Common/Graphs/WeightableEdge.swift new file mode 100644 index 0000000000..1a63bfb2c0 --- /dev/null +++ b/novawallet/Common/Graphs/WeightableEdge.swift @@ -0,0 +1,7 @@ +import Foundation + +protocol Weightable { + var weight: Int { get } +} + +typealias GraphWeightableEdgeProtocol = GraphEdgeProtocol & Weightable diff --git a/novawallet/Common/Helpers/ChainAccountFetching.swift b/novawallet/Common/Helpers/ChainAccountFetching.swift index 11b8d4111d..82e2deddce 100644 --- a/novawallet/Common/Helpers/ChainAccountFetching.swift +++ b/novawallet/Common/Helpers/ChainAccountFetching.swift @@ -163,6 +163,10 @@ extension MetaAccountModel { } } + func hasAccount(in chain: ChainModel) -> Bool { + fetch(for: chain.accountRequest()) != nil + } + // Note that this query might return an account in another chain if it can't be found for provided chain func fetchByAccountId(_ accountId: AccountId, request: ChainAccountRequest) -> ChainAccountResponse? { if @@ -325,7 +329,7 @@ extension MetaAccountModel { func address(for chainAsset: ChainAsset) throws -> AccountAddress? { let request = chainAsset.chain.accountRequest() - return try fetch(for: request)?.toAddress() + return fetch(for: request)?.toAddress() } } diff --git a/novawallet/Common/Helpers/Debouncer.swift b/novawallet/Common/Helpers/Debouncer.swift new file mode 100644 index 0000000000..1ffa796544 --- /dev/null +++ b/novawallet/Common/Helpers/Debouncer.swift @@ -0,0 +1,29 @@ +import Foundation + +class Debouncer { + private let delay: TimeInterval + private var workItem: DispatchWorkItem? + private let queue: DispatchQueue + + init(delay: TimeInterval, queue: DispatchQueue = DispatchQueue.main) { + self.delay = delay + self.queue = queue + } + + func debounce(action: @escaping (() -> Void)) { + workItem?.cancel() + workItem = DispatchWorkItem { [weak self] in + action() + self?.workItem = nil + } + + if let workItem = workItem { + queue.asyncAfter(deadline: .now() + delay, execute: workItem) + } + } + + func cancel() { + workItem?.cancel() + workItem = nil + } +} diff --git a/novawallet/Common/Helpers/MaxCounter.swift b/novawallet/Common/Helpers/MaxCounter.swift index 3e0d038052..2ee545f4a7 100644 --- a/novawallet/Common/Helpers/MaxCounter.swift +++ b/novawallet/Common/Helpers/MaxCounter.swift @@ -26,6 +26,6 @@ struct MaxCounter { extension MaxCounter { static func feeCorrection() -> MaxCounter { - MaxCounter(maxCount: 3) + MaxCounter(maxCount: 2) } } diff --git a/novawallet/Common/Helpers/WeakWrapper.swift b/novawallet/Common/Helpers/WeakWrapper.swift index 89d517a988..5cfa6e9638 100644 --- a/novawallet/Common/Helpers/WeakWrapper.swift +++ b/novawallet/Common/Helpers/WeakWrapper.swift @@ -13,3 +13,21 @@ extension Array where Element == WeakWrapper { self = filter { $0.target != nil } } } + +final class WeakObserver { + weak var target: AnyObject? + let notificationQueue: DispatchQueue + let closure: () -> Void + + init(target: AnyObject, notificationQueue: DispatchQueue, closure: @escaping () -> Void) { + self.target = target + self.notificationQueue = notificationQueue + self.closure = closure + } +} + +extension Array where Element == WeakObserver { + mutating func clearEmptyItems() { + self = filter { $0.target != nil } + } +} diff --git a/novawallet/Common/Model/BigRational.swift b/novawallet/Common/Model/BigRational.swift index fe03f31f9f..fe4e96855a 100644 --- a/novawallet/Common/Model/BigRational.swift +++ b/novawallet/Common/Model/BigRational.swift @@ -1,7 +1,7 @@ import Foundation import BigInt -struct BigRational: Hashable { +struct BigRational: Hashable, Equatable { let numerator: BigUInt let denominator: BigUInt diff --git a/novawallet/Common/Model/ChainRegistry/Account/MetaAccountModel+Async.swift b/novawallet/Common/Model/ChainRegistry/Account/MetaAccountModel+Async.swift new file mode 100644 index 0000000000..fbbc849219 --- /dev/null +++ b/novawallet/Common/Model/ChainRegistry/Account/MetaAccountModel+Async.swift @@ -0,0 +1,27 @@ +import Foundation +import Operation_iOS + +extension MetaAccountModel { + func fetchChainAccountWrapper( + for chainId: ChainModel.Id, + using chainRegistry: ChainRegistryProtocol + ) -> CompoundOperationWrapper { + let chainWrapper = chainRegistry.asyncWaitChainWrapper(for: chainId) + + let selectedAccountOperation = ClosureOperation { + guard let chain = try chainWrapper.targetOperation.extractNoCancellableResultData() else { + throw ChainRegistryError.noChain(chainId) + } + + guard let selectedAccount = self.fetchMetaChainAccount(for: chain.accountRequest()) else { + throw ChainAccountFetchingError.accountNotExists + } + + return selectedAccount + } + + selectedAccountOperation.addDependency(chainWrapper.targetOperation) + + return chainWrapper.insertingTail(operation: selectedAccountOperation) + } +} diff --git a/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift b/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift index 6d0e19d01e..009e60f312 100644 --- a/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift +++ b/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift @@ -181,16 +181,16 @@ struct ChainModel: Equatable, Hashable { hasSwapHub || hasSwapHydra } - var hasAssetHubTransferFees: Bool { + var hasAssetHubFees: Bool { options?.contains(where: { $0 == .assetHubFees }) ?? false } - var hasHydrationTransferFees: Bool { + var hasHydrationFees: Bool { options?.contains(where: { $0 == .hydrationFees }) ?? false } - var hasCustomTransferFees: Bool { - hasAssetHubTransferFees || hasHydrationTransferFees + var hasCustomFees: Bool { + hasAssetHubFees || hasHydrationFees } var hasProxy: Bool { @@ -243,6 +243,30 @@ struct ChainModel: Equatable, Hashable { assets.map { ChainAsset(chain: self, asset: $0) } } + func chainAsset(for assetId: AssetModel.Id) -> ChainAsset? { + guard let asset = assets.first(where: { $0.assetId == assetId }) else { + return nil + } + + return .init(chain: self, asset: asset) + } + + func chainAssetOrError(for assetId: AssetModel.Id) throws -> ChainAsset { + guard let chainAsset = chainAsset(for: assetId) else { + throw ChainModelFetchError.noAsset(assetId: assetId) + } + + return chainAsset + } + + func chainAssetForSymbol(_ symbol: String) -> ChainAsset? { + guard let asset = assets.first(where: { $0.symbol == symbol }) else { + return nil + } + + return .init(chain: self, asset: asset) + } + func utilityAssetDisplayInfo() -> AssetBalanceDisplayInfo? { utilityAsset()?.displayInfo(with: icon) } diff --git a/novawallet/Common/Model/ChainRegistry/LocalChain/IndexedChainModels.swift b/novawallet/Common/Model/ChainRegistry/LocalChain/IndexedChainModels.swift new file mode 100644 index 0000000000..2fb70799f5 --- /dev/null +++ b/novawallet/Common/Model/ChainRegistry/LocalChain/IndexedChainModels.swift @@ -0,0 +1,9 @@ +import Foundation + +typealias IndexedChainModels = [ChainModel.Id: ChainModel] + +extension IndexedChainModels { + func resolve(chainAssetId: ChainAssetId) -> ChainAsset? { + self[chainAssetId.chainId]?.chainAsset(for: chainAssetId.assetId) + } +} diff --git a/novawallet/Common/Model/SubstrateAlias.swift b/novawallet/Common/Model/SubstrateAlias.swift index 46df57f39c..e8af1b5686 100644 --- a/novawallet/Common/Model/SubstrateAlias.swift +++ b/novawallet/Common/Model/SubstrateAlias.swift @@ -1,4 +1,5 @@ import Foundation +import BigInt typealias AccountAddress = String typealias AccountId = Data @@ -15,6 +16,10 @@ typealias EpochIndex = UInt64 typealias Moment = UInt32 typealias EraIndex = UInt32 typealias EraRange = (start: EraIndex, end: EraIndex) +typealias Balance = BigUInt +typealias ExtrinsicIndex = UInt32 +typealias ExtrinsicHash = String +typealias BlockHash = String extension AccountId { static func matchHex(_ value: String, chainFormat: ChainFormat) -> AccountId? { diff --git a/novawallet/Common/Model/Xcm/XcmDeliveryFee.swift b/novawallet/Common/Model/Xcm/XcmDeliveryFee.swift index a8ba828d7a..76c3ddb090 100644 --- a/novawallet/Common/Model/Xcm/XcmDeliveryFee.swift +++ b/novawallet/Common/Model/Xcm/XcmDeliveryFee.swift @@ -41,6 +41,15 @@ struct XcmDeliveryFee: Decodable { self = .undefined } } + + var alwaysHoldingPays: Bool? { + switch self { + case let .exponential(exponential): + return exponential.alwaysHoldingPays + default: + return nil + } + } } let toParent: Price? diff --git a/novawallet/Common/Model/Xcm/XcmTotalFeeModel.swift b/novawallet/Common/Model/Xcm/XcmTotalFeeModel.swift new file mode 100644 index 0000000000..7ec52739df --- /dev/null +++ b/novawallet/Common/Model/Xcm/XcmTotalFeeModel.swift @@ -0,0 +1,6 @@ +import Foundation + +struct XcmTotalFeeModel { + let origin: ExtrinsicFeeProtocol + let crosschain: XcmFeeModelProtocol +} diff --git a/novawallet/Common/Model/Xcm/XcmTransferRequest.swift b/novawallet/Common/Model/Xcm/XcmTransferRequest.swift index ec5be682b2..853b4ff6ff 100644 --- a/novawallet/Common/Model/Xcm/XcmTransferRequest.swift +++ b/novawallet/Common/Model/Xcm/XcmTransferRequest.swift @@ -4,4 +4,15 @@ import BigInt struct XcmTransferRequest { let unweighted: XcmUnweightedTransferRequest let maxWeight: BigUInt + let originFeeAsset: ChainAssetId? + + init( + unweighted: XcmUnweightedTransferRequest, + maxWeight: BigUInt, + originFeeAsset: ChainAssetId? = nil + ) { + self.unweighted = unweighted + self.maxWeight = maxWeight + self.originFeeAsset = originFeeAsset + } } diff --git a/novawallet/Common/Model/Xcm/XcmUnweightedTransferRequest.swift b/novawallet/Common/Model/Xcm/XcmUnweightedTransferRequest.swift index dabe33f083..336a5dc0b8 100644 --- a/novawallet/Common/Model/Xcm/XcmUnweightedTransferRequest.swift +++ b/novawallet/Common/Model/Xcm/XcmUnweightedTransferRequest.swift @@ -10,4 +10,16 @@ struct XcmUnweightedTransferRequest { var isNonReserveTransfer: Bool { reserve.chain.chainId != origin.chain.chainId && reserve.chain.chainId != destination.chain.chainId } + + init( + origin: ChainAsset, + destination: XcmTransferDestination, + reserve: XcmTransferReserve, + amount: BigUInt + ) { + self.origin = origin + self.destination = destination + self.reserve = reserve + self.amount = amount + } } diff --git a/novawallet/Common/Network/JSONRPC/ExtrinsicStatus.swift b/novawallet/Common/Network/JSONRPC/ExtrinsicStatus.swift index 4041407ff4..ed2595aa45 100644 --- a/novawallet/Common/Network/JSONRPC/ExtrinsicStatus.swift +++ b/novawallet/Common/Network/JSONRPC/ExtrinsicStatus.swift @@ -1,5 +1,21 @@ import Foundation +struct ExtrinsicStatusUpdate { + let extrinsicHash: String + let extrinsicStatus: ExtrinsicStatus + + func getInBlockOrFinalizedHash() -> BlockHash? { + switch extrinsicStatus { + case let .inBlock(blockHash): + blockHash + case let .finalized(blockHash): + blockHash + default: + nil + } + } +} + enum ExtrinsicStatus: Decodable { case inBlock(String) case finalized(String) diff --git a/novawallet/Common/Operation/OperationCombiningService.swift b/novawallet/Common/Operation/OperationCombiningService.swift index 0dc49ef306..18404437f8 100644 --- a/novawallet/Common/Operation/OperationCombiningService.swift +++ b/novawallet/Common/Operation/OperationCombiningService.swift @@ -167,4 +167,14 @@ extension OperationCombiningService { return .init(targetOperation: mappingOperation, dependencies: [loadingOperation]) } + + static func compoundNonOptionalWrapper( + operationQueue: OperationQueue, + wrapperClosure: @escaping () throws -> CompoundOperationWrapper + ) -> CompoundOperationWrapper { + compoundNonOptionalWrapper( + operationManager: OperationManager(operationQueue: operationQueue), + wrapperClosure: wrapperClosure + ) + } } diff --git a/novawallet/Common/Protocols/ExtrinsicSigningErrorHandling.swift b/novawallet/Common/Protocols/ExtrinsicSigningErrorHandling.swift index 34534bb8f6..e99332fa69 100644 --- a/novawallet/Common/Protocols/ExtrinsicSigningErrorHandling.swift +++ b/novawallet/Common/Protocols/ExtrinsicSigningErrorHandling.swift @@ -51,7 +51,7 @@ extension ExtrinsicSigningErrorHandling where Self: MessageSheetPresentable { completionClosure?(true) } case .dismissAllModals: - var root = view.controller.view.window?.rootViewController + let root = view.controller.view.window?.rootViewController root?.dismiss(animated: true) { completionClosure?(true) diff --git a/novawallet/Modules/AssetConversion/Model/AssetConversion.swift b/novawallet/Common/Services/AssetConversion/AssetConversion.swift similarity index 70% rename from novawallet/Modules/AssetConversion/Model/AssetConversion.swift rename to novawallet/Common/Services/AssetConversion/AssetConversion.swift index a34c9dbc6b..686047463e 100644 --- a/novawallet/Modules/AssetConversion/Model/AssetConversion.swift +++ b/novawallet/Common/Services/AssetConversion/AssetConversion.swift @@ -2,7 +2,7 @@ import Foundation import BigInt enum AssetConversion { - enum Direction { + enum Direction: Equatable { case sell case buy } @@ -35,19 +35,6 @@ enum AssetConversion { assetOut = args.assetOut self.context = context } - - func matches(other quote: Quote, slippage: BigRational, direction: Direction) -> Bool { - switch direction { - case .sell: - let amountOutMin = amountOut - slippage.mul(value: amountOut) - - return amountOutMin <= quote.amountOut - case .buy: - let amountInMax = amountIn + slippage.mul(value: amountIn) - - return amountInMax >= quote.amountIn - } - } } struct CallArgs: Hashable { @@ -58,7 +45,6 @@ enum AssetConversion { let receiver: AccountId let direction: Direction let slippage: BigRational - let context: String? } } diff --git a/novawallet/Modules/AssetConversion/Service/AssetConversionOperationFactory.swift b/novawallet/Common/Services/AssetConversion/AssetConversionOperationError.swift similarity index 100% rename from novawallet/Modules/AssetConversion/Service/AssetConversionOperationFactory.swift rename to novawallet/Common/Services/AssetConversion/AssetConversionOperationError.swift diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubExtrinsicConverter.swift b/novawallet/Common/Services/AssetConversion/AssetHub/AssetHubExtrinsicConverter.swift similarity index 87% rename from novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubExtrinsicConverter.swift rename to novawallet/Common/Services/AssetConversion/AssetHub/AssetHubExtrinsicConverter.swift index 3ce44684b5..e1407a3a87 100644 --- a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubExtrinsicConverter.swift +++ b/novawallet/Common/Services/AssetConversion/AssetHub/AssetHubExtrinsicConverter.swift @@ -1,6 +1,10 @@ import Foundation import SubstrateSdk +enum AssetHubExtrinsicConverterError: Error { + case remoteAssetNotFound(ChainAssetId) +} + enum AssetHubExtrinsicConverter { static func addingOperation( to builder: ExtrinsicBuilderProtocol, @@ -14,7 +18,7 @@ enum AssetHubExtrinsicConverter { chain: chain, codingFactory: codingFactory ) else { - throw AssetConversionExtrinsicServiceError.remoteAssetNotFound(args.assetIn) + throw AssetHubExtrinsicConverterError.remoteAssetNotFound(args.assetIn) } guard @@ -23,7 +27,7 @@ enum AssetHubExtrinsicConverter { chain: chain, codingFactory: codingFactory ) else { - throw AssetConversionExtrinsicServiceError.remoteAssetNotFound(args.assetOut) + throw AssetHubExtrinsicConverterError.remoteAssetNotFound(args.assetOut) } switch args.direction { diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubReQuoteService.swift b/novawallet/Common/Services/AssetConversion/AssetHub/AssetHubReQuoteService.swift similarity index 88% rename from novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubReQuoteService.swift rename to novawallet/Common/Services/AssetConversion/AssetHub/AssetHubReQuoteService.swift index 0c3d6c09ef..8202c9054b 100644 --- a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubReQuoteService.swift +++ b/novawallet/Common/Services/AssetConversion/AssetHub/AssetHubReQuoteService.swift @@ -4,7 +4,7 @@ final class AssetHubReQuoteService: ObservableSubscriptionSyncService [BatchStorageSubscriptionRequest] { let blockNumberRequest = BatchStorageSubscriptionRequest( innerRequest: UnkeyedSubscriptionRequest( - storagePath: .blockNumber, + storagePath: SystemPallet.blockNumberPath, localKey: "" ), mappingKey: nil diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift b/novawallet/Common/Services/AssetConversion/AssetHub/AssetHubSwapOperationFactory.swift similarity index 100% rename from novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift rename to novawallet/Common/Services/AssetConversion/AssetHub/AssetHubSwapOperationFactory.swift diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapQuoteBuilder.swift b/novawallet/Common/Services/AssetConversion/AssetHub/AssetHubSwapQuoteBuilder.swift similarity index 100% rename from novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapQuoteBuilder.swift rename to novawallet/Common/Services/AssetConversion/AssetHub/AssetHubSwapQuoteBuilder.swift diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift b/novawallet/Common/Services/AssetConversion/AssetHub/AssetHubTokensConverter.swift similarity index 100% rename from novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift rename to novawallet/Common/Services/AssetConversion/AssetHub/AssetHubTokensConverter.swift diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraConstants.swift b/novawallet/Common/Services/AssetConversion/HydraDx/HydraConstants.swift similarity index 100% rename from novawallet/Modules/AssetConversion/Service/HydraDx/HydraConstants.swift rename to novawallet/Common/Services/AssetConversion/HydraDx/HydraConstants.swift diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraDxTokenConverter.swift b/novawallet/Common/Services/AssetConversion/HydraDx/HydraDxTokenConverter.swift similarity index 80% rename from novawallet/Modules/AssetConversion/Service/HydraDx/HydraDxTokenConverter.swift rename to novawallet/Common/Services/AssetConversion/HydraDx/HydraDxTokenConverter.swift index 21d2f3bae4..d1c14f6dd4 100644 --- a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraDxTokenConverter.swift +++ b/novawallet/Common/Services/AssetConversion/HydraDx/HydraDxTokenConverter.swift @@ -31,7 +31,7 @@ enum HydraDxTokenConverterError: Error { enum HydraDxTokenConverter { static let nativeRemoteAssetId = HydraDx.AssetId(0) - static func converToLocal( + static func convertToLocal( for remoteAsset: HydraDx.AssetId, chain: ChainModel, codingFactory: RuntimeCoderFactoryProtocol @@ -73,6 +73,27 @@ enum HydraDxTokenConverter { return ChainAssetId(chainId: chain.chainId, assetId: localAsset.assetId) } + static func convertToRemoteLocalMapping( + remoteAssets: Set, + chain: ChainModel, + codingFactory: RuntimeCoderFactoryProtocol, + failureClosure: (HydraDx.AssetId, Error) throws -> Void + ) throws -> [HydraDx.AssetId: ChainAssetId] { + try remoteAssets.reduce(into: [:]) { accum, remoteAsset in + do { + let localAsset = try HydraDxTokenConverter.convertToLocal( + for: remoteAsset, + chain: chain, + codingFactory: codingFactory + ) + + accum[remoteAsset] = localAsset + } catch { + try failureClosure(remoteAsset, error) + } + } + } + static func convertToRemote( chainAsset: ChainAsset, codingFactory: RuntimeCoderFactoryProtocol diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraExtrinsicConverter.swift b/novawallet/Common/Services/AssetConversion/HydraDx/HydraExtrinsicConverter.swift similarity index 98% rename from novawallet/Modules/AssetConversion/Service/HydraDx/HydraExtrinsicConverter.swift rename to novawallet/Common/Services/AssetConversion/HydraDx/HydraExtrinsicConverter.swift index 45e89ef0f4..8580f4e94c 100644 --- a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraExtrinsicConverter.swift +++ b/novawallet/Common/Services/AssetConversion/HydraDx/HydraExtrinsicConverter.swift @@ -55,7 +55,7 @@ enum HydraExtrinsicConverter { assetIn: component.assetIn, assetOut: component.assetOut ) - case let .xyk: + case .xyk: return HydraRouter.Trade( pool: .xyk, assetIn: component.assetIn, diff --git a/novawallet/Common/Services/AssetConversion/HydraDx/HydraFlowState.swift b/novawallet/Common/Services/AssetConversion/HydraDx/HydraFlowState.swift new file mode 100644 index 0000000000..743ee74cc8 --- /dev/null +++ b/novawallet/Common/Services/AssetConversion/HydraDx/HydraFlowState.swift @@ -0,0 +1,160 @@ +import Foundation +import SubstrateSdk +import Operation_iOS + +final class HydraFlowState { + let account: ChainAccountResponse + let chain: ChainModel + let connection: JSONRPCEngine + let runtimeProvider: RuntimeProviderProtocol + let userStorageFacade: StorageFacadeProtocol + let substrateStorageFacade: StorageFacadeProtocol + let operationQueue: OperationQueue + + let mutex = NSLock() + + private var omnipoolFlowState: HydraOmnipoolFlowState? + private var stableswapFlowState: HydraStableswapFlowState? + private var xykswapFlowState: HydraXYKFlowState? + private var routesFactory: HydraRoutesOperationFactoryProtocol? + + private var currentSwapPair: HydraDx.LocalSwapPair? + + init( + account: ChainAccountResponse, + chain: ChainModel, + connection: JSONRPCEngine, + runtimeProvider: RuntimeProviderProtocol, + userStorageFacade: StorageFacadeProtocol, + substrateStorageFacade: StorageFacadeProtocol, + operationQueue: OperationQueue + ) { + self.account = account + self.chain = chain + self.connection = connection + self.runtimeProvider = runtimeProvider + self.userStorageFacade = userStorageFacade + self.substrateStorageFacade = substrateStorageFacade + self.operationQueue = operationQueue + } +} + +extension HydraFlowState { + func resetServicesIfNotMatchingPair(_ swapPair: HydraDx.LocalSwapPair) { + mutex.lock() + + defer { + mutex.unlock() + } + + guard swapPair != currentSwapPair else { + return + } + + omnipoolFlowState?.resetServices() + stableswapFlowState?.resetServices() + xykswapFlowState?.resetServices() + + routesFactory = nil + + currentSwapPair = swapPair + } + + func getOmnipoolFlowState() -> HydraOmnipoolFlowState { + mutex.lock() + + defer { + mutex.unlock() + } + + if let state = omnipoolFlowState { + return state + } + + let newState = HydraOmnipoolFlowState( + account: account, + chain: chain, + connection: connection, + runtimeProvider: runtimeProvider, + notificationsRegistrar: nil, + operationQueue: operationQueue + ) + + omnipoolFlowState = newState + + return newState + } + + func getStableswapFlowState() -> HydraStableswapFlowState { + mutex.lock() + + defer { + mutex.unlock() + } + + if let state = stableswapFlowState { + return state + } + + let newState = HydraStableswapFlowState( + account: account, + chain: chain, + connection: connection, + runtimeProvider: runtimeProvider, + notificationsRegistrar: nil, + operationQueue: operationQueue + ) + + stableswapFlowState = newState + + return newState + } + + func getXYKSwapFlowState() -> HydraXYKFlowState { + mutex.lock() + + defer { + mutex.unlock() + } + + if let state = xykswapFlowState { + return state + } + + let newState = HydraXYKFlowState( + account: account, + chain: chain, + connection: connection, + runtimeProvider: runtimeProvider, + notificationsRegistrar: nil, + operationQueue: operationQueue + ) + + xykswapFlowState = newState + + return newState + } + + func getRoutesFactory() -> HydraRoutesOperationFactoryProtocol { + mutex.lock() + + defer { + mutex.unlock() + } + + if let factory = routesFactory { + return factory + } + + let factory = HydraRoutesOperationFactory( + chain: chain, + connection: connection, + runtimeProvider: runtimeProvider, + operationQueue: operationQueue + ) + + routesFactory = factory + + return factory + } +} diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraQuoteFactory.swift b/novawallet/Common/Services/AssetConversion/HydraDx/HydraQuoteFactory.swift similarity index 100% rename from novawallet/Modules/AssetConversion/Service/HydraDx/HydraQuoteFactory.swift rename to novawallet/Common/Services/AssetConversion/HydraDx/HydraQuoteFactory.swift diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraRoutesOperationFactory.swift b/novawallet/Common/Services/AssetConversion/HydraDx/HydraRoutesOperationFactory.swift similarity index 98% rename from novawallet/Modules/AssetConversion/Service/HydraDx/HydraRoutesOperationFactory.swift rename to novawallet/Common/Services/AssetConversion/HydraDx/HydraRoutesOperationFactory.swift index 3c6a14f196..de7832f913 100644 --- a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraRoutesOperationFactory.swift +++ b/novawallet/Common/Services/AssetConversion/HydraDx/HydraRoutesOperationFactory.swift @@ -10,7 +10,7 @@ protocol HydraRoutesOperationFactoryProtocol { final class HydraRoutesOperationFactory { let omnipoolTokensFactory: HydraOmnipoolTokensFactory - let stableswapTokensFactory: HydraStableSwapsTokensFactory + let stableswapTokensFactory: HydraStableswapTokensFactory let xykTokensFactory: HydraXYKPoolTokensFactory let runtimeProvider: RuntimeProviderProtocol let chain: ChainModel diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraRoutesResolver.swift b/novawallet/Common/Services/AssetConversion/HydraDx/HydraRoutesResolver.swift similarity index 95% rename from novawallet/Modules/AssetConversion/Service/HydraDx/HydraRoutesResolver.swift rename to novawallet/Common/Services/AssetConversion/HydraDx/HydraRoutesResolver.swift index 822f4cdcdb..a7d85ff63c 100644 --- a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraRoutesResolver.swift +++ b/novawallet/Common/Services/AssetConversion/HydraDx/HydraRoutesResolver.swift @@ -87,7 +87,7 @@ enum HydraRoutesResolver { let routes = GraphModel( connections: allConnections - ).calculateShortestPath(from: assetIn, nodeEnd: assetOut, topN: 4) + ).calculateShortestPath(from: assetIn, nodeEnd: assetOut, topN: 4, filter: .allEdges()) return routes.map { HydraDx.LocalSwapRoute(components: $0) } } @@ -127,9 +127,17 @@ enum HydraRoutesResolver { } } -extension HydraDx.SwapRoute.Component: GraphEdgeProtocol where Asset: Hashable { +extension HydraDx.SwapRoute.Component: GraphWeightableEdgeProtocol where Asset: Hashable { typealias Node = Asset + var weight: Int { + 1 + } + + var origin: Asset { + assetIn + } + var destination: Asset { assetOut } diff --git a/novawallet/Common/Services/AssetConversion/HydraDx/HydraSwapFeeCurrencyService.swift b/novawallet/Common/Services/AssetConversion/HydraDx/HydraSwapFeeCurrencyService.swift new file mode 100644 index 0000000000..b48367f20c --- /dev/null +++ b/novawallet/Common/Services/AssetConversion/HydraDx/HydraSwapFeeCurrencyService.swift @@ -0,0 +1,47 @@ +import Foundation +import SubstrateSdk +import Operation_iOS + +class HydraSwapFeeCurrencyService: ObservableSubscriptionSyncService { + let payerAccountId: AccountId + + init( + payerAccountId: AccountId, + connection: JSONRPCEngine, + runtimeProvider: RuntimeCodingServiceProtocol, + operationQueue: OperationQueue, + repository: AnyDataProviderRepository? = nil, + workQueue: DispatchQueue = .global(), + retryStrategy: ReconnectionStrategyProtocol = ExponentialReconnection(), + logger: LoggerProtocol = Logger.shared + ) { + self.payerAccountId = payerAccountId + + super.init( + connection: connection, + runtimeProvider: runtimeProvider, + operationQueue: operationQueue, + repository: repository, + workQueue: workQueue, + retryStrategy: retryStrategy, + logger: logger + ) + } + + func getRequests(for accountId: AccountId) -> [BatchStorageSubscriptionRequest] { + let feeCurrencyRequest = BatchStorageSubscriptionRequest( + innerRequest: MapSubscriptionRequest( + storagePath: HydraDx.accountFeeCurrencyPath, + localKey: "", + keyParamClosure: { BytesCodable(wrappedValue: accountId) } + ), + mappingKey: HydraDx.SwapFeeCurrencyStateChange.Key.feeCurrency.rawValue + ) + + return [feeCurrencyRequest] + } + + override func getRequests() throws -> [BatchStorageSubscriptionRequest] { + getRequests(for: payerAccountId) + } +} diff --git a/novawallet/Common/Services/AssetConversion/HydraDx/HydraSwapParams.swift b/novawallet/Common/Services/AssetConversion/HydraDx/HydraSwapParams.swift new file mode 100644 index 0000000000..43577d46c3 --- /dev/null +++ b/novawallet/Common/Services/AssetConversion/HydraDx/HydraSwapParams.swift @@ -0,0 +1,23 @@ +import Foundation + +struct HydraSwapParams { + struct Params { + let newFeeCurrency: ChainAssetId + let referral: AccountId? + + var shouldSetReferral: Bool { + referral == nil + } + } + + enum Operation { + case omniSell(HydraOmnipool.SellCall) + case omniBuy(HydraOmnipool.BuyCall) + case routedSell(HydraRouter.SellCall) + case routedBuy(HydraRouter.BuyCall) + } + + let params: Params + let updateReferral: HydraDx.LinkReferralCodeCall? + let swap: Operation +} diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraSwapParamsService.swift b/novawallet/Common/Services/AssetConversion/HydraDx/HydraSwapParamsService.swift similarity index 77% rename from novawallet/Modules/AssetConversion/Service/HydraDx/HydraSwapParamsService.swift rename to novawallet/Common/Services/AssetConversion/HydraDx/HydraSwapParamsService.swift index 2154c5f8cd..2e561e20e5 100644 --- a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraSwapParamsService.swift +++ b/novawallet/Common/Services/AssetConversion/HydraDx/HydraSwapParamsService.swift @@ -29,15 +29,6 @@ class HydraSwapParamsService: ObservableSubscriptionSyncService [BatchStorageSubscriptionRequest] { - let feeCurrencyRequest = BatchStorageSubscriptionRequest( - innerRequest: MapSubscriptionRequest( - storagePath: HydraDx.accountFeeCurrencyPath, - localKey: "", - keyParamClosure: { BytesCodable(wrappedValue: accountId) } - ), - mappingKey: HydraDx.SwapRemoteStateChange.Key.feeCurrency.rawValue - ) - let referralRequest = BatchStorageSubscriptionRequest( innerRequest: MapSubscriptionRequest( storagePath: HydraDx.referralLinkedAccountPath, @@ -47,7 +38,7 @@ class HydraSwapParamsService: ObservableSubscriptionSyncService [BatchStorageSubscriptionRequest] { diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraSwapRemoteState.swift b/novawallet/Common/Services/AssetConversion/HydraDx/HydraSwapRemoteState.swift similarity index 58% rename from novawallet/Modules/AssetConversion/Service/HydraDx/HydraSwapRemoteState.swift rename to novawallet/Common/Services/AssetConversion/HydraDx/HydraSwapRemoteState.swift index ae440a40eb..fd3ea2d101 100644 --- a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraSwapRemoteState.swift +++ b/novawallet/Common/Services/AssetConversion/HydraDx/HydraSwapRemoteState.swift @@ -5,37 +5,26 @@ extension HydraDx { struct SwapRemoteState: ObservableSubscriptionStateProtocol { typealias TChange = SwapRemoteStateChange - let feeCurrency: HydraDx.AssetId? let referralLink: AccountId? - init( - feeCurrency: HydraDx.AssetId?, - referralLink: AccountId? - ) { - self.feeCurrency = feeCurrency + init(referralLink: AccountId?) { self.referralLink = referralLink } init(change: HydraDx.SwapRemoteStateChange) { - feeCurrency = change.feeCurrency.valueWhenDefined(else: nil) referralLink = change.referralLink.valueWhenDefined(else: nil) } func merging(change: SwapRemoteStateChange) -> SwapRemoteState { - .init( - feeCurrency: change.feeCurrency.valueWhenDefined(else: feeCurrency), - referralLink: change.referralLink.valueWhenDefined(else: referralLink) - ) + .init(referralLink: change.referralLink.valueWhenDefined(else: referralLink)) } } struct SwapRemoteStateChange: BatchStorageSubscriptionResult { enum Key: String { - case feeCurrency case referralLink } - let feeCurrency: UncertainStorage let referralLink: UncertainStorage init( @@ -43,12 +32,6 @@ extension HydraDx { blockHashJson _: JSON, context: [CodingUserInfoKey: Any]? ) throws { - feeCurrency = try UncertainStorage?>( - values: values, - mappingKey: Key.feeCurrency.rawValue, - context: context - ).map { $0?.value } - referralLink = try UncertainStorage( values: values, mappingKey: Key.referralLink.rawValue, diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraSwapRoute.swift b/novawallet/Common/Services/AssetConversion/HydraDx/HydraSwapRoute.swift similarity index 100% rename from novawallet/Modules/AssetConversion/Service/HydraDx/HydraSwapRoute.swift rename to novawallet/Common/Services/AssetConversion/HydraDx/HydraSwapRoute.swift diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraTokensFactory.swift b/novawallet/Common/Services/AssetConversion/HydraDx/HydraTokensFactory.swift similarity index 96% rename from novawallet/Modules/AssetConversion/Service/HydraDx/HydraTokensFactory.swift rename to novawallet/Common/Services/AssetConversion/HydraDx/HydraTokensFactory.swift index 466389a13d..9c3b866d7d 100644 --- a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraTokensFactory.swift +++ b/novawallet/Common/Services/AssetConversion/HydraDx/HydraTokensFactory.swift @@ -68,7 +68,7 @@ extension HydraTokensFactory: HydraTokensFactoryProtocol { let graph = GraphModelFactory.createFromConnections(pairPools) return graph.connections.keys.reduce(into: [ChainAssetId: Set]()) { accum, asset in - accum[asset] = graph.reachableNodes(for: asset) + accum[asset] = graph.calculateReachableNodes(for: asset, filter: .allEdges()) } } @@ -89,7 +89,7 @@ extension HydraTokensFactory: HydraTokensFactoryProtocol { let graph = GraphModelFactory.createFromConnections(pairPools) - return graph.reachableNodes(for: chainAssetId) + return graph.calculateReachableNodes(for: chainAssetId, filter: .allEdges()) } wrappers.forEach { mergeOperation.addDependency($0.targetOperation) } @@ -152,7 +152,7 @@ extension HydraTokensFactory { operationQueue: operationQueue ) - let stableswap = HydraStableSwapsTokensFactory( + let stableswap = HydraStableswapTokensFactory( chain: chain, runtimeService: runtimeService, connection: connection, diff --git a/novawallet/Common/Services/AssetConversion/HydraDx/SwapFeeCurrencyState.swift b/novawallet/Common/Services/AssetConversion/HydraDx/SwapFeeCurrencyState.swift new file mode 100644 index 0000000000..5aa9863f3a --- /dev/null +++ b/novawallet/Common/Services/AssetConversion/HydraDx/SwapFeeCurrencyState.swift @@ -0,0 +1,42 @@ +import Foundation +import SubstrateSdk + +extension HydraDx { + struct SwapFeeCurrencyState: ObservableSubscriptionStateProtocol { + typealias TChange = SwapFeeCurrencyStateChange + + let feeCurrency: HydraDx.AssetId? + + init(feeCurrency: HydraDx.AssetId?) { + self.feeCurrency = feeCurrency + } + + init(change: HydraDx.SwapFeeCurrencyStateChange) { + feeCurrency = change.feeCurrency.valueWhenDefined(else: nil) + } + + func merging(change: SwapFeeCurrencyStateChange) -> SwapFeeCurrencyState { + .init(feeCurrency: change.feeCurrency.valueWhenDefined(else: feeCurrency)) + } + } + + struct SwapFeeCurrencyStateChange: BatchStorageSubscriptionResult { + enum Key: String { + case feeCurrency + } + + let feeCurrency: UncertainStorage + + init( + values: [BatchStorageSubscriptionResultValue], + blockHashJson _: JSON, + context: [CodingUserInfoKey: Any]? + ) throws { + feeCurrency = try UncertainStorage?>( + values: values, + mappingKey: Key.feeCurrency.rawValue, + context: context + ).map { $0?.value } + } + } +} diff --git a/novawallet/Common/Services/AssetExchange/AssetExchangeExecutionManager.swift b/novawallet/Common/Services/AssetExchange/AssetExchangeExecutionManager.swift new file mode 100644 index 0000000000..3157f6be9e --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/AssetExchangeExecutionManager.swift @@ -0,0 +1,151 @@ +import Foundation +import Operation_iOS + +enum AssetExchangeExecutionManagerError: Error { + case invalidRouteDetails +} + +typealias AssetExchangeOperationExecutionStartClosure = (Int) -> Void + +final class AssetExchangeExecutionManager { + typealias ResultType = Balance + + let operations: [AssetExchangeAtomicOperationProtocol] + let fee: AssetExchangeFee + let operationQueue: OperationQueue + let syncQueue: DispatchQueue + let operationStartClosure: AssetExchangeOperationExecutionStartClosure + let notificationQueue: DispatchQueue + let logger: LoggerProtocol + + private var completionClosure: ((Result) -> Void)? + private let callStore = CancellableCallStore() + private var isFinished: Bool = false + + init( + operations: [AssetExchangeAtomicOperationProtocol], + fee: AssetExchangeFee, + operationQueue: OperationQueue, + operationStartClosure: @escaping AssetExchangeOperationExecutionStartClosure, + notificationQueue: DispatchQueue, + logger: LoggerProtocol + ) { + self.operations = operations + self.fee = fee + self.operationQueue = operationQueue + self.operationStartClosure = operationStartClosure + self.notificationQueue = notificationQueue + self.logger = logger + + syncQueue = DispatchQueue(label: "io.novawallet.asset.exchange.exec.\(UUID().uuidString)") + } + + private func complete(with result: Result) { + isFinished = true + completionClosure?(result) + } + + private func startFirstSegmentExecution() { + do { + guard + let firstSegment = fee.route.items.first, + let firstFees = fee.operationFees.first else { + throw AssetExchangeExecutionManagerError.invalidRouteDetails + } + + let amountIn = firstSegment.amountIn(for: fee.route.direction) + let amountInWithFee = amountIn + fee.intermediateFeesInAssetIn + + let holdingFee = try firstFees.totalToPayFromAmountEnsuring(asset: firstSegment.edge.origin) + let amountInWithHolding = amountInWithFee + holdingFee + + executeSegment(at: 0, amountIn: amountInWithHolding) + } catch { + logger.error("Failed first segment processing: \(error)") + complete(with: .failure(error)) + } + } + + private func executeSegment(at index: Int, amountIn: Balance) { + guard !isFinished else { + return + } + + logger.debug("Executing swap \(index)") + + let shouldReplaceBuyWithSell = index != 0 + let swapLimit = operations[index].swapLimit.replacingAmountIn( + amountIn, + shouldReplaceBuyWithSell: shouldReplaceBuyWithSell + ) + + let wrapper = operations[index].executeWrapper(for: swapLimit) + + notificationQueue.async { [weak self] in + self?.operationStartClosure(index) + } + + executeCancellable( + wrapper: wrapper, + inOperationQueue: operationQueue, + backingCallIn: callStore, + runningCallbackIn: syncQueue + ) { [weak self] result in + switch result { + case let .success(amountOut): + self?.logger.debug("Executed swap \(index): \(String(amountOut))") + self?.correctAmountAndExecuteNext(after: index, amountOut: amountOut) + case let .failure(error): + self?.logger.error("Failed swap exec \(index): \(error)") + self?.complete(with: .failure(error)) + } + } + } + + private func correctAmountAndExecuteNext(after currentSegment: Int, amountOut: Balance) { + if currentSegment == operations.count - 1 { + complete(with: .success(amountOut)) + return + } + + let nextSegment = currentSegment + 1 + + do { + let leaveOnAccount = try fee.operationFees[nextSegment].totalAmountToPayFromSelectedAccount() + + logger.debug("Amount for fee: \(Balance(leaveOnAccount))") + + let correctedAmount = amountOut.subtractOrZero(leaveOnAccount) + + executeSegment(at: nextSegment, amountIn: correctedAmount) + } catch { + logger.error("Failed segment processing \(nextSegment): \(error)") + complete(with: .failure(error)) + } + } +} + +extension AssetExchangeExecutionManager: Longrunable { + func start(with completionClosure: @escaping (Result) -> Void) { + syncQueue.async { + guard !self.isFinished else { + return + } + + self.completionClosure = completionClosure + + self.startFirstSegmentExecution() + } + } + + func cancel() { + syncQueue.async { + guard !self.isFinished else { + return + } + + self.isFinished = true + self.callStore.cancel() + } + } +} diff --git a/novawallet/Common/Services/AssetExchange/AssetExchangeOperationPrototypeFactory.swift b/novawallet/Common/Services/AssetExchange/AssetExchangeOperationPrototypeFactory.swift new file mode 100644 index 0000000000..3e711710ee --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/AssetExchangeOperationPrototypeFactory.swift @@ -0,0 +1,20 @@ +import Foundation + +final class AssetExchangeOperationPrototypeFactory { + func createOperationPrototypes( + from path: AssetExchangeGraphPath + ) throws -> [AssetExchangeOperationPrototypeProtocol] { + try path.reduce([]) { curOperations, edge in + if + let lastOperation = curOperations.last, + let newOperation = try edge.appendToOperationPrototype( + lastOperation + ) { + return curOperations.dropLast() + [newOperation] + } else { + let newOperation = try edge.beginOperationPrototype() + return curOperations + [newOperation] + } + } + } +} diff --git a/novawallet/Common/Services/AssetExchange/AssetExchangePathFilter.swift b/novawallet/Common/Services/AssetExchange/AssetExchangePathFilter.swift new file mode 100644 index 0000000000..673182395e --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/AssetExchangePathFilter.swift @@ -0,0 +1,57 @@ +import Foundation + +final class AssetExchangePathFilter { + typealias Edge = AnyAssetExchangeEdge + + let selectedWallet: MetaAccountModel + let chainRegistry: ChainRegistryProtocol + let sufficiencyProvider: AssetExchangeSufficiencyProviding + let feeSupport: AssetExchangeFeeSupporting + + init( + selectedWallet: MetaAccountModel, + chainRegistry: ChainRegistryProtocol, + sufficiencyProvider: AssetExchangeSufficiencyProviding, + feeSupport: AssetExchangeFeeSupporting + ) { + self.selectedWallet = selectedWallet + self.chainRegistry = chainRegistry + self.sufficiencyProvider = sufficiencyProvider + self.feeSupport = feeSupport + } +} + +extension AssetExchangePathFilter: GraphEdgeFiltering { + func shouldVisit(edge: Edge, predecessor: Edge?) -> Bool { + guard + let chainIn = chainRegistry.getChain(for: edge.origin.chainId), + let chainAssetIn = chainIn.chainAsset(for: edge.origin.assetId), + let chainOut = chainRegistry.getChain(for: edge.destination.chainId), + let chainAssetOut = chainOut.chainAsset(for: edge.destination.assetId) else { + return false + } + + // make sure there is origin and destination accounts + guard selectedWallet.hasAccount(in: chainIn), selectedWallet.hasAccount(in: chainOut) else { + return false + } + + // first segment always allowed + guard let predecessor else { + return true + } + + if !sufficiencyProvider.isSufficient(chainAsset: chainAssetOut) { + return false + } + + if edge.shouldIgnoreFeeRequirement(after: predecessor) { + return true + } + + let canPayFees = (chainAssetIn.isUtilityAsset || feeSupport.canPayFee(inNonNative: chainAssetIn)) && + edge.canPayNonNativeFeesInIntermediatePosition() + + return canPayFees + } +} diff --git a/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetConversionEventParser.swift b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetConversionEventParser.swift new file mode 100644 index 0000000000..d365b499f5 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetConversionEventParser.swift @@ -0,0 +1,27 @@ +import Foundation + +final class AssetConversionEventParser { + let logger: LoggerProtocol + + init(logger: LoggerProtocol) { + self.logger = logger + } + + func extractDeposit(from events: [Event], using codingFactory: RuntimeCoderFactoryProtocol) -> Balance? { + guard let event = events.last else { + return nil + } + + do { + let parsedEvent: AssetConversionPallet.SwapExecutedEvent = try ExtrinsicExtraction.getEventParams( + from: event, + context: codingFactory.createRuntimeJsonContext() + ) + + return parsedEvent.amountOut + } catch { + logger.error("Event parsing error: \(error)") + return nil + } + } +} diff --git a/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetConversionEventsMatching.swift b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetConversionEventsMatching.swift new file mode 100644 index 0000000000..8895f7b8f0 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetConversionEventsMatching.swift @@ -0,0 +1,10 @@ +import Foundation + +final class AssetConversionEventsMatching: ExtrinsicEventsMatching { + func match(event: Event, using codingFactory: RuntimeCoderFactoryProtocol) -> Bool { + codingFactory.metadata.eventMatches( + event, + path: AssetConversionPallet.swapExecutedEvent + ) + } +} diff --git a/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetExchangeOperationFee+AssetHub.swift b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetExchangeOperationFee+AssetHub.swift new file mode 100644 index 0000000000..4338a2e8fb --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetExchangeOperationFee+AssetHub.swift @@ -0,0 +1,16 @@ +import Foundation + +extension AssetExchangeOperationFee { + init(extrinsicFee: ExtrinsicFeeProtocol, args: AssetExchangeAtomicOperationArgs) { + submissionFee = .init( + amountWithAsset: .init( + amount: extrinsicFee.amount, + asset: args.feeAsset + ), + payer: extrinsicFee.payer, + weight: extrinsicFee.weight + ) + + postSubmissionFee = .init(paidByAccount: [], paidFromAmount: []) + } +} diff --git a/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubExchangeAtomicOperation.swift b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubExchangeAtomicOperation.swift new file mode 100644 index 0000000000..76b2feb58e --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubExchangeAtomicOperation.swift @@ -0,0 +1,155 @@ +import Foundation +import Operation_iOS + +enum AssetHubExchangeAtomicOperationError: Error { + case noEventsInResult +} + +final class AssetHubExchangeAtomicOperation { + let host: AssetHubExchangeHostProtocol + let edge: any AssetExchangableGraphEdge + let operationArgs: AssetExchangeAtomicOperationArgs + + init( + host: AssetHubExchangeHostProtocol, + operationArgs: AssetExchangeAtomicOperationArgs, + edge: any AssetExchangableGraphEdge + ) { + self.host = host + self.operationArgs = operationArgs + self.edge = edge + } + + private func createFeeWrapper() -> CompoundOperationWrapper { + let callArgs = AssetConversion.CallArgs( + assetIn: edge.origin, + amountIn: operationArgs.swapLimit.amountIn, + assetOut: edge.destination, + amountOut: operationArgs.swapLimit.amountOut, + receiver: host.selectedAccount.accountId, + direction: operationArgs.swapLimit.direction, + slippage: operationArgs.swapLimit.slippage + ) + + let codingFactoryOperation = host.runtimeService.fetchCoderFactoryOperation() + + let feeWrapper = host.extrinsicOperationFactory.estimateFeeOperation({ builder in + let codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + + return try AssetHubExtrinsicConverter.addingOperation( + to: builder, + chain: self.host.chain, + args: callArgs, + codingFactory: codingFactory + ) + }, payingIn: operationArgs.feeAsset) + + feeWrapper.addDependency(operations: [codingFactoryOperation]) + + return feeWrapper.insertingHead(operations: [codingFactoryOperation]) + } +} + +extension AssetHubExchangeAtomicOperation: AssetExchangeAtomicOperationProtocol { + func executeWrapper(for swapLimit: AssetExchangeSwapLimit) -> CompoundOperationWrapper { + let codingFactoryOperation = host.runtimeService.fetchCoderFactoryOperation() + + let executeWrapper = OperationCombiningService.compoundNonOptionalWrapper( + operationQueue: host.operationQueue + ) { + let callArgs = AssetConversion.CallArgs( + assetIn: self.edge.origin, + amountIn: swapLimit.amountIn, + assetOut: self.edge.destination, + amountOut: swapLimit.amountOut, + receiver: self.host.selectedAccount.accountId, + direction: swapLimit.direction, + slippage: swapLimit.slippage + ) + + let codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + + let submittionWrapper = self.host.submissionMonitorFactory.submitAndMonitorWrapper( + extrinsicBuilderClosure: { builder in + try AssetHubExtrinsicConverter.addingOperation( + to: builder, + chain: self.host.chain, + args: callArgs, + codingFactory: codingFactory + ) + }, + payingIn: self.operationArgs.feeAsset, + signer: self.host.signingWrapper, + matchingEvents: AssetConversionEventsMatching() + ) + + let codingFactoryOperation = self.host.runtimeService.fetchCoderFactoryOperation() + + let monitorOperation = ClosureOperation { + let submittionResult = try submittionWrapper.targetOperation.extractNoCancellableResultData() + let codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + + switch submittionResult { + case let .success(executionResult): + let eventParser = AssetConversionEventParser(logger: self.host.logger) + + self.host.logger.debug("Execution success: \(executionResult.interestedEvents)") + + guard let amountOut = eventParser.extractDeposit( + from: executionResult.interestedEvents, + using: codingFactory + ) else { + throw AssetHubExchangeAtomicOperationError.noEventsInResult + } + + self.host.logger.debug("Arrived amount: \(String(amountOut))") + + return amountOut + case let .failure(executionFailure): + throw executionFailure.error + } + } + + monitorOperation.addDependency(submittionWrapper.targetOperation) + monitorOperation.addDependency(codingFactoryOperation) + + return submittionWrapper + .insertingHead(operations: [codingFactoryOperation]) + .insertingTail(operation: monitorOperation) + } + + executeWrapper.addDependency(operations: [codingFactoryOperation]) + + return executeWrapper.insertingHead(operations: [codingFactoryOperation]) + } + + func estimateFee() -> CompoundOperationWrapper { + let feeWrapper = createFeeWrapper() + + let mappingOperation = ClosureOperation { + let extrinsicFee = try feeWrapper.targetOperation.extractNoCancellableResultData() + + return AssetExchangeOperationFee(extrinsicFee: extrinsicFee, args: self.operationArgs) + } + + mappingOperation.addDependency(feeWrapper.targetOperation) + + return feeWrapper.insertingTail(operation: mappingOperation) + } + + func requiredAmountToGetAmountOut( + _ amountOutClosure: @escaping () throws -> Balance + ) -> CompoundOperationWrapper { + OperationCombiningService.compoundNonOptionalWrapper( + operationQueue: host.operationQueue + ) { + let amountOut = try amountOutClosure() + + return self.edge.quote(amount: amountOut, direction: .buy) + } + } + + var swapLimit: AssetExchangeSwapLimit { + operationArgs.swapLimit + } +} diff --git a/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubExchangeEdge.swift b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubExchangeEdge.swift new file mode 100644 index 0000000000..c8cb786bf7 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubExchangeEdge.swift @@ -0,0 +1,126 @@ +import Foundation +import Operation_iOS + +final class AssetHubExchangeEdge { + let origin: ChainAssetId + let destination: ChainAssetId + let quoteFactory: AssetHubSwapOperationFactoryProtocol + let host: AssetHubExchangeHostProtocol + + init( + origin: ChainAssetId, + destination: ChainAssetId, + quoteFactory: AssetHubSwapOperationFactoryProtocol, + host: AssetHubExchangeHostProtocol + ) { + self.origin = origin + self.destination = destination + self.quoteFactory = quoteFactory + self.host = host + } +} + +extension AssetHubExchangeEdge: AssetExchangableGraphEdge { + var type: AssetExchangeEdgeType { .assetHubSwap } + + var weight: Int { AssetsExchange.defaultEdgeWeight + 1 } + + func quote( + amount: Balance, + direction: AssetConversion.Direction + ) -> CompoundOperationWrapper { + let quoteArgs = AssetConversion.QuoteArgs( + assetIn: origin, + assetOut: destination, + amount: amount, + direction: direction + ) + + _ = host.flowState.setupReQuoteService() + + let quoteWrapper = quoteFactory.quote(for: quoteArgs) + + let mappingOperation = ClosureOperation { + let quote = try quoteWrapper.targetOperation.extractNoCancellableResultData() + switch direction { + case .sell: + return quote.amountOut + case .buy: + return quote.amountIn + } + } + + mappingOperation.addDependency(quoteWrapper.targetOperation) + + return quoteWrapper.insertingTail(operation: mappingOperation) + } + + func beginOperation(for args: AssetExchangeAtomicOperationArgs) throws -> AssetExchangeAtomicOperationProtocol { + AssetHubExchangeAtomicOperation( + host: host, + operationArgs: args, + edge: self + ) + } + + func appendToOperation( + _: AssetExchangeAtomicOperationProtocol, + args _: AssetExchangeAtomicOperationArgs + ) -> AssetExchangeAtomicOperationProtocol? { + nil + } + + func shouldIgnoreFeeRequirement(after _: any AssetExchangableGraphEdge) -> Bool { + false + } + + func canPayNonNativeFeesInIntermediatePosition() -> Bool { + true + } + + func beginMetaOperation( + for amountIn: Balance, + amountOut: Balance + ) throws -> AssetExchangeMetaOperationProtocol { + guard let assetIn = host.chain.chainAsset(for: origin.assetId) else { + throw ChainModelFetchError.noAsset(assetId: origin.assetId) + } + + guard let assetOut = host.chain.chainAsset(for: destination.assetId) else { + throw ChainModelFetchError.noAsset(assetId: destination.assetId) + } + + return AssetHubExchangeMetaOperation( + assetIn: assetIn, + assetOut: assetOut, + amountIn: amountIn, + amountOut: amountOut + ) + } + + func appendToMetaOperation( + _: AssetExchangeMetaOperationProtocol, + amountIn _: Balance, + amountOut _: Balance + ) throws -> AssetExchangeMetaOperationProtocol? { + nil + } + + func beginOperationPrototype() throws -> AssetExchangeOperationPrototypeProtocol { + guard let assetIn = host.chain.chainAsset(for: origin.assetId) else { + throw ChainModelFetchError.noAsset(assetId: origin.assetId) + } + + guard let assetOut = host.chain.chainAsset(for: destination.assetId) else { + throw ChainModelFetchError.noAsset(assetId: destination.assetId) + } + + return AssetHubExchangeOperationPrototype(assetIn: assetIn, assetOut: assetOut, host: host) + } + + func appendToOperationPrototype( + _: AssetExchangeOperationPrototypeProtocol + ) throws -> AssetExchangeOperationPrototypeProtocol? { + nil + } +} diff --git a/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubExchangeHost.swift b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubExchangeHost.swift new file mode 100644 index 0000000000..c3325c647e --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubExchangeHost.swift @@ -0,0 +1,56 @@ +import Foundation +import SubstrateSdk + +protocol AssetHubExchangeHostProtocol { + var chain: ChainModel { get } + var selectedAccount: ChainAccountResponse { get } + var submissionMonitorFactory: ExtrinsicSubmitMonitorFactoryProtocol { get } + var extrinsicOperationFactory: ExtrinsicOperationFactoryProtocol { get } + var signingWrapper: SigningWrapperProtocol { get } + var runtimeService: RuntimeProviderProtocol { get } + var connection: JSONRPCEngine { get } + var operationQueue: OperationQueue { get } + var executionTimeEstimator: AssetExchangeTimeEstimating { get } + var flowState: AssetHubFlowStateProtocol { get } + var logger: LoggerProtocol { get } +} + +final class AssetHubExchangeHost: AssetHubExchangeHostProtocol { + let chain: ChainModel + let selectedAccount: ChainAccountResponse + let flowState: AssetHubFlowStateProtocol + let submissionMonitorFactory: ExtrinsicSubmitMonitorFactoryProtocol + let extrinsicOperationFactory: ExtrinsicOperationFactoryProtocol + let signingWrapper: SigningWrapperProtocol + let runtimeService: RuntimeProviderProtocol + let connection: JSONRPCEngine + let executionTimeEstimator: AssetExchangeTimeEstimating + let operationQueue: OperationQueue + let logger: LoggerProtocol + + init( + chain: ChainModel, + selectedAccount: ChainAccountResponse, + flowState: AssetHubFlowStateProtocol, + submissionMonitorFactory: ExtrinsicSubmitMonitorFactoryProtocol, + extrinsicOperationFactory: ExtrinsicOperationFactoryProtocol, + signingWrapper: SigningWrapperProtocol, + runtimeService: RuntimeProviderProtocol, + connection: JSONRPCEngine, + executionTimeEstimator: AssetExchangeTimeEstimating, + operationQueue: OperationQueue, + logger: LoggerProtocol + ) { + self.chain = chain + self.selectedAccount = selectedAccount + self.flowState = flowState + self.submissionMonitorFactory = submissionMonitorFactory + self.extrinsicOperationFactory = extrinsicOperationFactory + self.signingWrapper = signingWrapper + self.runtimeService = runtimeService + self.connection = connection + self.executionTimeEstimator = executionTimeEstimator + self.operationQueue = operationQueue + self.logger = logger + } +} diff --git a/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubExchangeMetaOperation.swift b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubExchangeMetaOperation.swift new file mode 100644 index 0000000000..080045f30a --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubExchangeMetaOperation.swift @@ -0,0 +1,7 @@ +import Foundation + +final class AssetHubExchangeMetaOperation: AssetExchangeBaseMetaOperation {} + +extension AssetHubExchangeMetaOperation: AssetExchangeMetaOperationProtocol { + var label: AssetExchangeMetaOperationLabel { .swap } +} diff --git a/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubExchangeOperationPrototype.swift b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubExchangeOperationPrototype.swift new file mode 100644 index 0000000000..7129a3394f --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubExchangeOperationPrototype.swift @@ -0,0 +1,26 @@ +import Foundation +import Operation_iOS + +final class AssetHubExchangeOperationPrototype: AssetExchangeBaseOperationPrototype { + let host: AssetHubExchangeHostProtocol + + init(assetIn: ChainAsset, assetOut: ChainAsset, host: AssetHubExchangeHostProtocol) { + self.host = host + + super.init(assetIn: assetIn, assetOut: assetOut) + } +} + +extension AssetHubExchangeOperationPrototype: AssetExchangeOperationPrototypeProtocol { + func estimatedCostInUsdt(using converter: AssetExchageUsdtConverting) throws -> Decimal { + guard let nativeAsset = assetIn.chain.utilityChainAsset() else { + throw ChainModelFetchError.noAsset(assetId: AssetModel.utilityAssetId) + } + + return converter.convertToUsdt(the: nativeAsset, decimalAmount: 0.015) ?? 0 + } + + func estimatedExecutionTimeWrapper() -> CompoundOperationWrapper { + host.executionTimeEstimator.totalTimeWrapper(for: [host.chain.chainId]) + } +} diff --git a/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubFlowState.swift b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubFlowState.swift new file mode 100644 index 0000000000..e0e444572a --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubFlowState.swift @@ -0,0 +1,74 @@ +import Foundation +import SubstrateSdk +import Operation_iOS + +protocol AssetHubFlowStateProtocol { + func setupReQuoteService() -> AssetHubReQuoteService +} + +final class AssetHubFlowState { + let connection: JSONRPCEngine + let runtimeProvider: RuntimeProviderProtocol + let operationQueue: OperationQueue + let notificationsRegistrar: AssetsExchangeStateRegistring? + + let mutex = NSLock() + + private var reQuoteService: AssetHubReQuoteService? + + init( + connection: JSONRPCEngine, + runtimeProvider: RuntimeProviderProtocol, + notificationsRegistrar: AssetsExchangeStateRegistring?, + operationQueue: OperationQueue + ) { + self.connection = connection + self.runtimeProvider = runtimeProvider + self.notificationsRegistrar = notificationsRegistrar + self.operationQueue = operationQueue + } +} + +extension AssetHubFlowState: AssetHubFlowStateProtocol { + func setupReQuoteService() -> AssetHubReQuoteService { + mutex.lock() + + defer { + mutex.unlock() + } + + if let reQuoteService = reQuoteService { + return reQuoteService + } + + let service = AssetHubReQuoteService( + connection: connection, + runtimeProvider: runtimeProvider, + operationQueue: operationQueue + ) + + reQuoteService = service + service.setup() + + notificationsRegistrar?.registerStateService(service) + + return service + } +} + +extension AssetHubFlowState: AssetsExchangeStateProviding { + func throttleStateServices() { + mutex.lock() + + defer { + mutex.unlock() + } + + if let reQuoteService { + notificationsRegistrar?.deregisterStateService(reQuoteService) + reQuoteService.throttle() + } + + reQuoteService = nil + } +} diff --git a/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetsHubExchange.swift b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetsHubExchange.swift new file mode 100644 index 0000000000..647224ae73 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetsHubExchange.swift @@ -0,0 +1,52 @@ +import Foundation +import Operation_iOS +import SubstrateSdk + +final class AssetsHubExchange { + let swapFactory: AssetHubSwapOperationFactoryProtocol + let host: AssetHubExchangeHostProtocol + + init(host: AssetHubExchangeHostProtocol) { + self.host = host + + swapFactory = AssetHubSwapOperationFactory( + chain: host.chain, + runtimeService: host.runtimeService, + connection: host.connection, + operationQueue: host.operationQueue + ) + } + + private func availableDirectSwapConnections( + using swapFactory: AssetHubSwapOperationFactoryProtocol + ) -> CompoundOperationWrapper<[any AssetExchangableGraphEdge]> { + let connectionsWrapper = swapFactory.availableDirections() + + let mappingOperation = ClosureOperation<[any AssetExchangableGraphEdge]> { + let connections = try connectionsWrapper.targetOperation.extractNoCancellableResultData() + + return connections.flatMap { keyValue in + let origin = keyValue.key + + return keyValue.value.map { destination in + AssetHubExchangeEdge( + origin: origin, + destination: destination, + quoteFactory: swapFactory, + host: self.host + ) + } + } + } + + mappingOperation.addDependency(connectionsWrapper.targetOperation) + + return connectionsWrapper.insertingTail(operation: mappingOperation) + } +} + +extension AssetsHubExchange: AssetsExchangeProtocol { + func availableDirectSwapConnections() -> CompoundOperationWrapper<[any AssetExchangableGraphEdge]> { + availableDirectSwapConnections(using: swapFactory) + } +} diff --git a/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetsHubExchangeProvider.swift b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetsHubExchangeProvider.swift new file mode 100644 index 0000000000..5bdac4fc5e --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetsHubExchangeProvider.swift @@ -0,0 +1,159 @@ +import Foundation +import Operation_iOS + +final class AssetsHubExchangeProvider: AssetsExchangeBaseProvider { + private var supportedChains: [ChainModel.Id: ChainModel]? + let wallet: MetaAccountModel + let signingWrapperFactory: SigningWrapperFactoryProtocol + let substrateStorageFacade: StorageFacadeProtocol + let exchangeStateRegistrar: AssetsExchangeStateRegistring + let userStorageFacade: StorageFacadeProtocol + + init( + wallet: MetaAccountModel, + chainRegistry: ChainRegistryProtocol, + pathCostEstimator: AssetsExchangePathCostEstimating, + signingWrapperFactory: SigningWrapperFactoryProtocol, + userStorageFacade: StorageFacadeProtocol, + substrateStorageFacade: StorageFacadeProtocol, + exchangeStateRegistrar: AssetsExchangeStateRegistring, + operationQueue: OperationQueue, + logger: LoggerProtocol + ) { + self.wallet = wallet + self.signingWrapperFactory = signingWrapperFactory + self.userStorageFacade = userStorageFacade + self.substrateStorageFacade = substrateStorageFacade + self.exchangeStateRegistrar = exchangeStateRegistrar + + super.init( + chainRegistry: chainRegistry, + pathCostEstimator: pathCostEstimator, + operationQueue: operationQueue, + syncQueue: DispatchQueue(label: "io.novawallet.assetshubprovider.\(UUID().uuidString)"), + logger: logger + ) + } + + private func updateStateIfNeeded() { + guard let supportedChains else { + return + } + + let exchanges: [AssetsExchangeProtocol] = supportedChains.values.compactMap { chain in + guard + let runtimeService = chainRegistry.getRuntimeProvider(for: chain.chainId), + let connection = chainRegistry.getConnection(for: chain.chainId), + let selectedAccount = wallet.fetch(for: chain.accountRequest()) else { + logger.warning("Wallet, Connection or runtime unavailable for \(chain.name)") + return nil + } + + let serviceFactory = ExtrinsicServiceFactory( + runtimeRegistry: runtimeService, + engine: connection, + operationQueue: operationQueue, + userStorageFacade: userStorageFacade, + substrateStorageFacade: substrateStorageFacade + ) + + let customFeeEstimatingFactory = AssetExchangeFeeEstimatingFactory( + graphProxy: graphProxy, + operationQueue: operationQueue + ) + + let extrinsicOperationFactory = serviceFactory.createOperationFactory( + account: selectedAccount, + chain: chain, + customFeeEstimatingFactory: customFeeEstimatingFactory + ) + + let extrinsicService = serviceFactory.createService( + account: selectedAccount, + chain: chain, + customFeeEstimatingFactory: customFeeEstimatingFactory + ) + + let submissionMonitorFactory = ExtrinsicSubmissionMonitorFactory( + submissionService: extrinsicService, + statusService: ExtrinsicStatusService( + connection: connection, + runtimeProvider: runtimeService, + eventsQueryFactory: BlockEventsQueryFactory(operationQueue: operationQueue, logger: logger) + ), + operationQueue: operationQueue + ) + + let signingWrapper = signingWrapperFactory.createSigningWrapper( + for: selectedAccount.metaId, + accountResponse: selectedAccount + ) + + let flowState = AssetHubFlowState( + connection: connection, + runtimeProvider: runtimeService, + notificationsRegistrar: exchangeStateRegistrar, + operationQueue: operationQueue + ) + + exchangeStateRegistrar.addStateProvider(flowState) + + let host = AssetHubExchangeHost( + chain: chain, + selectedAccount: selectedAccount, + flowState: flowState, + submissionMonitorFactory: submissionMonitorFactory, + extrinsicOperationFactory: extrinsicOperationFactory, + signingWrapper: signingWrapper, + runtimeService: runtimeService, + connection: connection, + executionTimeEstimator: AssetExchangeTimeEstimator(chainRegistry: chainRegistry), + operationQueue: operationQueue, + logger: logger + ) + + return AssetsHubExchange(host: host) + } + + updateState(with: exchanges) + } + + private func handleChains(changes: [DataProviderChange]) -> Bool { + let updatedChains = changes.reduce(into: supportedChains ?? [:]) { accum, change in + switch change { + case let .insert(newItem), let .update(newItem): + accum[newItem.chainId] = newItem.hasSwapHub ? newItem : nil + case let .delete(deletedIdentifier): + accum[deletedIdentifier] = nil + } + } + + guard supportedChains != updatedChains else { + return false + } + + supportedChains = updatedChains + + return true + } + + // MARK: Subsclass + + override func performSetup() { + chainRegistry.chainsSubscribe( + self, + runningInQueue: syncQueue, + filterStrategy: nil + ) { [weak self] changes in + guard let self, handleChains(changes: changes) else { + return + } + + updateStateIfNeeded() + } + } + + override func performThrottle() { + chainRegistry.chainsUnsubscribe(self) + } +} diff --git a/novawallet/Common/Services/AssetExchange/AssetsExchange.swift b/novawallet/Common/Services/AssetExchange/AssetsExchange.swift new file mode 100644 index 0000000000..b89ebe8046 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/AssetsExchange.swift @@ -0,0 +1,6 @@ +import Foundation + +enum AssetsExchange { + static let maxQuotePaths: Int = 4 + static let defaultEdgeWeight: Int = 100 +} diff --git a/novawallet/Common/Services/AssetExchange/AssetsExchangeGraph.swift b/novawallet/Common/Services/AssetExchange/AssetsExchangeGraph.swift new file mode 100644 index 0000000000..9acb3e28b6 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/AssetsExchangeGraph.swift @@ -0,0 +1,51 @@ +import Foundation + +typealias AssetsExchangeGraphModel = GraphModel + +protocol AssetsExchangeGraphProtocol: AnyObject { + func fetchPaths( + from assetIn: ChainAssetId, + to assetOut: ChainAssetId, + maxTopPaths: Int + ) -> [AssetExchangeGraphPath] + + func fetchReachability() -> AssetsExchageGraphReachabilityProtocol +} + +final class AssetsExchangeGraph { + let model: AssetsExchangeGraphModel + let filter: AnyGraphEdgeFilter + + private var cachedReachability: AssetsExchageGraphReachability? + + init(model: AssetsExchangeGraphModel, filter: AnyGraphEdgeFilter) { + self.model = model + self.filter = filter + } +} + +extension AssetsExchangeGraph: AssetsExchangeGraphProtocol { + func fetchPaths( + from assetIn: ChainAssetId, + to assetOut: ChainAssetId, + maxTopPaths: Int + ) -> [AssetExchangeGraphPath] { + model.calculateShortestPath(from: assetIn, nodeEnd: assetOut, topN: maxTopPaths, filter: filter) + } + + func fetchReachability() -> AssetsExchageGraphReachabilityProtocol { + if let cachedReachability { return cachedReachability } + + let allNodes = model.connections.keys + + let mapping = allNodes.reduce(into: [ChainAssetId: Set]()) { accum, assetIn in + accum[assetIn] = model.calculateReachableNodes(for: assetIn, filter: filter) + } + + let reachability = AssetsExchageGraphReachability(mapping: mapping) + + cachedReachability = reachability + + return reachability + } +} diff --git a/novawallet/Common/Services/AssetExchange/AssetsExchangeGraphProvider.swift b/novawallet/Common/Services/AssetExchange/AssetsExchangeGraphProvider.swift new file mode 100644 index 0000000000..ff1c9984d9 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/AssetsExchangeGraphProvider.swift @@ -0,0 +1,184 @@ +import Foundation +import Operation_iOS + +final class AssetsExchangeGraphProvider { + let selectedWallet: MetaAccountModel + let chainRegistry: ChainRegistryProtocol + let feeSupportProvider: AssetsExchangeFeeSupportProviding + let suffiencyProvider: AssetExchangeSufficiencyProviding + let supportedExchangeProviders: [AssetsExchangeProviding] + let operationQueue: OperationQueue + let logger: LoggerProtocol + + private let syncQueue: DispatchQueue + private var edgesRequestPerProvider: [Int: CancellableCallStore] + private var graphPerProvider: [Int: AssetsExchangeGraphModel] = [:] + private var observableState: Observable> = .init( + state: .init(value: nil) + ) + + private var feeSupporting: AssetExchangeFeeSupporting? + + init( + selectedWallet: MetaAccountModel, + chainRegistry: ChainRegistryProtocol, + supportedExchangeProviders: [AssetsExchangeProviding], + feeSupportProvider: AssetsExchangeFeeSupportProviding, + suffiencyProvider: AssetExchangeSufficiencyProviding, + operationQueue: OperationQueue, + logger: LoggerProtocol + ) { + self.selectedWallet = selectedWallet + self.chainRegistry = chainRegistry + self.supportedExchangeProviders = supportedExchangeProviders + self.feeSupportProvider = feeSupportProvider + self.suffiencyProvider = suffiencyProvider + self.operationQueue = operationQueue + self.logger = logger + + edgesRequestPerProvider = supportedExchangeProviders.enumerated().reduce(into: [:]) { + $0[$1.offset] = CancellableCallStore() + } + + syncQueue = DispatchQueue(label: "io.novawallet.exchangegraphprovider.\(UUID().uuidString)") + } + + private func clearCurrentRequests() { + edgesRequestPerProvider.values.forEach { $0.cancel() } + } + + private func updateExchanges( + _ exchanges: [AssetsExchangeProtocol], + providerIndex: Int + ) { + guard let cancellableStore = edgesRequestPerProvider[providerIndex] else { + return + } + + cancellableStore.cancel() + + let edgeWrappers = exchanges.map { $0.availableDirectSwapConnections() } + + let graphOperation = ClosureOperation { + let edges = edgeWrappers + .flatMap { edgeWrapper in + do { + return try edgeWrapper.targetOperation.extractNoCancellableResultData() + } catch { + self.logger.warning("Edge wrapper failed (provider \(providerIndex)): \(error)") + return [] + } + } + .map { AnyAssetExchangeEdge($0) } + + return GraphModelFactory.createFromEdges(edges) + } + + edgeWrappers.forEach { graphOperation.addDependency($0.targetOperation) } + let dependencies = edgeWrappers.flatMap(\.allOperations) + + let totalWrapper = CompoundOperationWrapper(targetOperation: graphOperation, dependencies: dependencies) + + executeCancellable( + wrapper: totalWrapper, + inOperationQueue: operationQueue, + backingCallIn: cancellableStore, + runningCallbackIn: syncQueue + ) { [weak self] result in + guard let self else { + return + } + + switch result { + case let .success(graph): + logger.debug("Did receive graph for provider \(providerIndex).") + + graphPerProvider[providerIndex] = graph + rebuildGraph() + case let .failure(error): + logger.error("Did receive error (provider index \(providerIndex)): \(error).") + } + } + } + + private func rebuildGraph() { + let graphModel: AssetsExchangeGraphModel = graphPerProvider.values.reduce( + AssetsExchangeGraphModel(connections: [:]) + ) { currentGraph, nextGraph in + currentGraph.merging(with: nextGraph) + } + + let filter = AssetExchangePathFilter( + selectedWallet: selectedWallet, + chainRegistry: chainRegistry, + sufficiencyProvider: suffiencyProvider, + feeSupport: feeSupporting ?? CompoundAssetExchangeFeeSupport(supporters: []) + ) + + let graph = AssetsExchangeGraph(model: graphModel, filter: AnyGraphEdgeFilter(filter: filter)) + + supportedExchangeProviders.forEach { $0.inject(graph: graph) } + + observableState.state = .init(value: graph) + } +} + +extension AssetsExchangeGraphProvider: AssetsExchangeGraphProviding { + func setup() { + supportedExchangeProviders.enumerated().forEach { index, provider in + provider.setup() + + provider.subscribeExchanges( + self, + notifyingIn: syncQueue + ) { [weak self] exchanges in + self?.updateExchanges(exchanges, providerIndex: index) + } + } + + feeSupportProvider.setup() + + feeSupportProvider.subscribeFeeSupport( + self, + notifyingIn: syncQueue + ) { [weak self] newState in + self?.feeSupporting = newState + self?.rebuildGraph() + } + } + + func throttle() { + supportedExchangeProviders.forEach { provider in + provider.unsubscribeExchanges(self) + provider.throttle() + } + + feeSupportProvider.unsubscribe(self) + + syncQueue.async { + self.clearCurrentRequests() + } + } + + func subscribeGraph( + _ target: AnyObject, + notifyingIn queue: DispatchQueue, + onChange: @escaping (AssetsExchangeGraphProtocol?) -> Void + ) { + syncQueue.async { [weak self] in + self?.observableState.addObserver( + with: target, + sendStateOnSubscription: true, + queue: queue + ) { _, newState in + onChange(newState.value) + } + } + } + + func unsubscribeGraph(_ target: AnyObject) { + syncQueue.async { [weak self] in + self?.observableState.removeObserver(by: target) + } + } +} diff --git a/novawallet/Common/Services/AssetExchange/AssetsExchangeOperationFactory.swift b/novawallet/Common/Services/AssetExchange/AssetsExchangeOperationFactory.swift new file mode 100644 index 0000000000..7810095ffd --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/AssetsExchangeOperationFactory.swift @@ -0,0 +1,335 @@ +import Foundation +import Operation_iOS + +protocol AssetsExchangeOperationFactoryProtocol { + func createQuoteWrapper(args: AssetConversion.QuoteArgs) -> CompoundOperationWrapper + func createFeeWrapper(for args: AssetExchangeFeeArgs) -> CompoundOperationWrapper + + func createExecutionWrapper( + for fee: AssetExchangeFee, + notifyingIn queue: DispatchQueue, + operationStartClosure: @escaping (Int) -> Void + ) -> CompoundOperationWrapper +} + +enum AssetsExchangeOperationFactoryError: Error { + case noRoute + case feesOperationsMismatch +} + +final class AssetsExchangeOperationFactory { + let graph: AssetsExchangeGraphProtocol + let operationQueue: OperationQueue + let pathCostEstimator: AssetsExchangePathCostEstimating + let maxQuotePaths: Int + let logger: LoggerProtocol + + init( + graph: AssetsExchangeGraphProtocol, + pathCostEstimator: AssetsExchangePathCostEstimating, + maxQuotePaths: Int = AssetsExchange.maxQuotePaths, + operationQueue: OperationQueue, + logger: LoggerProtocol + ) { + self.graph = graph + self.pathCostEstimator = pathCostEstimator + self.operationQueue = operationQueue + self.maxQuotePaths = maxQuotePaths + self.logger = logger + } + + private func createOperationArgs( + for segment: AssetExchangeRouteItem, + routeDirection: AssetConversion.Direction, + slippage: BigRational, + feeAssetId: ChainAssetId, + isFirst: Bool + ) -> AssetExchangeAtomicOperationArgs { + // on the first segment fee paid in configurable asset and further only in assetIn + let feeAssetId = isFirst ? feeAssetId : segment.edge.origin + + return .init( + swapLimit: .init( + direction: routeDirection, + amountIn: segment.amountIn(for: routeDirection), + amountOut: segment.amountOut(for: routeDirection), + slippage: slippage + ), + feeAsset: feeAssetId + ) + } + + private func prepareAtomicOperations( + for route: AssetExchangeRoute, + slippage: BigRational, + feeAssetId: ChainAssetId + ) throws -> [AssetExchangeAtomicOperationProtocol] { + try route.items.reduce([]) { curOperations, segment in + let args = createOperationArgs( + for: segment, + routeDirection: route.direction, + slippage: slippage, + feeAssetId: feeAssetId, + isFirst: curOperations.isEmpty + ) + + if + let lastOperation = curOperations.last, + let newOperation = segment.edge.appendToOperation( + lastOperation, + args: args + ) { + return curOperations.dropLast() + [newOperation] + } else { + let newOperation = try segment.edge.beginOperation(for: args) + return curOperations + [newOperation] + } + } + } + + private func calculateIntermediateFeesInAssetIn( + for operations: [AssetExchangeAtomicOperationProtocol], + operationFees: [AssetExchangeOperationFee] + ) -> CompoundOperationWrapper { + guard operations.count == operationFees.count else { + return .createWithError(AssetsExchangeOperationFactoryError.feesOperationsMismatch) + } + + let segmentsWithFee = zip(operations, operationFees) + + let feeWrapper: CompoundOperationWrapper? = segmentsWithFee.enumerated().reversed().reduce( + nil + ) { prevWrapper, segmentWithFeeIndex in + let index = segmentWithFeeIndex.offset + let segment = segmentWithFeeIndex.element.0 + let segmentFee = segmentWithFeeIndex.element.1 + + let quoteWrapper: CompoundOperationWrapper + if let prevWrapper { + let childWrapper = segment.requiredAmountToGetAmountOut { + try prevWrapper.targetOperation.extractNoCancellableResultData() + } + + childWrapper.addDependency(wrapper: prevWrapper) + + quoteWrapper = childWrapper.insertingHead(operations: prevWrapper.allOperations) + } else { + quoteWrapper = .createWithResult(0) + } + + let mappingOperation = ClosureOperation { + let amountIn = try quoteWrapper.targetOperation.extractNoCancellableResultData() + + if index > 0 { + let totalFee = try segmentFee.totalEnsuringSubmissionAsset( + payerMatcher: .selectedAccount + ) + + return amountIn + totalFee + } else { + return amountIn + } + } + + mappingOperation.addDependency(quoteWrapper.targetOperation) + + return quoteWrapper.insertingTail(operation: mappingOperation) + } + + guard let feeWrapper else { + return .createWithError(AssetsExchangeOperationFactoryError.noRoute) + } + + return feeWrapper + } + + private func estimateExecutionTime( + using path: AssetExchangeGraphPath + ) throws -> CompoundOperationWrapper<[TimeInterval]> { + let prototypes = try createOperationPrototypesFrom(path: path) + + return estimateExecutionTime(for: prototypes) + } + + private func estimateExecutionTime( + for operations: [AssetExchangeOperationPrototypeProtocol] + ) -> CompoundOperationWrapper<[TimeInterval]> { + let wrappers: [CompoundOperationWrapper] = operations.map { operation in + operation.estimatedExecutionTimeWrapper() + } + + let mappingOperation = ClosureOperation<[TimeInterval]> { + try wrappers.map { try $0.targetOperation.extractNoCancellableResultData() } + } + + wrappers.forEach { mappingOperation.addDependency($0.targetOperation) } + + let dependecies = wrappers.flatMap(\.allOperations) + + return CompoundOperationWrapper(targetOperation: mappingOperation, dependencies: dependecies) + } + + private func createMetaOperationsFrom(route: AssetExchangeRoute) throws -> [AssetExchangeMetaOperationProtocol] { + try route.items.reduce([]) { curOperations, segment in + let amountIn = segment.amountIn(for: route.direction) + let amountOut = segment.amountOut(for: route.direction) + + if + let lastOperation = curOperations.last, + let newOperation = try segment.edge.appendToMetaOperation( + lastOperation, + amountIn: amountIn, + amountOut: amountOut + ) { + return curOperations.dropLast() + [newOperation] + } else { + let newOperation = try segment.edge.beginMetaOperation(for: amountIn, amountOut: amountOut) + return curOperations + [newOperation] + } + } + } + + private func createOperationPrototypesFrom( + path: AssetExchangeGraphPath + ) throws -> [AssetExchangeOperationPrototypeProtocol] { + try AssetExchangeOperationPrototypeFactory().createOperationPrototypes( + from: path + ) + } +} + +extension AssetsExchangeOperationFactory: AssetsExchangeOperationFactoryProtocol { + func createQuoteWrapper(args: AssetConversion.QuoteArgs) -> CompoundOperationWrapper { + let routeWrapper = OperationCombiningService.compoundNonOptionalWrapper( + operationQueue: operationQueue + ) { + let paths = self.graph.fetchPaths( + from: args.assetIn, + to: args.assetOut, + maxTopPaths: self.maxQuotePaths + ) + + guard !paths.isEmpty else { + return .createWithResult(nil) + } + + let routeWrapper = AssetsExchangeRouteManager( + possiblePaths: paths, + pathCostEstimator: self.pathCostEstimator, + operationQueue: self.operationQueue, + logger: self.logger + ).fetchRoute(for: args.amount, direction: args.direction) + + return routeWrapper + } + + let executionTimesWrapper = OperationCombiningService<[TimeInterval]>.compoundNonOptionalWrapper( + operationQueue: operationQueue + ) { + guard let route = try routeWrapper.targetOperation.extractNoCancellableResultData() else { + throw AssetsExchangeOperationFactoryError.noRoute + } + + let path = route.items.map(\.edge) + + return try self.estimateExecutionTime(using: path) + } + + executionTimesWrapper.addDependency(wrapper: routeWrapper) + + let mappingOperation = ClosureOperation { + guard let route = try routeWrapper.targetOperation.extractNoCancellableResultData() else { + throw AssetsExchangeOperationFactoryError.noRoute + } + + let metaOperations = try self.createMetaOperationsFrom(route: route) + + let executionTimes = try executionTimesWrapper.targetOperation.extractNoCancellableResultData() + + return AssetExchangeQuote(route: route, metaOperations: metaOperations, executionTimes: executionTimes) + } + + mappingOperation.addDependency(routeWrapper.targetOperation) + mappingOperation.addDependency(executionTimesWrapper.targetOperation) + + return executionTimesWrapper + .insertingHead(operations: routeWrapper.allOperations) + .insertingTail(operation: mappingOperation) + } + + func createFeeWrapper(for args: AssetExchangeFeeArgs) -> CompoundOperationWrapper { + do { + let atomicOperations = try prepareAtomicOperations( + for: args.route, + slippage: args.slippage, + feeAssetId: args.feeAssetId + ) + + let feeWrappers = atomicOperations.map { $0.estimateFee() } + + let intermediateFeesWrapper = OperationCombiningService.compoundNonOptionalWrapper( + operationQueue: operationQueue + ) { + let operationFees = try feeWrappers.map { try $0.targetOperation.extractNoCancellableResultData() } + + return self.calculateIntermediateFeesInAssetIn(for: atomicOperations, operationFees: operationFees) + } + + feeWrappers.forEach { intermediateFeesWrapper.addDependency(wrapper: $0) } + + let mappingOperation = ClosureOperation { + let operationFees = try feeWrappers.map { try $0.targetOperation.extractNoCancellableResultData() } + + let intermediateFees = try intermediateFeesWrapper.targetOperation.extractNoCancellableResultData() + + return AssetExchangeFee( + route: args.route, + operationFees: operationFees, + intermediateFeesInAssetIn: intermediateFees, + slippage: args.slippage, + feeAssetId: args.feeAssetId + ) + } + + feeWrappers.forEach { mappingOperation.addDependency($0.targetOperation) } + mappingOperation.addDependency(intermediateFeesWrapper.targetOperation) + + let dependencies = feeWrappers.flatMap(\.allOperations) + + return intermediateFeesWrapper + .insertingHead(operations: dependencies) + .insertingTail(operation: mappingOperation) + } catch { + return .createWithError(error) + } + } + + func createExecutionWrapper( + for fee: AssetExchangeFee, + notifyingIn queue: DispatchQueue, + operationStartClosure: @escaping (Int) -> Void + ) -> CompoundOperationWrapper { + do { + let atomicOperations = try prepareAtomicOperations( + for: fee.route, + slippage: fee.slippage, + feeAssetId: fee.feeAssetId + ) + + let executionManager = AssetExchangeExecutionManager( + operations: atomicOperations, + fee: fee, + operationQueue: operationQueue, + operationStartClosure: operationStartClosure, + notificationQueue: queue, + logger: logger + ) + + let operation = LongrunOperation(longrun: AnyLongrun(longrun: executionManager)) + + return CompoundOperationWrapper(targetOperation: operation) + } catch { + return .createWithError(error) + } + } +} diff --git a/novawallet/Common/Services/AssetExchange/AssetsExchangePathCostEstimator.swift b/novawallet/Common/Services/AssetExchange/AssetsExchangePathCostEstimator.swift new file mode 100644 index 0000000000..322071a514 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/AssetsExchangePathCostEstimator.swift @@ -0,0 +1,74 @@ +import Foundation +import Operation_iOS + +protocol AssetsExchangePathCostEstimating: AnyObject { + func costEstimationWrapper( + for path: AssetExchangeGraphPath + ) -> CompoundOperationWrapper +} + +struct AssetsExchangePathCost { + let amountInAssetIn: Balance + let amountInAssetOut: Balance + + static var zero: AssetsExchangePathCost { + AssetsExchangePathCost(amountInAssetIn: 0, amountInAssetOut: 0) + } +} + +final class AssetsExchangePathCostEstimator { + let priceStore: AssetExchangePriceStoring + let chainRegistry: ChainRegistryProtocol + + init(priceStore: AssetExchangePriceStoring, chainRegistry: ChainRegistryProtocol) { + self.priceStore = priceStore + self.chainRegistry = chainRegistry + } +} + +extension AssetsExchangePathCostEstimator: AssetsExchangePathCostEstimating { + func costEstimationWrapper( + for path: AssetExchangeGraphPath + ) -> CompoundOperationWrapper { + let operation = ClosureOperation { + guard let usdtTiedAsset = self.chainRegistry.getChain( + for: KnowChainId.statemint + )?.chainAssetForSymbol("USDT") else { + return .zero + } + + let usdtConverter = AssetExchageUsdtConverter( + priceStore: self.priceStore, + usdtTiedAsset: usdtTiedAsset.chainAssetId + ) + + let operations = try AssetExchangeOperationPrototypeFactory().createOperationPrototypes(from: path) + + let totalCostInUsdt = try operations.reduce(Decimal(0)) { total, operation in + let estimatedCostInUsdt = try operation.estimatedCostInUsdt(using: usdtConverter) + + return total + estimatedCostInUsdt + } + + guard + let assetIn = operations.first?.assetIn, + let assetOut = operations.last?.assetOut else { + return .zero + } + + let assetInCost = usdtConverter.convertToAssetInPlankFromUsdt( + amount: totalCostInUsdt, + asset: assetIn + ) ?? .zero + + let assetOutCost = usdtConverter.convertToAssetInPlankFromUsdt( + amount: totalCostInUsdt, + asset: assetOut + ) ?? .zero + + return AssetsExchangePathCost(amountInAssetIn: assetInCost, amountInAssetOut: assetOutCost) + } + + return CompoundOperationWrapper(targetOperation: operation) + } +} diff --git a/novawallet/Common/Services/AssetExchange/AssetsExchangeProtocol.swift b/novawallet/Common/Services/AssetExchange/AssetsExchangeProtocol.swift new file mode 100644 index 0000000000..8ecbc14d9a --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/AssetsExchangeProtocol.swift @@ -0,0 +1,64 @@ +import Foundation +import Operation_iOS + +protocol AssetsExchangeProtocol { + func availableDirectSwapConnections() -> CompoundOperationWrapper<[any AssetExchangableGraphEdge]> +} + +protocol AssetsExchangeProviding: AnyObject { + func setup() + func throttle() + + func subscribeExchanges( + _ target: AnyObject, + notifyingIn queue: DispatchQueue, + onChange: @escaping ([AssetsExchangeProtocol]) -> Void + ) + + func unsubscribeExchanges(_ target: AnyObject) + + func inject(graph: AssetsExchangeGraphProtocol) +} + +protocol AssetsExchangeGraphProviding: AnyObject { + func setup() + func throttle() + + func subscribeGraph( + _ target: AnyObject, + notifyingIn queue: DispatchQueue, + onChange: @escaping (AssetsExchangeGraphProtocol?) -> Void + ) + + func unsubscribeGraph(_ target: AnyObject) +} + +extension AssetsExchangeGraphProviding { + func asyncWaitGraphWrapper( + using workingQueue: DispatchQueue = .global() + ) -> CompoundOperationWrapper { + let subscriber = NSObject() + + let operation = AsyncClosureOperation( + operationClosure: { [weak self] completion in + self?.subscribeGraph( + subscriber, + notifyingIn: workingQueue + ) { graph in + self?.unsubscribeGraph(subscriber) + + guard let graph else { + return + } + + completion(.success(graph)) + } + }, + cancelationClosure: { [weak self] in + self?.unsubscribeGraph(subscriber) + } + ) + + return CompoundOperationWrapper(targetOperation: operation) + } +} diff --git a/novawallet/Common/Services/AssetExchange/AssetsExchangeRouteManager.swift b/novawallet/Common/Services/AssetExchange/AssetsExchangeRouteManager.swift new file mode 100644 index 0000000000..646fd51fd7 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/AssetsExchangeRouteManager.swift @@ -0,0 +1,136 @@ +import Foundation +import Operation_iOS + +final class AssetsExchangeRouteManager { + struct AssetExchangeRouteWithCost { + let route: AssetExchangeRoute + let additionalEstimatedCost: AssetsExchangePathCost + } + + let possiblePaths: [AssetExchangeGraphPath] + let pathCostEstimator: AssetsExchangePathCostEstimating + let operationQueue: OperationQueue + let logger: LoggerProtocol + + init( + possiblePaths: [AssetExchangeGraphPath], + pathCostEstimator: AssetsExchangePathCostEstimating, + operationQueue: OperationQueue, + logger: LoggerProtocol + ) { + self.possiblePaths = possiblePaths + self.pathCostEstimator = pathCostEstimator + self.operationQueue = operationQueue + self.logger = logger + } + + private func createQuote( + for path: AssetExchangeGraphPath, + amount: Balance, + direction: AssetConversion.Direction + ) -> CompoundOperationWrapper { + let wrappers: [CompoundOperationWrapper] + wrappers = path.quoteIteration(for: direction).reduce([]) { prevWrappers, item in + let prevWrapper = prevWrappers.last + + let quoteWrapper: CompoundOperationWrapper = OperationCombiningService.compoundNonOptionalWrapper( + operationManager: OperationManager(operationQueue: operationQueue) + ) { + let prevRouteItem = try prevWrapper?.targetOperation.extractNoCancellableResultData() + + let wrapper = item.quote(amount: prevRouteItem?.quote ?? amount, direction: direction) + + return wrapper + } + + if let prevWrapper { + quoteWrapper.addDependency(wrapper: prevWrapper) + } + + let mappingOperation = ClosureOperation { + let quote = try quoteWrapper.targetOperation.extractNoCancellableResultData() + let prevQuoteItem = try prevWrapper?.targetOperation.extractNoCancellableResultData() + + return AssetExchangeRouteItem( + edge: item, + amount: prevQuoteItem?.quote ?? amount, + quote: quote + ) + } + + mappingOperation.addDependency(quoteWrapper.targetOperation) + + let totalWrapper = quoteWrapper.insertingTail(operation: mappingOperation) + + return prevWrappers + [totalWrapper] + } + + let mappingOperation = ClosureOperation { + let initRoute = AssetExchangeRoute(items: [], amount: amount, direction: direction) + + return try wrappers.reduce(initRoute) { route, wrapper in + let item = try wrapper.targetOperation.extractNoCancellableResultData() + + return route.byAddingNext(item: item) + } + } + + wrappers.forEach { mappingOperation.addDependency($0.targetOperation) } + + let dependencies = wrappers.flatMap(\.allOperations) + + return CompoundOperationWrapper(targetOperation: mappingOperation, dependencies: dependencies) + } +} + +extension AssetsExchangeRouteManager { + func fetchRoute( + for amount: Balance, + direction: AssetConversion.Direction + ) -> CompoundOperationWrapper { + let routeWithCostWrappers = possiblePaths.map { path in + let routeWrapper = createQuote(for: path, amount: amount, direction: direction) + let costWrapper = pathCostEstimator.costEstimationWrapper(for: path) + + return (routeWrapper, costWrapper) + } + + let winnerCalculator = ClosureOperation { + let exchangeRoutes: [AssetExchangeRouteWithCost] = routeWithCostWrappers.compactMap { pairWrappers in + do { + let route = try pairWrappers.0.targetOperation.extractNoCancellableResultData() + let cost = try pairWrappers.1.targetOperation.extractNoCancellableResultData() + + return AssetExchangeRouteWithCost(route: route, additionalEstimatedCost: cost) + } catch { + return nil + } + } + + switch direction { + case .sell: + return exchangeRoutes.max { res1, res2 in + let value1 = res1.route.quote.subtractOrZero(res1.additionalEstimatedCost.amountInAssetOut) + let value2 = res2.route.quote.subtractOrZero(res2.additionalEstimatedCost.amountInAssetOut) + + return value1 < value2 + }?.route + case .buy: + return exchangeRoutes.min { res1, res2 in + let value1 = res1.route.quote + res1.additionalEstimatedCost.amountInAssetIn + let value2 = res2.route.quote + res2.additionalEstimatedCost.amountInAssetIn + + return value1 < value2 + }?.route + } + } + + let dependencies = routeWithCostWrappers.flatMap { routeWithCostWrapper in + routeWithCostWrapper.0.allOperations + routeWithCostWrapper.1.allOperations + } + + dependencies.forEach { winnerCalculator.addDependency($0) } + + return CompoundOperationWrapper(targetOperation: winnerCalculator, dependencies: dependencies) + } +} diff --git a/novawallet/Common/Services/AssetExchange/AssetsExchangeService.swift b/novawallet/Common/Services/AssetExchange/AssetsExchangeService.swift new file mode 100644 index 0000000000..c228979488 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/AssetsExchangeService.swift @@ -0,0 +1,177 @@ +import Foundation +import Operation_iOS + +protocol AssetsExchangeServiceProtocol: ApplicationServiceProtocol { + func subscribeUpdates(for target: AnyObject, notifyingIn queue: DispatchQueue, closure: @escaping () -> Void) + func unsubscribeUpdates(for target: AnyObject) + + func fetchReachibilityWrapper() -> CompoundOperationWrapper + func fetchQuoteWrapper(for args: AssetConversion.QuoteArgs) -> CompoundOperationWrapper + func estimateFee(for args: AssetExchangeFeeArgs) -> CompoundOperationWrapper + func canPayFee(in asset: ChainAsset) -> CompoundOperationWrapper + + func submit( + using estimation: AssetExchangeFee, + notifyingIn queue: DispatchQueue, + operationStartClosure: @escaping (Int) -> Void + ) -> CompoundOperationWrapper + + func subscribeRequoteService( + for target: AnyObject, + ignoreIfAlreadyAdded: Bool, + notifyingIn queue: DispatchQueue, + closure: @escaping () -> Void + ) + + func throttleRequoteService() +} + +enum AssetsExchangeServiceError: Error { + case noRoute +} + +final class AssetsExchangeService { + let exchangesStateMediator: AssetsExchangeStateManaging + let graphProvider: AssetsExchangeGraphProviding + let feeSupportProvider: AssetsExchangeFeeSupportProviding + let pathCostEstimator: AssetsExchangePathCostEstimating + let operationQueue: OperationQueue + let logger: LoggerProtocol + + init( + graphProvider: AssetsExchangeGraphProviding, + feeSupportProvider: AssetsExchangeFeeSupportProviding, + exchangesStateMediator: AssetsExchangeStateManaging, + pathCostEstimator: AssetsExchangePathCostEstimating, + operationQueue: OperationQueue, + logger: LoggerProtocol + ) { + self.graphProvider = graphProvider + self.feeSupportProvider = feeSupportProvider + self.exchangesStateMediator = exchangesStateMediator + self.pathCostEstimator = pathCostEstimator + self.operationQueue = operationQueue + self.logger = logger + } + + private func prepareWrapper( + for factoryClosure: @escaping (AssetsExchangeOperationFactoryProtocol) -> CompoundOperationWrapper + ) -> CompoundOperationWrapper { + let graphWrapper = graphProvider.asyncWaitGraphWrapper() + + let targetWrapper = OperationCombiningService.compoundNonOptionalWrapper( + operationQueue: operationQueue + ) { + let graph = try graphWrapper.targetOperation.extractNoCancellableResultData() + + let operationFactory = AssetsExchangeOperationFactory( + graph: graph, + pathCostEstimator: self.pathCostEstimator, + operationQueue: self.operationQueue, + logger: self.logger + ) + + return factoryClosure(operationFactory) + } + + targetWrapper.addDependency(wrapper: graphWrapper) + + return targetWrapper.insertingHead(operations: graphWrapper.allOperations) + } +} + +extension AssetsExchangeService: AssetsExchangeServiceProtocol { + func setup() { + graphProvider.setup() + feeSupportProvider.setup() + } + + func throttle() { + graphProvider.throttle() + feeSupportProvider.throttle() + } + + func subscribeUpdates(for target: AnyObject, notifyingIn queue: DispatchQueue, closure: @escaping () -> Void) { + graphProvider.subscribeGraph( + target, + notifyingIn: queue + ) { _ in + closure() + } + } + + func unsubscribeUpdates(for target: AnyObject) { + graphProvider.unsubscribeGraph(target) + } + + func fetchReachibilityWrapper() -> CompoundOperationWrapper { + let graphWrapper = graphProvider.asyncWaitGraphWrapper() + + let directionsOperation = ClosureOperation { + let graph = try graphWrapper.targetOperation.extractNoCancellableResultData() + + return graph.fetchReachability() + } + + directionsOperation.addDependency(graphWrapper.targetOperation) + + return graphWrapper.insertingTail(operation: directionsOperation) + } + + func fetchQuoteWrapper(for args: AssetConversion.QuoteArgs) -> CompoundOperationWrapper { + prepareWrapper { operationFactory in + operationFactory.createQuoteWrapper(args: args) + } + } + + func estimateFee(for args: AssetExchangeFeeArgs) -> CompoundOperationWrapper { + prepareWrapper { $0.createFeeWrapper(for: args) } + } + + func submit( + using estimation: AssetExchangeFee, + notifyingIn queue: DispatchQueue, + operationStartClosure: @escaping (Int) -> Void + ) -> CompoundOperationWrapper { + prepareWrapper { + $0.createExecutionWrapper( + for: estimation, + notifyingIn: queue, + operationStartClosure: operationStartClosure + ) + } + } + + func subscribeRequoteService( + for target: AnyObject, + ignoreIfAlreadyAdded: Bool, + notifyingIn queue: DispatchQueue, + closure: @escaping () -> Void + ) { + exchangesStateMediator.subscribeStateChanges( + target, + ignoreIfAlreadyAdded: ignoreIfAlreadyAdded, + notifyingIn: queue, + closure: closure + ) + } + + func throttleRequoteService() { + exchangesStateMediator.throttleStateServicesSynchroniously() + } + + func canPayFee(in asset: ChainAsset) -> CompoundOperationWrapper { + guard !asset.isUtilityAsset else { + return CompoundOperationWrapper.createWithResult(true) + } + + let operation = AsyncClosureOperation(operationClosure: { completionClosure in + self.feeSupportProvider.fetchCurrentState(in: .global()) { state in + let isFeeSupported = state?.canPayFee(inNonNative: asset) ?? false + completionClosure(.success(isFeeSupported)) + } + }) + + return CompoundOperationWrapper(targetOperation: operation) + } +} diff --git a/novawallet/Common/Services/AssetExchange/AssetsExchangeStateMediator.swift b/novawallet/Common/Services/AssetExchange/AssetsExchangeStateMediator.swift new file mode 100644 index 0000000000..5295f03bc1 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/AssetsExchangeStateMediator.swift @@ -0,0 +1,102 @@ +import Foundation + +protocol AssetsExchangeStateProviding: AnyObject { + func throttleStateServices() +} + +protocol AssetsExchangeStateRegistring: AnyObject { + func addStateProvider(_ provider: AssetsExchangeStateProviding) + func registerStateService(_ service: ObservableSyncServiceProtocol) + func deregisterStateService(_ service: ObservableSyncServiceProtocol) +} + +protocol AssetsExchangeStateManaging: AnyObject { + func subscribeStateChanges( + _ target: AnyObject, + ignoreIfAlreadyAdded: Bool, + notifyingIn queue: DispatchQueue, + closure: @escaping () -> Void + ) + + func throttleStateServicesSynchroniously() +} + +typealias AssetsExchangeStateMediating = AssetsExchangeStateManaging & AssetsExchangeStateRegistring + +final class AssetsExchangeStateMediator { + private var stateProviders: [WeakWrapper] = [] + private var observers: [WeakObserver] = [] + + private let syncQueue: DispatchQueue = .init(label: "io.novawallet.assetexchangestatemediator.\(UUID().uuidString)") + + private func notifyObservers() { + observers.forEach { observer in + if observer.target != nil { + dispatchInQueueWhenPossible(observer.notificationQueue, block: observer.closure) + } + } + } + + private func addObserver(to service: ObservableSyncServiceProtocol) { + observers.clearEmptyItems() + + service.subscribeSyncState( + self, + queue: syncQueue + ) { [weak self] wasSyncing, isSyncing in + if wasSyncing, !isSyncing { + self?.notifyObservers() + } + } + } +} + +extension AssetsExchangeStateMediator: AssetsExchangeStateRegistring { + func addStateProvider(_ provider: AssetsExchangeStateProviding) { + syncQueue.async { + self.stateProviders.append(.init(target: provider)) + } + } + + func registerStateService(_ service: ObservableSyncServiceProtocol) { + if !service.hasSubscription(for: self) { + service.subscribeSyncState(self, queue: syncQueue) { [weak self] wasSyncing, isSyncing in + if wasSyncing, !isSyncing { + self?.notifyObservers() + } + } + } + } + + func deregisterStateService(_ service: ObservableSyncServiceProtocol) { + service.unsubscribeSyncState(self) + } +} + +extension AssetsExchangeStateMediator: AssetsExchangeStateManaging { + func subscribeStateChanges( + _ target: AnyObject, + ignoreIfAlreadyAdded: Bool, + notifyingIn queue: DispatchQueue, + closure: @escaping () -> Void + ) { + syncQueue.async { + if ignoreIfAlreadyAdded, self.observers.contains(where: { $0.target === target }) { + return + } + + self.observers = self.observers.filter { $0.target !== target || $0.target == nil } + self.observers.append(.init(target: target, notificationQueue: queue, closure: closure)) + } + } + + func throttleStateServicesSynchroniously() { + syncQueue.sync { + self.stateProviders.forEach { wrapper in + if let provider = wrapper.target as? AssetsExchangeStateProviding { + provider.throttleStateServices() + } + } + } + } +} diff --git a/novawallet/Common/Services/AssetExchange/Common/AnyAssetExchangeEdge.swift b/novawallet/Common/Services/AssetExchange/Common/AnyAssetExchangeEdge.swift new file mode 100644 index 0000000000..b96a0ac4dd --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/Common/AnyAssetExchangeEdge.swift @@ -0,0 +1,108 @@ +import Foundation +import Operation_iOS + +class AnyAssetExchangeEdge { + let identifier = UUID() + + private let fetchWeight: () -> Int + private let fetchOrigin: () -> ChainAssetId + private let fetchDestination: () -> ChainAssetId + private let fetchQuote: (Balance, AssetConversion.Direction) -> CompoundOperationWrapper + private let beginOperationClosure: (AssetExchangeAtomicOperationArgs) throws -> AssetExchangeAtomicOperationProtocol + private let appendToOperationClosure: ( + AssetExchangeAtomicOperationProtocol, + AssetExchangeAtomicOperationArgs + ) -> AssetExchangeAtomicOperationProtocol? + + private let shouldIgnoreFeeRequirementClosure: (any AssetExchangableGraphEdge) -> Bool + private let canPayFeesInIntermediatePositionClosure: () -> Bool + private let typeClosure: () -> AssetExchangeEdgeType + + private let beginMetaOperationClosure: (Balance, Balance) throws -> AssetExchangeMetaOperationProtocol + + private let appendToMetaOperationClosure: (AssetExchangeMetaOperationProtocol, Balance, Balance) + throws -> AssetExchangeMetaOperationProtocol? + + private let beginOperationPrototypeClosure: () throws -> AssetExchangeOperationPrototypeProtocol + + private let appendToOperationPrototypeClosure: (AssetExchangeOperationPrototypeProtocol) throws + -> AssetExchangeOperationPrototypeProtocol? + + init(_ edge: any AssetExchangableGraphEdge) { + fetchWeight = { edge.weight } + fetchOrigin = { edge.origin } + fetchDestination = { edge.destination } + fetchQuote = edge.quote + beginOperationClosure = edge.beginOperation + appendToOperationClosure = edge.appendToOperation + shouldIgnoreFeeRequirementClosure = edge.shouldIgnoreFeeRequirement + canPayFeesInIntermediatePositionClosure = edge.canPayNonNativeFeesInIntermediatePosition + typeClosure = { edge.type } + beginMetaOperationClosure = edge.beginMetaOperation + appendToMetaOperationClosure = edge.appendToMetaOperation + beginOperationPrototypeClosure = edge.beginOperationPrototype + appendToOperationPrototypeClosure = edge.appendToOperationPrototype + } +} + +extension AnyAssetExchangeEdge: AssetExchangableGraphEdge { + func quote(amount: Balance, direction: AssetConversion.Direction) -> CompoundOperationWrapper { + fetchQuote(amount, direction) + } + + var origin: ChainAssetId { fetchOrigin() } + var destination: ChainAssetId { fetchDestination() } + var weight: Int { fetchWeight() } + var type: AssetExchangeEdgeType { typeClosure() } + + func beginOperation(for args: AssetExchangeAtomicOperationArgs) throws -> AssetExchangeAtomicOperationProtocol { + try beginOperationClosure(args) + } + + func appendToOperation( + _ currentOperation: AssetExchangeAtomicOperationProtocol, + args: AssetExchangeAtomicOperationArgs + ) -> AssetExchangeAtomicOperationProtocol? { + appendToOperationClosure(currentOperation, args) + } + + func shouldIgnoreFeeRequirement(after predecessor: any AssetExchangableGraphEdge) -> Bool { + shouldIgnoreFeeRequirementClosure(predecessor) + } + + func canPayNonNativeFeesInIntermediatePosition() -> Bool { + canPayFeesInIntermediatePositionClosure() + } + + func beginMetaOperation(for amountIn: Balance, amountOut: Balance) throws -> AssetExchangeMetaOperationProtocol { + try beginMetaOperationClosure(amountIn, amountOut) + } + + func appendToMetaOperation( + _ currentOperation: AssetExchangeMetaOperationProtocol, + amountIn: Balance, + amountOut: Balance + ) throws -> AssetExchangeMetaOperationProtocol? { + try appendToMetaOperationClosure(currentOperation, amountIn, amountOut) + } + + func beginOperationPrototype() throws -> AssetExchangeOperationPrototypeProtocol { + try beginOperationPrototypeClosure() + } + + func appendToOperationPrototype( + _ currentPrototype: AssetExchangeOperationPrototypeProtocol + ) throws -> AssetExchangeOperationPrototypeProtocol? { + try appendToOperationPrototypeClosure(currentPrototype) + } +} + +extension AnyAssetExchangeEdge: Hashable { + static func == (lhs: AnyAssetExchangeEdge, rhs: AnyAssetExchangeEdge) -> Bool { + lhs.identifier == rhs.identifier + } + + func hash(into hasher: inout Hasher) { + hasher.combine(identifier) + } +} diff --git a/novawallet/Common/Services/AssetExchange/Common/AssetExchangeAtomicOperation.swift b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeAtomicOperation.swift new file mode 100644 index 0000000000..1271b0bb2b --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeAtomicOperation.swift @@ -0,0 +1,13 @@ +import Foundation +import Operation_iOS + +protocol AssetExchangeAtomicOperationProtocol { + var swapLimit: AssetExchangeSwapLimit { get } + + func executeWrapper(for swapLimit: AssetExchangeSwapLimit) -> CompoundOperationWrapper + func estimateFee() -> CompoundOperationWrapper + + func requiredAmountToGetAmountOut( + _ amountOutClosure: @escaping () throws -> Balance + ) -> CompoundOperationWrapper +} diff --git a/novawallet/Common/Services/AssetExchange/Common/AssetExchangeAtomicOperationArgs.swift b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeAtomicOperationArgs.swift new file mode 100644 index 0000000000..a871289557 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeAtomicOperationArgs.swift @@ -0,0 +1,6 @@ +import Foundation + +struct AssetExchangeAtomicOperationArgs { + let swapLimit: AssetExchangeSwapLimit + let feeAsset: ChainAssetId +} diff --git a/novawallet/Common/Services/AssetExchange/Common/AssetExchangeEdgeType.swift b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeEdgeType.swift new file mode 100644 index 0000000000..42db6385da --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeEdgeType.swift @@ -0,0 +1,7 @@ +import Foundation + +enum AssetExchangeEdgeType: Equatable { + case assetHubSwap + case hydraSwap + case crossChain +} diff --git a/novawallet/Common/Services/AssetExchange/Common/AssetExchangeFee.swift b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeFee.swift new file mode 100644 index 0000000000..33f7f2294f --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeFee.swift @@ -0,0 +1,99 @@ +import Foundation + +struct AssetExchangeFeeArgs { + let route: AssetExchangeRoute + let slippage: BigRational + let feeAssetId: ChainAssetId +} + +enum AssetExchangeFeeError: Error { + case mismatchBetweenFeeAndRoute +} + +struct AssetExchangeFee: Equatable { + let route: AssetExchangeRoute + let operationFees: [AssetExchangeOperationFee] + let intermediateFeesInAssetIn: Balance + let slippage: BigRational + let feeAssetId: ChainAssetId +} + +extension AssetExchangeFee { + func originPostsubmissionFeeInAsset( + _ asset: ChainAsset, + matchingPayer: AssetExchangeFeePayerMatcher = .selectedAccount + ) -> Balance { + guard let originFee = operationFees.first else { + return 0 + } + + return originFee.postSubmissionFee.totalAmountIn( + asset: asset.chainAssetId, + matchingPayer: matchingPayer + ) + } + + func originFeeInAsset( + _ asset: ChainAsset, + matchingPayer: AssetExchangeFeePayerMatcher = .selectedAccount + ) -> Balance { + guard let originFee = operationFees.first else { + return 0 + } + + return originFee.totalAmountIn(asset: asset.chainAssetId, matchingPayer: matchingPayer) + } + + func originExtrinsicFee() -> ExtrinsicFeeProtocol? { + operationFees.first?.submissionFee + } + + func postSubmissionFeeInAssetIn( + _ assetIn: ChainAsset, + matchingPayer: AssetExchangeFeePayerMatcher = .selectedAccount + ) -> Balance { + guard let originFee = operationFees.first else { + return 0 + } + + let originFeeAmount = originFee.postSubmissionFee.totalAmountIn( + asset: assetIn.chainAssetId, + matchingPayer: matchingPayer + ) + + return originFeeAmount + intermediateFeesInAssetIn + } + + func totalFeeInAssetIn( + _ assetIn: ChainAsset, + matchingPayer: AssetExchangeFeePayerMatcher = .selectedAccount + ) -> Balance { + guard let originFee = operationFees.first else { + return 0 + } + + let originFeeAmount = originFee.totalAmountIn( + asset: assetIn.chainAssetId, + matchingPayer: matchingPayer + ) + + return originFeeAmount + intermediateFeesInAssetIn + } + + var hasOriginPostSubmissionByAccount: Bool { + guard let originFee = operationFees.first else { + return false + } + + return !originFee.postSubmissionFee.paidByAccount.isEmpty + } + + func calculateTotalFeeInFiat( + matching operations: [AssetExchangeMetaOperationProtocol], + priceStore: AssetExchangePriceStoring + ) -> Decimal { + zip(operations, operationFees).map { operation, fee in + fee.totalInFiat(in: operation.assetIn.chain, priceStore: priceStore) + }.reduce(Decimal(0)) { $0 + $1 } + } +} diff --git a/novawallet/Common/Services/AssetExchange/Common/AssetExchangeFeePayerMatcher.swift b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeFeePayerMatcher.swift new file mode 100644 index 0000000000..68ae9cf965 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeFeePayerMatcher.swift @@ -0,0 +1,18 @@ +import Foundation + +enum AssetExchangeFeePayerMatcher { + case selectedAccount + case anyAccount + case givenAccount(AccountId) + + func matches(payer: ExtrinsicFeePayer?) -> Bool { + switch self { + case .selectedAccount: + payer == nil + case .anyAccount: + true + case let .givenAccount(accountId): + payer?.accountId == accountId + } + } +} diff --git a/novawallet/Common/Services/AssetExchange/Common/AssetExchangeGraphEdge.swift b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeGraphEdge.swift new file mode 100644 index 0000000000..fdc6cfe76c --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeGraphEdge.swift @@ -0,0 +1,31 @@ +import Foundation +import Operation_iOS + +protocol AssetExchangableGraphEdge: GraphQuotableEdge { + func beginOperation(for args: AssetExchangeAtomicOperationArgs) throws -> AssetExchangeAtomicOperationProtocol + + func appendToOperation( + _ currentOperation: AssetExchangeAtomicOperationProtocol, + args: AssetExchangeAtomicOperationArgs + ) -> AssetExchangeAtomicOperationProtocol? + + func shouldIgnoreFeeRequirement(after predecessor: any AssetExchangableGraphEdge) -> Bool + + func canPayNonNativeFeesInIntermediatePosition() -> Bool + + var type: AssetExchangeEdgeType { get } + + func beginMetaOperation(for amountIn: Balance, amountOut: Balance) throws -> AssetExchangeMetaOperationProtocol + + func appendToMetaOperation( + _ currentOperation: AssetExchangeMetaOperationProtocol, + amountIn: Balance, + amountOut: Balance + ) throws -> AssetExchangeMetaOperationProtocol? + + func beginOperationPrototype() throws -> AssetExchangeOperationPrototypeProtocol + + func appendToOperationPrototype( + _ currentPrototype: AssetExchangeOperationPrototypeProtocol + ) throws -> AssetExchangeOperationPrototypeProtocol? +} diff --git a/novawallet/Common/Services/AssetExchange/Common/AssetExchangeGraphPath.swift b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeGraphPath.swift new file mode 100644 index 0000000000..33f7d64651 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeGraphPath.swift @@ -0,0 +1,3 @@ +import Foundation + +typealias AssetExchangeGraphPath = [AnyAssetExchangeEdge] diff --git a/novawallet/Common/Services/AssetExchange/Common/AssetExchangeMetaOperationProtocol.swift b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeMetaOperationProtocol.swift new file mode 100644 index 0000000000..cf39e758b7 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeMetaOperationProtocol.swift @@ -0,0 +1,28 @@ +import Foundation + +enum AssetExchangeMetaOperationLabel: Equatable { + case swap + case transfer +} + +protocol AssetExchangeMetaOperationProtocol { + var assetIn: ChainAsset { get } + var assetOut: ChainAsset { get } + var amountIn: Balance { get } + var amountOut: Balance { get } + var label: AssetExchangeMetaOperationLabel { get } +} + +class AssetExchangeBaseMetaOperation { + let assetIn: ChainAsset + let assetOut: ChainAsset + let amountIn: Balance + let amountOut: Balance + + init(assetIn: ChainAsset, assetOut: ChainAsset, amountIn: Balance, amountOut: Balance) { + self.assetIn = assetIn + self.assetOut = assetOut + self.amountIn = amountIn + self.amountOut = amountOut + } +} diff --git a/novawallet/Common/Services/AssetExchange/Common/AssetExchangeOperationFee.swift b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeOperationFee.swift new file mode 100644 index 0000000000..f1538f15a3 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeOperationFee.swift @@ -0,0 +1,283 @@ +import Foundation +import BigInt + +enum AssetExchangeOperationFeeError: Error { + case assetMismatch + case payerMismatch +} + +struct AssetExchangeOperationFee: Equatable { + struct Amount: Equatable { + let amount: Balance + let asset: ChainAssetId + + func totalAmountEnsuring(asset: ChainAssetId) throws -> Balance { + guard self.asset == asset else { + throw AssetExchangeOperationFeeError.assetMismatch + } + + return amount + } + + func totalAmountIn(asset: ChainAssetId) -> Balance { + self.asset == asset ? amount : 0 + } + + func addAmount(to store: inout [ChainAssetId: Balance]) { + store[asset] = (store[asset] ?? 0) + amount + } + } + + struct Submission: Equatable { + let amountWithAsset: Amount + + // nil means selected account pays fee + let payer: ExtrinsicFeePayer? + + let weight: BigUInt + + func totalAmountEnsuring( + asset: ChainAssetId, + matchingPayer: AssetExchangeFeePayerMatcher + ) throws -> Balance { + guard matchingPayer.matches(payer: payer) else { + throw AssetExchangeOperationFeeError.payerMismatch + } + + return try amountWithAsset.totalAmountEnsuring(asset: asset) + } + + func totalAmountIn( + asset: ChainAssetId, + matchingPayer: AssetExchangeFeePayerMatcher + ) -> Balance { + guard matchingPayer.matches(payer: payer) else { + return 0 + } + + return amountWithAsset.totalAmountIn(asset: asset) + } + + func addAmount(to store: inout [ChainAssetId: Balance]) { + amountWithAsset.addAmount(to: &store) + } + } + + struct AmountByPayer: Equatable { + let amountWithAsset: Amount + + let payer: ExtrinsicFeePayer? + + func totalAmountEnsuring( + asset: ChainAssetId, + matchingPayer: AssetExchangeFeePayerMatcher + ) throws -> Balance { + guard matchingPayer.matches(payer: payer) else { + throw AssetExchangeOperationFeeError.payerMismatch + } + + return try amountWithAsset.totalAmountEnsuring(asset: asset) + } + + func totalAmountIn( + asset: ChainAssetId, + matchingPayer: AssetExchangeFeePayerMatcher + ) -> Balance { + guard matchingPayer.matches(payer: payer) else { + return 0 + } + + return amountWithAsset.totalAmountIn(asset: asset) + } + + func addAmount(to store: inout [ChainAssetId: Balance]) { + amountWithAsset.addAmount(to: &store) + } + } + + struct PostSubmission: Equatable { + /** + * Post-submission fees paid by (some) origin account. + * This is typed as `AmountByAccount` as those fee might still + * use different accounts (e.g. delivery fees are always paid from requested account) + */ + let paidByAccount: [AmountByPayer] + + /** + * Post-submission fees paid from swapping amount directly. Its payment is isolated + * and does not involve any withdrawals from accounts + */ + let paidFromAmount: [Amount] + + func totalByAccountEnsuring( + asset: ChainAssetId, + matchingPayer: AssetExchangeFeePayerMatcher + ) throws -> Balance { + try paidByAccount.reduce(0) { total, item in + let current = try item.totalAmountEnsuring( + asset: asset, + matchingPayer: matchingPayer + ) + + return total + current + } + } + + func totalByAccountAmountIn( + asset: ChainAssetId, + matchingPayer: AssetExchangeFeePayerMatcher + ) -> Balance { + paidByAccount.reduce(0) { total, item in + total + item.totalAmountIn(asset: asset, matchingPayer: matchingPayer) + } + } + + func totalFromAmountEnsuring(asset: ChainAssetId) throws -> Balance { + try paidFromAmount.reduce(0) { total, item in + let current = try item.totalAmountEnsuring(asset: asset) + + return total + current + } + } + + func totalFromAmountIn(asset: ChainAssetId) -> Balance { + paidFromAmount.reduce(0) { total, item in + total + item.totalAmountIn(asset: asset) + } + } + + func totalAmountEnsuring( + asset: ChainAssetId, + matchingPayer: AssetExchangeFeePayerMatcher + ) throws -> Balance { + let totalByAccount = try totalByAccountEnsuring( + asset: asset, + matchingPayer: matchingPayer + ) + + let totalFromAmount = try totalFromAmountEnsuring(asset: asset) + + return totalByAccount + totalFromAmount + } + + func totalAmountIn( + asset: ChainAssetId, + matchingPayer: AssetExchangeFeePayerMatcher + ) -> Balance { + let totalByAccount = totalByAccountAmountIn( + asset: asset, + matchingPayer: matchingPayer + ) + + let totalFromAmount = totalFromAmountIn(asset: asset) + + return totalByAccount + totalFromAmount + } + + func addAmount(to store: inout [ChainAssetId: Balance]) { + paidByAccount.forEach { $0.amountWithAsset.addAmount(to: &store) } + paidFromAmount.forEach { $0.addAmount(to: &store) } + } + } + + /** + * Fee that is paid when submitting transaction + */ + let submissionFee: Submission + + /** + * Fee that is paid after transaction started execution on-chain. For example, delivery fee for the crosschain + */ + let postSubmissionFee: PostSubmission +} + +extension AssetExchangeOperationFee { + func totalAmountToPayFromSelectedAccount() throws -> Balance { + let asset = submissionFee.amountWithAsset.asset + + let submissionByAccount = try submissionFee.totalAmountEnsuring( + asset: asset, + matchingPayer: .selectedAccount + ) + + let postSubmissionByAccount = try postSubmissionFee.totalByAccountEnsuring( + asset: asset, + matchingPayer: .selectedAccount + ) + + return submissionByAccount + postSubmissionByAccount + } + + func totalToPayFromAmountEnsuring(asset: ChainAssetId) throws -> Balance { + try postSubmissionFee.totalFromAmountEnsuring(asset: asset) + } + + func totalEnsuringSubmissionAsset(payerMatcher: AssetExchangeFeePayerMatcher) throws -> Balance { + let asset = submissionFee.amountWithAsset.asset + + let submissionTotal = try submissionFee.totalAmountEnsuring( + asset: asset, + matchingPayer: payerMatcher + ) + + let postSubmissionTotal = try postSubmissionFee.totalAmountEnsuring( + asset: asset, + matchingPayer: payerMatcher + ) + + return submissionTotal + postSubmissionTotal + } + + func totalAmountIn( + asset: ChainAssetId, + matchingPayer: AssetExchangeFeePayerMatcher + ) -> Balance { + let submissionTotal = submissionFee.totalAmountIn( + asset: asset, + matchingPayer: matchingPayer + ) + + let postSubmissionTotal = postSubmissionFee.totalAmountIn( + asset: asset, + matchingPayer: matchingPayer + ) + + return submissionTotal + postSubmissionTotal + } + + func groupedAmountByAsset() -> [ChainAssetId: Balance] { + var store: [ChainAssetId: Balance] = [:] + + submissionFee.addAmount(to: &store) + postSubmissionFee.addAmount(to: &store) + + return store + } + + func totalInFiat( + in chain: ChainModel, + priceStore: AssetExchangePriceStoring + ) -> Decimal { + let amounts = groupedAmountByAsset() + + return amounts + .map { keyValue in + guard + keyValue.key.chainId == chain.chainId, + let chainAssetInfo = chain.chainAsset(for: keyValue.key.assetId)?.assetDisplayInfo else { + return 0 + } + + return Decimal.fiatValue( + from: keyValue.value, + price: priceStore.fetchPrice(for: keyValue.key), + precision: chainAssetInfo.assetPrecision + ) + } + .reduce(Decimal(0)) { $1 + $0 } + } +} + +extension AssetExchangeOperationFee.Submission: ExtrinsicFeeProtocol { + var amount: Balance { amountWithAsset.amount } +} diff --git a/novawallet/Common/Services/AssetExchange/Common/AssetExchangeOperationPrototype.swift b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeOperationPrototype.swift new file mode 100644 index 0000000000..5edea6079b --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeOperationPrototype.swift @@ -0,0 +1,21 @@ +import Foundation +import Operation_iOS + +protocol AssetExchangeOperationPrototypeProtocol { + var assetIn: ChainAsset { get } + var assetOut: ChainAsset { get } + + func estimatedCostInUsdt(using converter: AssetExchageUsdtConverting) throws -> Decimal + + func estimatedExecutionTimeWrapper() -> CompoundOperationWrapper +} + +class AssetExchangeBaseOperationPrototype { + let assetIn: ChainAsset + let assetOut: ChainAsset + + init(assetIn: ChainAsset, assetOut: ChainAsset) { + self.assetIn = assetIn + self.assetOut = assetOut + } +} diff --git a/novawallet/Common/Services/AssetExchange/Common/AssetExchangeQuote.swift b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeQuote.swift new file mode 100644 index 0000000000..78a932a9e6 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeQuote.swift @@ -0,0 +1,29 @@ +import Foundation + +struct AssetExchangeQuote { + let route: AssetExchangeRoute + let metaOperations: [AssetExchangeMetaOperationProtocol] + let executionTimes: [TimeInterval] + + func totalExecutionTime() -> TimeInterval { + executionTimes.reduce(0, +) + } + + func totalExecutionTime(from index: Int) -> TimeInterval { + let subarray = if index > 0 { + Array(executionTimes.dropFirst(index)) + } else { + executionTimes + } + + return subarray.reduce(0, +) + } + + func hasSamePath(other: AssetExchangeRoute) -> Bool { + guard route.items.count == other.items.count else { return false } + + return zip(route.items, other.items).allSatisfy { + $0.0.edge.identifier == $0.1.edge.identifier + } + } +} diff --git a/novawallet/Common/Services/AssetExchange/Common/AssetExchangeRoute.swift b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeRoute.swift new file mode 100644 index 0000000000..9d5f55c962 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeRoute.swift @@ -0,0 +1,91 @@ +import Foundation + +struct AssetExchangeRoute: Equatable { + let items: [AssetExchangeRouteItem] + let amount: Balance + let direction: AssetConversion.Direction + + var quote: Balance { + switch direction { + case .sell: + return items.last?.quote ?? amount + case .buy: + return items.first?.quote ?? amount + } + } + + var amountIn: Balance { + items.first?.amountIn(for: direction) ?? amount + } + + var amountOut: Balance { + items.last?.amountOut(for: direction) ?? amount + } + + func byAddingNext(item: AssetExchangeRouteItem) -> AssetExchangeRoute { + switch direction { + case .sell: + return .init(items: items + [item], amount: amount, direction: direction) + case .buy: + return .init(items: [item] + items, amount: amount, direction: direction) + } + } + + func matches(otherRoute: AssetExchangeRoute, slippage: BigRational) -> Bool { + guard direction == otherRoute.direction else { return false } + + switch direction { + case .sell: + let amountOutMin = amountOut - slippage.mul(value: amountOut) + + return amountOutMin <= otherRoute.amountOut + case .buy: + let amountInMax = amountIn + slippage.mul(value: amountIn) + + return amountInMax >= otherRoute.amountIn + } + } +} + +extension AssetExchangeGraphPath { + func quoteIteration(for direction: AssetConversion.Direction) -> AssetExchangeGraphPath { + switch direction { + case .sell: + self + case .buy: + AssetExchangeGraphPath(reversed()) + } + } +} + +struct AssetExchangeRouteItem { + let edge: AnyAssetExchangeEdge + let amount: Balance + let quote: Balance + + func amountIn(for direction: AssetConversion.Direction) -> Balance { + switch direction { + case .sell: + return amount + case .buy: + return quote + } + } + + func amountOut(for direction: AssetConversion.Direction) -> Balance { + switch direction { + case .sell: + return quote + case .buy: + return amount + } + } +} + +extension AssetExchangeRouteItem: Equatable { + static func == (lhs: AssetExchangeRouteItem, rhs: AssetExchangeRouteItem) -> Bool { + lhs.edge.identifier == rhs.edge.identifier && + lhs.amount == rhs.amount && + lhs.quote == rhs.quote + } +} diff --git a/novawallet/Common/Services/AssetExchange/Common/AssetExchangeSufficiencyProvider.swift b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeSufficiencyProvider.swift new file mode 100644 index 0000000000..e9451ef70c --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeSufficiencyProvider.swift @@ -0,0 +1,16 @@ +import Foundation + +protocol AssetExchangeSufficiencyProviding { + func isSufficient(chainAsset: ChainAsset) -> Bool +} + +final class AssetExchangeSufficiencyProvider: AssetExchangeSufficiencyProviding { + func isSufficient(chainAsset: ChainAsset) -> Bool { + switch AssetType(rawType: chainAsset.asset.type) { + case .none, .orml, .equilibrium, .evmAsset, .evmNative: + return true + case .statemine: + return chainAsset.asset.typeExtras?.isSufficient?.boolValue ?? false + } + } +} diff --git a/novawallet/Common/Services/AssetExchange/Common/AssetExchangeSwapLimit.swift b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeSwapLimit.swift new file mode 100644 index 0000000000..d486ed460a --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeSwapLimit.swift @@ -0,0 +1,34 @@ +import Foundation + +struct AssetExchangeSwapLimit { + let direction: AssetConversion.Direction + let amountIn: Balance + let amountOut: Balance + let slippage: BigRational +} + +extension AssetExchangeSwapLimit { + private func getNewDirection(for shouldReplaceBuyWithSell: Bool) -> AssetConversion.Direction { + switch direction { + case .sell: + .sell + case .buy: + shouldReplaceBuyWithSell ? .sell : .buy + } + } + + func replacingAmountIn( + _ newAmountIn: Balance, + shouldReplaceBuyWithSell: Bool + ) -> AssetExchangeSwapLimit { + let newAmountOut = (newAmountIn * amountOut) / amountIn + let newDirection = getNewDirection(for: shouldReplaceBuyWithSell) + + return AssetExchangeSwapLimit( + direction: newDirection, + amountIn: newAmountIn, + amountOut: newAmountOut, + slippage: slippage + ) + } +} diff --git a/novawallet/Common/Services/AssetExchange/Common/AssetExchangeTimeEstimation.swift b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeTimeEstimation.swift new file mode 100644 index 0000000000..be2ad136b5 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeTimeEstimation.swift @@ -0,0 +1,48 @@ +import Foundation +import Operation_iOS + +protocol AssetExchangeTimeEstimating { + func totalTimeWrapper(for chainIds: [ChainModel.Id]) -> CompoundOperationWrapper +} + +final class AssetExchangeTimeEstimator { + let chainRegistry: ChainRegistryProtocol + + init(chainRegistry: ChainRegistryProtocol) { + self.chainRegistry = chainRegistry + } +} + +extension AssetExchangeTimeEstimator: AssetExchangeTimeEstimating { + func totalTimeWrapper(for chainIds: [ChainModel.Id]) -> CompoundOperationWrapper { + do { + let wrappers: [CompoundOperationWrapper] = try chainIds.compactMap { chainId in + guard + let chain = chainRegistry.getChain(for: chainId), + let runtimeProvider = chainRegistry.getRuntimeProvider(for: chainId) else { + throw ChainRegistryError.runtimeMetadaUnavailable + } + + let operationFactory = BlockTimeOperationFactory(chain: chain) + + return operationFactory.createExpectedBlockTimeWrapper(from: runtimeProvider) + } + + let mappingOperation = ClosureOperation { + let blockTime: BlockTime = try wrappers + .map { try $0.targetOperation.extractNoCancellableResultData() } + .reduce(0) { $0 + $1 } + + return TimeInterval(blockTime).seconds + } + + wrappers.forEach { mappingOperation.addDependency($0.targetOperation) } + + let dependecies = wrappers.flatMap(\.allOperations) + + return CompoundOperationWrapper(targetOperation: mappingOperation, dependencies: dependecies) + } catch { + return .createWithError(error) + } + } +} diff --git a/novawallet/Common/Services/AssetExchange/Common/AssetsExchageGraphReachability.swift b/novawallet/Common/Services/AssetExchange/Common/AssetsExchageGraphReachability.swift new file mode 100644 index 0000000000..848acdb5f0 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/Common/AssetsExchageGraphReachability.swift @@ -0,0 +1,37 @@ +import Foundation + +protocol AssetsExchageGraphReachabilityProtocol { + func getAllAssetIn() -> Set + func getAllAssetOut() -> Set + func getAssetsIn(for assetOut: ChainAssetId) -> Set + func getAssetsOut(for assetIn: ChainAssetId) -> Set +} + +final class AssetsExchageGraphReachability { + let mapping: [ChainAssetId: Set] + + init(mapping: [ChainAssetId: Set]) { + self.mapping = mapping + } +} + +extension AssetsExchageGraphReachability: AssetsExchageGraphReachabilityProtocol { + func getAllAssetIn() -> Set { + Set(mapping.keys) + } + + func getAllAssetOut() -> Set { + mapping.values.reduce(Set()) { accum, assets in + accum.union(assets) + } + } + + func getAssetsIn(for assetOut: ChainAssetId) -> Set { + let assetsIn = mapping.filter { $0.value.contains(assetOut) }.keys + return Set(assetsIn) + } + + func getAssetsOut(for assetIn: ChainAssetId) -> Set { + mapping[assetIn] ?? [] + } +} diff --git a/novawallet/Common/Services/AssetExchange/Common/AssetsExchangeBaseProvider.swift b/novawallet/Common/Services/AssetExchange/Common/AssetsExchangeBaseProvider.swift new file mode 100644 index 0000000000..a14f39f83f --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/Common/AssetsExchangeBaseProvider.swift @@ -0,0 +1,81 @@ +import Foundation + +class AssetsExchangeBaseProvider { + let chainRegistry: ChainRegistryProtocol + let graphProxy: AssetExchangeGraphProxy + + private var observableState: Observable> = .init( + state: .init(value: []) + ) + + let operationQueue: OperationQueue + let syncQueue: DispatchQueue + let logger: LoggerProtocol + + init( + chainRegistry: ChainRegistryProtocol, + pathCostEstimator: AssetsExchangePathCostEstimating, + operationQueue: OperationQueue, + syncQueue: DispatchQueue, + logger: LoggerProtocol + ) { + self.chainRegistry = chainRegistry + self.operationQueue = operationQueue + self.syncQueue = syncQueue + self.logger = logger + + graphProxy = AssetExchangeGraphProxy( + pathCostEstimator: pathCostEstimator, + operationQueue: operationQueue, + logger: logger + ) + } + + func updateState(with newExchanges: [AssetsExchangeProtocol]) { + observableState.state = .init(value: newExchanges) + } + + func performSetup() { + fatalError("Must be overriden by subsclass") + } + + func performThrottle() { + fatalError("Must be overriden by subsclass") + } +} + +extension AssetsExchangeBaseProvider: AssetsExchangeProviding { + func setup() { + performSetup() + } + + func throttle() { + performThrottle() + } + + func subscribeExchanges( + _ target: AnyObject, + notifyingIn queue: DispatchQueue, + onChange: @escaping ([AssetsExchangeProtocol]) -> Void + ) { + syncQueue.async { [weak self] in + self?.observableState.addObserver( + with: target, + sendStateOnSubscription: true, + queue: queue + ) { _, newState in + onChange(newState.value) + } + } + } + + func unsubscribeExchanges(_ target: AnyObject) { + syncQueue.async { [weak self] in + self?.observableState.removeObserver(by: target) + } + } + + func inject(graph: AssetsExchangeGraphProtocol) { + graphProxy.install(graph: graph) + } +} diff --git a/novawallet/Common/Services/AssetExchange/Common/GraphQuotableEdge.swift b/novawallet/Common/Services/AssetExchange/Common/GraphQuotableEdge.swift new file mode 100644 index 0000000000..dc1c2186f1 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/Common/GraphQuotableEdge.swift @@ -0,0 +1,6 @@ +import Foundation +import Operation_iOS + +protocol GraphQuotableEdge: GraphWeightableEdgeProtocol where Node == ChainAssetId { + func quote(amount: Balance, direction: AssetConversion.Direction) -> CompoundOperationWrapper +} diff --git a/novawallet/Common/Services/AssetExchange/CrosschainExchange/AssetExchangeOperationFee+Crosschain.swift b/novawallet/Common/Services/AssetExchange/CrosschainExchange/AssetExchangeOperationFee+Crosschain.swift new file mode 100644 index 0000000000..aca0fca1d0 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/CrosschainExchange/AssetExchangeOperationFee+Crosschain.swift @@ -0,0 +1,45 @@ +import Foundation + +extension AssetExchangeOperationFee { + init( + crosschainFee: XcmFeeModelProtocol, + originFee: ExtrinsicFeeProtocol, + assetIn: ChainAssetId, + assetOut _: ChainAssetId, + originUtilityAsset: ChainAssetId, + args: AssetExchangeAtomicOperationArgs + ) { + submissionFee = .init( + amountWithAsset: .init( + amount: originFee.amount, + asset: args.feeAsset + ), + payer: originFee.payer, + weight: originFee.weight + ) + + let paidByAccount: [AmountByPayer] = if crosschainFee.senderPart > 0 { + [ + .init( + amountWithAsset: .init(amount: crosschainFee.senderPart, asset: originUtilityAsset), + payer: nil + ) + ] + } else { + [] + } + + let paidFromAmount: [Amount] = if crosschainFee.holdingPart > 0 { + [ + .init(amount: crosschainFee.holdingPart, asset: assetIn) + ] + } else { + [] + } + + postSubmissionFee = .init( + paidByAccount: paidByAccount, + paidFromAmount: paidFromAmount + ) + } +} diff --git a/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainAssetsExchange.swift b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainAssetsExchange.swift new file mode 100644 index 0000000000..d0c9d5319d --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainAssetsExchange.swift @@ -0,0 +1,37 @@ +import Foundation +import Operation_iOS + +final class CrosschainAssetsExchange { + let host: CrosschainExchangeHostProtocol + + init(host: CrosschainExchangeHostProtocol) { + self.host = host + } + + private func createExchange(from origin: ChainAssetId, destination: ChainAssetId) -> CrosschainExchangeEdge? { + .init(origin: origin, destination: destination, host: host) + } +} + +extension CrosschainAssetsExchange: AssetsExchangeProtocol { + func availableDirectSwapConnections() -> CompoundOperationWrapper<[any AssetExchangableGraphEdge]> { + let operation = ClosureOperation<[any AssetExchangableGraphEdge]> { + self.host.xcmTransfers.chains.flatMap { xcmChain in + xcmChain.assets.flatMap { xcmAsset in + let origin = ChainAssetId(chainId: xcmChain.chainId, assetId: xcmAsset.assetId) + + return xcmAsset.xcmTransfers.compactMap { xcmTransfer in + let destination = ChainAssetId( + chainId: xcmTransfer.destination.chainId, + assetId: xcmTransfer.destination.assetId + ) + + return self.createExchange(from: origin, destination: destination) + } + } + } + } + + return CompoundOperationWrapper(targetOperation: operation) + } +} diff --git a/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainAssetsExchangeProvider.swift b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainAssetsExchangeProvider.swift new file mode 100644 index 0000000000..6b3811d1b3 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainAssetsExchangeProvider.swift @@ -0,0 +1,136 @@ +import Foundation +import Operation_iOS + +final class CrosschainAssetsExchangeProvider: AssetsExchangeBaseProvider { + private var xcmTransfers: XcmTransfers? + private var allChains: [ChainModel.Id: ChainModel]? + + let wallet: MetaAccountModel + let syncService: XcmTransfersSyncServiceProtocol + let userStorageFacade: StorageFacadeProtocol + let substrateStorageFacade: StorageFacadeProtocol + let signingWrapperFactory: SigningWrapperFactoryProtocol + + init( + wallet: MetaAccountModel, + syncService: XcmTransfersSyncServiceProtocol, + chainRegistry: ChainRegistryProtocol, + pathCostEstimator: AssetsExchangePathCostEstimating, + signingWrapperFactory: SigningWrapperFactoryProtocol, + userStorageFacade: StorageFacadeProtocol, + substrateStorageFacade: StorageFacadeProtocol, + operationQueue: OperationQueue, + logger: LoggerProtocol + ) { + self.wallet = wallet + self.syncService = syncService + self.signingWrapperFactory = signingWrapperFactory + self.userStorageFacade = userStorageFacade + self.substrateStorageFacade = substrateStorageFacade + + super.init( + chainRegistry: chainRegistry, + pathCostEstimator: pathCostEstimator, + operationQueue: operationQueue, + syncQueue: DispatchQueue(label: "io.novawallet.crosschainassetsprovider.\(UUID().uuidString)"), + logger: logger + ) + } + + private func handleChains(changes: [DataProviderChange]) -> Bool { + let updatedChains = changes.reduce(into: allChains ?? [:]) { accum, change in + switch change { + case let .insert(newItem), let .update(newItem): + accum[newItem.chainId] = newItem + case let .delete(deletedIdentifier): + accum[deletedIdentifier] = nil + } + } + + guard allChains != updatedChains else { + return false + } + + allChains = updatedChains + + return true + } + + private func updateStateIfNeeded() { + guard let xcmTransfers, let allChains else { + return + } + + let host = CrosschainExchangeHost( + wallet: wallet, + allChains: allChains, + chainRegistry: chainRegistry, + signingWrapperFactory: signingWrapperFactory, + xcmService: XcmTransferService( + wallet: wallet, + chainRegistry: chainRegistry, + metadataHashOperationFactory: MetadataHashOperationFactory( + metadataRepositoryFactory: RuntimeMetadataRepositoryFactory(storageFacade: substrateStorageFacade), + operationQueue: operationQueue + ), + userStorageFacade: userStorageFacade, + substrateStorageFacade: substrateStorageFacade, + operationQueue: operationQueue, + customFeeEstimatingFactory: AssetExchangeFeeEstimatingFactory( + graphProxy: graphProxy, + operationQueue: operationQueue + ) + ), + resolutionFactory: XcmTransferResolutionFactory( + chainRegistry: chainRegistry, + paraIdOperationFactory: ParaIdOperationFactory( + chainRegistry: chainRegistry, + operationQueue: operationQueue + ) + ), + xcmTransfers: xcmTransfers, + executionTimeEstimator: AssetExchangeTimeEstimator(chainRegistry: chainRegistry), + operationQueue: operationQueue, + logger: logger + ) + + let exchange = CrosschainAssetsExchange(host: host) + + updateState(with: [exchange]) + } + + // MARK: Subsclass + + override func performSetup() { + syncService.notificationCallback = { [weak self] transfersResult in + switch transfersResult { + case let .success(transfers): + self?.xcmTransfers = transfers + self?.updateStateIfNeeded() + case let .failure(error): + self?.logger.error("Xcm trasfers fetch failed \(error)") + } + } + + syncService.notificationQueue = syncQueue + + syncService.setup() + + chainRegistry.chainsSubscribe( + self, + runningInQueue: syncQueue, + filterStrategy: .enabledChains + ) { [weak self] changes in + guard let self, handleChains(changes: changes) else { + return + } + + updateStateIfNeeded() + } + } + + override func performThrottle() { + syncService.throttle() + chainRegistry.chainsUnsubscribe(self) + } +} diff --git a/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeAtomicOperation.swift b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeAtomicOperation.swift new file mode 100644 index 0000000000..ef28ef9063 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeAtomicOperation.swift @@ -0,0 +1,253 @@ +import Foundation +import Operation_iOS + +final class CrosschainExchangeAtomicOperation { + let host: CrosschainExchangeHostProtocol + let operationArgs: AssetExchangeAtomicOperationArgs + let edge: any AssetExchangableGraphEdge + let workingQueue: DispatchQueue + + init( + host: CrosschainExchangeHostProtocol, + edge: any AssetExchangableGraphEdge, + operationArgs: AssetExchangeAtomicOperationArgs, + workingQueue: DispatchQueue = .global() + ) { + self.host = host + self.edge = edge + self.operationArgs = operationArgs + self.workingQueue = workingQueue + } + + private func createXcmPartiesResolutionWrapper( + for destinationAccount: ChainAccountResponse + ) -> CompoundOperationWrapper { + host.resolutionFactory.createResolutionWrapper( + for: edge.origin, + transferDestinationId: .init( + chainId: edge.destination.chainId, + accountId: destinationAccount.accountId + ), + xcmTransfers: host.xcmTransfers + ) + } + + private func createOriginFeeFetchWrapper( + dependingOn resolutionOperation: BaseOperation, + feeOperation: BaseOperation, + amountClosure: @escaping () throws -> Balance + ) -> CompoundOperationWrapper { + OperationCombiningService.compoundNonOptionalWrapper( + operationQueue: host.operationQueue + ) { + let transferParties = try resolutionOperation.extractNoCancellableResultData() + let crosschainFee = try feeOperation.extractNoCancellableResultData() + let amount = try amountClosure() + + let unweightedRequest = XcmUnweightedTransferRequest( + origin: transferParties.origin, + destination: transferParties.destination, + reserve: transferParties.reserve, + amount: amount + ) + + let transferRequest = XcmTransferRequest( + unweighted: unweightedRequest, + maxWeight: crosschainFee.weightLimit, + originFeeAsset: self.operationArgs.feeAsset + ) + + let feeOperation = AsyncClosureOperation { completion in + self.host.xcmService.estimateOriginFee( + request: transferRequest, + xcmTransfers: self.host.xcmTransfers, + runningIn: self.workingQueue + ) { result in + completion(result) + } + } + + return CompoundOperationWrapper(targetOperation: feeOperation) + } + } + + private func createCrosschainFeeFetchWrapper( + dependingOn resolutionOperation: BaseOperation, + amountClosure: @escaping () throws -> Balance + ) -> CompoundOperationWrapper { + OperationCombiningService.compoundNonOptionalWrapper( + operationQueue: host.operationQueue + ) { + let transferParties = try resolutionOperation.extractNoCancellableResultData() + let amount = try amountClosure() + + let request = XcmUnweightedTransferRequest( + origin: transferParties.origin, + destination: transferParties.destination, + reserve: transferParties.reserve, + amount: amount + ) + + let feeOperation = AsyncClosureOperation { completion in + self.host.xcmService.estimateCrossChainFee( + request: request, + xcmTransfers: self.host.xcmTransfers, + runningIn: self.workingQueue + ) { result in + completion(result) + } + } + + return CompoundOperationWrapper(targetOperation: feeOperation) + } + } + + private func createSubmitWrapper( + for originAccount: ChainAccountResponse, + destinationAsset: ChainAsset, + resolutionOperation: BaseOperation, + feeOperation: BaseOperation, + amountClosure: @escaping () throws -> Balance + ) -> CompoundOperationWrapper { + OperationCombiningService.compoundNonOptionalWrapper( + operationQueue: host.operationQueue + ) { + let transferParties = try resolutionOperation.extractNoCancellableResultData() + let fee = try feeOperation.extractNoCancellableResultData() + let amount = try amountClosure() + + let signer = self.host.signingWrapperFactory.createSigningWrapper( + for: originAccount.metaId, + accountResponse: originAccount + ) + + let unweightedRequest = XcmUnweightedTransferRequest( + origin: transferParties.origin, + destination: transferParties.destination, + reserve: transferParties.reserve, + amount: amount + ) + + let transferRequest = XcmTransferRequest( + unweighted: unweightedRequest, + maxWeight: fee.weightLimit, + originFeeAsset: self.operationArgs.feeAsset + ) + + let transactService = XcmTransactService( + chainRegistry: self.host.chainRegistry, + transferService: self.host.xcmService, + workingQueue: self.workingQueue, + operationQueue: self.host.operationQueue, + logger: self.host.logger + ) + + return transactService.transferAndWaitArrivalWrapper( + transferRequest, + destinationChainAsset: destinationAsset, + xcmTransfers: self.host.xcmTransfers, + signer: signer + ) + } + } +} + +extension CrosschainExchangeAtomicOperation: AssetExchangeAtomicOperationProtocol { + func executeWrapper(for swapLimit: AssetExchangeSwapLimit) -> CompoundOperationWrapper { + guard + let originChain = host.allChains[edge.origin.chainId], + let destinationChain = host.allChains[edge.destination.chainId], + let destinationAsset = destinationChain.chainAsset(for: edge.destination.assetId), + let originAccount = host.wallet.fetch(for: originChain.accountRequest()), + let destinationAccount = host.wallet.fetch(for: destinationChain.accountRequest()) else { + return .createWithError(ChainAccountFetchingError.accountNotExists) + } + + let resolutionWrapper = createXcmPartiesResolutionWrapper(for: destinationAccount) + + // TODO: We need only weight from the crosschain fee, probably we can calculate it during submission + let feeWrapper = createCrosschainFeeFetchWrapper( + dependingOn: resolutionWrapper.targetOperation, + amountClosure: { swapLimit.amountIn } + ) + + feeWrapper.addDependency(wrapper: resolutionWrapper) + + let submitWrapper = createSubmitWrapper( + for: originAccount, + destinationAsset: destinationAsset, + resolutionOperation: resolutionWrapper.targetOperation, + feeOperation: feeWrapper.targetOperation, + amountClosure: { swapLimit.amountIn } + ) + + submitWrapper.addDependency(wrapper: feeWrapper) + + return submitWrapper + .insertingHead(operations: feeWrapper.allOperations) + .insertingHead(operations: resolutionWrapper.allOperations) + } + + func estimateFee() -> CompoundOperationWrapper { + guard + let originChain = host.allChains[edge.origin.chainId], + let originUtilityAsset = originChain.utilityChainAsset(), + let destinationChain = host.allChains[edge.destination.chainId], + let destinationAccount = host.wallet.fetch(for: destinationChain.accountRequest()) else { + return .createWithError(ChainAccountFetchingError.accountNotExists) + } + + let resolutionWrapper = createXcmPartiesResolutionWrapper(for: destinationAccount) + + let crosschainFeeWrapper = createCrosschainFeeFetchWrapper( + dependingOn: resolutionWrapper.targetOperation, + amountClosure: { self.operationArgs.swapLimit.amountIn } + ) + + crosschainFeeWrapper.addDependency(wrapper: resolutionWrapper) + + let originFeeWrapper = createOriginFeeFetchWrapper( + dependingOn: resolutionWrapper.targetOperation, + feeOperation: crosschainFeeWrapper.targetOperation, + amountClosure: { self.operationArgs.swapLimit.amountIn } + ) + + originFeeWrapper.addDependency(wrapper: crosschainFeeWrapper) + + let mappingOperation = ClosureOperation { + let originFee = try originFeeWrapper.targetOperation.extractNoCancellableResultData() + let crosschainFee = try crosschainFeeWrapper.targetOperation.extractNoCancellableResultData() + + return .init( + crosschainFee: crosschainFee, + originFee: originFee, + assetIn: self.edge.origin, + assetOut: self.edge.destination, + originUtilityAsset: originUtilityAsset.chainAssetId, + args: self.operationArgs + ) + } + + mappingOperation.addDependency(crosschainFeeWrapper.targetOperation) + mappingOperation.addDependency(originFeeWrapper.targetOperation) + + return originFeeWrapper + .insertingHead(operations: crosschainFeeWrapper.allOperations) + .insertingHead(operations: resolutionWrapper.allOperations) + .insertingTail(operation: mappingOperation) + } + + func requiredAmountToGetAmountOut( + _ amountOutClosure: @escaping () throws -> Balance + ) -> CompoundOperationWrapper { + let operation = ClosureOperation { + try amountOutClosure() + } + + return CompoundOperationWrapper(targetOperation: operation) + } + + var swapLimit: AssetExchangeSwapLimit { + operationArgs.swapLimit + } +} diff --git a/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeEdge.swift b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeEdge.swift new file mode 100644 index 0000000000..b820635a02 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeEdge.swift @@ -0,0 +1,136 @@ +import Foundation +import Operation_iOS + +final class CrosschainExchangeEdge { + let origin: ChainAssetId + let destination: ChainAssetId + let host: CrosschainExchangeHostProtocol + + init(origin: ChainAssetId, destination: ChainAssetId, host: CrosschainExchangeHostProtocol) { + self.origin = origin + self.destination = destination + self.host = host + } + + private func deliveryFeeNotPaidOrFromHolding() -> Bool { + do { + guard let deliveryFee = try host.xcmTransfers.deliveryFee(from: origin.chainId) else { + return true + } + + guard + let originChain = host.allChains[origin.chainId], + let destinationChain = host.allChains[destination.chainId] else { + return false + } + + if !destinationChain.isRelaychain { + return deliveryFee.toParachain?.alwaysHoldingPays ?? false + } else if !originChain.isRelaychain { + return deliveryFee.toParent?.alwaysHoldingPays ?? false + } else { + return false + } + } catch { + return false + } + } +} + +extension CrosschainExchangeEdge: AssetExchangableGraphEdge { + var type: AssetExchangeEdgeType { .crossChain } + + var weight: Int { AssetsExchange.defaultEdgeWeight } + + func quote( + amount: Balance, + direction _: AssetConversion.Direction + ) -> CompoundOperationWrapper { + CompoundOperationWrapper.createWithResult(amount) + } + + func beginOperation(for args: AssetExchangeAtomicOperationArgs) throws -> AssetExchangeAtomicOperationProtocol { + CrosschainExchangeAtomicOperation( + host: host, + edge: self, + operationArgs: args + ) + } + + func appendToOperation( + _: AssetExchangeAtomicOperationProtocol, + args _: AssetExchangeAtomicOperationArgs + ) -> AssetExchangeAtomicOperationProtocol? { + nil + } + + func shouldIgnoreFeeRequirement(after _: any AssetExchangableGraphEdge) -> Bool { + false + } + + func canPayNonNativeFeesInIntermediatePosition() -> Bool { + deliveryFeeNotPaidOrFromHolding() + } + + func beginMetaOperation( + for amountIn: Balance, + amountOut: Balance + ) throws -> AssetExchangeMetaOperationProtocol { + guard let chainIn = host.allChains[origin.chainId] else { + throw ChainRegistryError.noChain(origin.chainId) + } + + guard let chainOut = host.allChains[destination.chainId] else { + throw ChainRegistryError.noChain(destination.chainId) + } + + guard let assetIn = chainIn.chainAsset(for: origin.assetId) else { + throw ChainModelFetchError.noAsset(assetId: origin.assetId) + } + + guard let assetOut = chainOut.chainAsset(for: destination.assetId) else { + throw ChainModelFetchError.noAsset(assetId: destination.assetId) + } + + return CrosschainExchangeMetaOperation( + assetIn: assetIn, + assetOut: assetOut, + amountIn: amountIn, + amountOut: amountOut + ) + } + + func appendToMetaOperation( + _: AssetExchangeMetaOperationProtocol, + amountIn _: Balance, + amountOut _: Balance + ) throws -> AssetExchangeMetaOperationProtocol? { + nil + } + + func beginOperationPrototype() throws -> AssetExchangeOperationPrototypeProtocol { + guard let chainIn = host.allChains[origin.chainId] else { + throw ChainRegistryError.noChain(origin.chainId) + } + + guard let chainOut = host.allChains[destination.chainId] else { + throw ChainRegistryError.noChain(destination.chainId) + } + + guard let assetIn = chainIn.chainAsset(for: origin.assetId) else { + throw ChainModelFetchError.noAsset(assetId: origin.assetId) + } + + guard let assetOut = chainOut.chainAsset(for: destination.assetId) else { + throw ChainModelFetchError.noAsset(assetId: destination.assetId) + } + + return CrosschainExchangeOperationPrototype(assetIn: assetIn, assetOut: assetOut, host: host) + } + + func appendToOperationPrototype( + _: AssetExchangeOperationPrototypeProtocol + ) throws -> AssetExchangeOperationPrototypeProtocol? { + nil + } +} diff --git a/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeHost.swift b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeHost.swift new file mode 100644 index 0000000000..c331341bab --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeHost.swift @@ -0,0 +1,51 @@ +import Foundation + +protocol CrosschainExchangeHostProtocol { + var wallet: MetaAccountModel { get } + var allChains: IndexedChainModels { get } + var chainRegistry: ChainRegistryProtocol { get } + var signingWrapperFactory: SigningWrapperFactoryProtocol { get } + var xcmService: XcmTransferServiceProtocol { get } + var resolutionFactory: XcmTransferResolutionFactoryProtocol { get } + var xcmTransfers: XcmTransfers { get } + var operationQueue: OperationQueue { get } + var executionTimeEstimator: AssetExchangeTimeEstimating { get } + var logger: LoggerProtocol { get } +} + +final class CrosschainExchangeHost: CrosschainExchangeHostProtocol { + let wallet: MetaAccountModel + let allChains: IndexedChainModels + let chainRegistry: ChainRegistryProtocol + let signingWrapperFactory: SigningWrapperFactoryProtocol + let xcmService: XcmTransferServiceProtocol + let resolutionFactory: XcmTransferResolutionFactoryProtocol + let xcmTransfers: XcmTransfers + let executionTimeEstimator: AssetExchangeTimeEstimating + let operationQueue: OperationQueue + let logger: LoggerProtocol + + init( + wallet: MetaAccountModel, + allChains: IndexedChainModels, + chainRegistry: ChainRegistryProtocol, + signingWrapperFactory: SigningWrapperFactoryProtocol, + xcmService: XcmTransferServiceProtocol, + resolutionFactory: XcmTransferResolutionFactoryProtocol, + xcmTransfers: XcmTransfers, + executionTimeEstimator: AssetExchangeTimeEstimating, + operationQueue: OperationQueue, + logger: LoggerProtocol + ) { + self.wallet = wallet + self.allChains = allChains + self.chainRegistry = chainRegistry + self.signingWrapperFactory = signingWrapperFactory + self.xcmService = xcmService + self.resolutionFactory = resolutionFactory + self.xcmTransfers = xcmTransfers + self.executionTimeEstimator = executionTimeEstimator + self.operationQueue = operationQueue + self.logger = logger + } +} diff --git a/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeMetaOperation.swift b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeMetaOperation.swift new file mode 100644 index 0000000000..9b0cdf7ece --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeMetaOperation.swift @@ -0,0 +1,7 @@ +import Foundation + +final class CrosschainExchangeMetaOperation: AssetExchangeBaseMetaOperation {} + +extension CrosschainExchangeMetaOperation: AssetExchangeMetaOperationProtocol { + var label: AssetExchangeMetaOperationLabel { .transfer } +} diff --git a/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeOperationPrototype.swift b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeOperationPrototype.swift new file mode 100644 index 0000000000..1ffdc3e92b --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeOperationPrototype.swift @@ -0,0 +1,104 @@ +import Foundation +import Operation_iOS + +final class CrosschainExchangeOperationPrototype: AssetExchangeBaseOperationPrototype { + let host: CrosschainExchangeHostProtocol + + init(assetIn: ChainAsset, assetOut: ChainAsset, host: CrosschainExchangeHostProtocol) { + self.host = host + + super.init(assetIn: assetIn, assetOut: assetOut) + } + + private func createXcmPartiesResolutionWrapper( + for destinationAccount: ChainAccountResponse + ) -> CompoundOperationWrapper { + host.resolutionFactory.createResolutionWrapper( + for: assetIn.chainAssetId, + transferDestinationId: .init( + chainId: assetOut.chain.chainId, + accountId: destinationAccount.accountId + ), + xcmTransfers: host.xcmTransfers + ) + } +} + +private extension CrosschainExchangeOperationPrototype { + private func isChainWithExpensiveCrossChain(chainId: ChainModel.Id) -> Bool { + chainId == KnowChainId.polkadot || chainId == KnowChainId.statemint + } +} + +extension CrosschainExchangeOperationPrototype: AssetExchangeOperationPrototypeProtocol { + func estimatedCostInUsdt(using _: AssetExchageUsdtConverting) throws -> Decimal { + var cost: Decimal = 0 + + let chainInExpensive = isChainWithExpensiveCrossChain(chainId: assetIn.chain.chainId) + let chainOutExpensive = isChainWithExpensiveCrossChain(chainId: assetOut.chain.chainId) + + if chainInExpensive { + cost += 0.15 + } + + if chainOutExpensive { + cost += 0.1 + } + + if !chainInExpensive || !chainOutExpensive { + cost += 0.01 + } + + return cost + } + + func estimatedExecutionTimeWrapper() -> CompoundOperationWrapper { + guard let destinationAccount = host.wallet.fetch(for: assetOut.chain.accountRequest()) else { + return .createWithError(ChainAccountFetchingError.accountNotExists) + } + + let resolutionWrapper = createXcmPartiesResolutionWrapper(for: destinationAccount) + + let estimationTimeWrapper = OperationCombiningService.compoundNonOptionalWrapper( + operationQueue: host.operationQueue + ) { + let partiesResolution = try resolutionWrapper.targetOperation.extractNoCancellableResultData() + + let originChain = partiesResolution.origin.chain + let destinationChain = partiesResolution.destination.chain + let reserveChain = partiesResolution.reserve.chain + + let relaychainId = [originChain, destinationChain, reserveChain] + .compactMap(\.parentId) + .first ?? originChain.chainId + + var participatingChains: [ChainModel.Id] = [originChain.chainId] + + if originChain.chainId != reserveChain.chainId { + participatingChains.append(reserveChain.chainId) + + if !originChain.isRelaychain, !reserveChain.isRelaychain { + participatingChains.append(relaychainId) + } + } + + if reserveChain.chainId != destinationChain.chainId { + participatingChains.append(destinationChain.chainId) + + if !reserveChain.isRelaychain, !destinationChain.isRelaychain { + participatingChains.append(relaychainId) + } + } + + guard !participatingChains.isEmpty else { + return .createWithResult(0) + } + + return self.host.executionTimeEstimator.totalTimeWrapper(for: participatingChains) + } + + estimationTimeWrapper.addDependency(wrapper: resolutionWrapper) + + return estimationTimeWrapper.insertingHead(operations: resolutionWrapper.allOperations) + } +} diff --git a/novawallet/Common/Services/AssetExchange/Facade/AssetExchangeFacade.swift b/novawallet/Common/Services/AssetExchange/Facade/AssetExchangeFacade.swift new file mode 100644 index 0000000000..5ced93030b --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/Facade/AssetExchangeFacade.swift @@ -0,0 +1,61 @@ +import Foundation + +final class AssetExchangeFacade { + static func createGraphProvider( + for params: AssetExchangeGraphProvidingParams, + feeSupportProvider: AssetsExchangeFeeSupportProviding, + exchangesStateMediator: AssetsExchangeStateMediating, + pathCostEstimator: AssetsExchangePathCostEstimating + ) -> AssetsExchangeGraphProviding { + let suffiencyProvider = AssetExchangeSufficiencyProvider() + + return AssetsExchangeGraphProvider( + selectedWallet: params.wallet, + chainRegistry: params.chainRegistry, + supportedExchangeProviders: [ + CrosschainAssetsExchangeProvider( + wallet: params.wallet, + syncService: XcmTransfersSyncService( + remoteUrl: params.config.xcmTransfersURL, + operationQueue: params.operationQueue, + logger: params.logger + ), + chainRegistry: params.chainRegistry, + pathCostEstimator: pathCostEstimator, + signingWrapperFactory: params.signingWrapperFactory, + userStorageFacade: params.userDataStorageFacade, + substrateStorageFacade: params.substrateStorageFacade, + operationQueue: params.operationQueue, + logger: params.logger + ), + + AssetsHydraExchangeProvider( + selectedWallet: params.wallet, + chainRegistry: params.chainRegistry, + pathCostEstimator: pathCostEstimator, + userStorageFacade: params.userDataStorageFacade, + substrateStorageFacade: params.substrateStorageFacade, + exchangeStateRegistrar: exchangesStateMediator, + operationQueue: params.operationQueue, + logger: params.logger + ), + + AssetsHubExchangeProvider( + wallet: params.wallet, + chainRegistry: params.chainRegistry, + pathCostEstimator: pathCostEstimator, + signingWrapperFactory: params.signingWrapperFactory, + userStorageFacade: params.userDataStorageFacade, + substrateStorageFacade: params.substrateStorageFacade, + exchangeStateRegistrar: exchangesStateMediator, + operationQueue: params.operationQueue, + logger: params.logger + ) + ], + feeSupportProvider: feeSupportProvider, + suffiencyProvider: suffiencyProvider, + operationQueue: params.operationQueue, + logger: params.logger + ) + } +} diff --git a/novawallet/Common/Services/AssetExchange/Facade/AssetExchangeGraphProvidingParams.swift b/novawallet/Common/Services/AssetExchange/Facade/AssetExchangeGraphProvidingParams.swift new file mode 100644 index 0000000000..02722a682f --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/Facade/AssetExchangeGraphProvidingParams.swift @@ -0,0 +1,44 @@ +import Foundation +import SoraKeystore +import Operation_iOS + +struct AssetExchangeGraphProvidingParams { + let wallet: MetaAccountModel + let substrateStorageFacade: StorageFacadeProtocol + let userDataStorageFacade: StorageFacadeProtocol + let chainRegistry: ChainRegistryProtocol + let config: ApplicationConfigProtocol + let operationQueue: OperationQueue + let keychain: KeystoreProtocol + let settingsManager: SettingsManagerProtocol + let signingWrapperFactory: SigningWrapperFactoryProtocol + let logger: LoggerProtocol + + init( + wallet: MetaAccountModel, + substrateStorageFacade: StorageFacadeProtocol = SubstrateDataStorageFacade.shared, + userDataStorageFacade: StorageFacadeProtocol = UserDataStorageFacade.shared, + chainRegistry: ChainRegistryProtocol = ChainRegistryFacade.sharedRegistry, + config: ApplicationConfigProtocol = ApplicationConfig.shared, + operationQueue: OperationQueue = OperationManagerFacade.sharedDefaultQueue, + keychain: KeystoreProtocol = Keychain(), + settingsManager: SettingsManagerProtocol = SettingsManager.shared, + logger: LoggerProtocol = Logger.shared + ) { + self.wallet = wallet + self.substrateStorageFacade = substrateStorageFacade + self.userDataStorageFacade = userDataStorageFacade + self.chainRegistry = chainRegistry + self.config = config + self.operationQueue = operationQueue + self.keychain = keychain + self.settingsManager = settingsManager + + signingWrapperFactory = SigningWrapperFactory( + keystore: keychain, + settingsManager: settingsManager + ) + + self.logger = logger + } +} diff --git a/novawallet/Common/Services/AssetExchange/FeeCapability/AssetExchangeFeeSupport.swift b/novawallet/Common/Services/AssetExchange/FeeCapability/AssetExchangeFeeSupport.swift new file mode 100644 index 0000000000..fd47dad9b8 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/FeeCapability/AssetExchangeFeeSupport.swift @@ -0,0 +1,21 @@ +import Foundation + +struct AssetExchangeFeeSupport { + let supportedAssets: Set +} + +extension AssetExchangeFeeSupport: AssetExchangeFeeSupporting { + func canPayFee(inNonNative chainAsset: ChainAsset) -> Bool { + supportedAssets.contains(chainAsset.chainAssetId) + } +} + +struct CompoundAssetExchangeFeeSupport { + let supporters: [AssetExchangeFeeSupporting] +} + +extension CompoundAssetExchangeFeeSupport: AssetExchangeFeeSupporting { + func canPayFee(inNonNative chainAsset: ChainAsset) -> Bool { + supporters.contains { $0.canPayFee(inNonNative: chainAsset) } + } +} diff --git a/novawallet/Common/Services/AssetExchange/FeeCapability/AssetExchangeFeeSupportFetchersProvider.swift b/novawallet/Common/Services/AssetExchange/FeeCapability/AssetExchangeFeeSupportFetchersProvider.swift new file mode 100644 index 0000000000..1e6b900052 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/FeeCapability/AssetExchangeFeeSupportFetchersProvider.swift @@ -0,0 +1,139 @@ +import Foundation +import Operation_iOS + +class AssetExchangeFeeSupportFetchersProvider { + private var observableState: Observable> = .init( + state: .init(value: []) + ) + + let chainRegistry: ChainRegistryProtocol + let operationQueue: OperationQueue + let syncQueue: DispatchQueue + let logger: LoggerProtocol + + private var supportedChains: [ChainModel.Id: ChainModel]? + + init( + chainRegistry: ChainRegistryProtocol, + operationQueue: OperationQueue, + logger: LoggerProtocol + ) { + self.chainRegistry = chainRegistry + self.operationQueue = operationQueue + syncQueue = DispatchQueue(label: "io.novawallet.assetexchangefeeprovider.\(UUID().uuidString)") + self.logger = logger + } + + private func updateState(with newSupporters: [AssetExchangeFeeSupportFetching]) { + observableState.state = .init(value: newSupporters) + } + + private func updateStateIfNeeded() { + guard let supportedChains else { + return + } + + let feeFetchers: [AssetExchangeFeeSupportFetching] = supportedChains.values.compactMap { chain in + do { + let connection = try chainRegistry.getConnectionOrError(for: chain.chainId) + let runtimeService = try chainRegistry.getRuntimeProviderOrError(for: chain.chainId) + + if chain.hasAssetHubFees { + return AssetHubExchangeFeeSupportFetcher( + chain: chain, + swapOperationFactory: AssetHubSwapOperationFactory( + chain: chain, + runtimeService: runtimeService, + connection: connection, + operationQueue: operationQueue + ) + ) + } else if chain.hasHydrationFees { + return HydraExchangeFeeSupportFetcher( + chain: chain, + connection: connection, + runtimeProvider: runtimeService, + operationQueue: operationQueue, + logger: logger + ) + } else { + return nil + } + } catch { + logger.error("Can't create fetcher \(error)") + return nil + } + } + + updateState(with: feeFetchers) + } + + private func handleChains(changes: [DataProviderChange]) -> Bool { + let updatedChains = changes.reduce(into: supportedChains ?? [:]) { accum, change in + switch change { + case let .insert(newItem), let .update(newItem): + accum[newItem.chainId] = newItem.hasCustomFees ? newItem : nil + case let .delete(deletedIdentifier): + accum[deletedIdentifier] = nil + } + } + + guard supportedChains != updatedChains else { + return false + } + + supportedChains = updatedChains + + return true + } + + private func performSetup() { + chainRegistry.chainsSubscribe( + self, + runningInQueue: syncQueue, + filterStrategy: nil + ) { [weak self] changes in + guard let self, handleChains(changes: changes) else { + return + } + + updateStateIfNeeded() + } + } + + private func performThrottle() { + chainRegistry.chainsUnsubscribe(self) + } +} + +extension AssetExchangeFeeSupportFetchersProvider: AssetExchangeFeeSupportFetchersProviding { + func setup() { + performSetup() + } + + func throttle() { + performThrottle() + } + + func subscribeFeeFetchers( + _ target: AnyObject, + notifyingIn queue: DispatchQueue, + onChange: @escaping ([AssetExchangeFeeSupportFetching]) -> Void + ) { + syncQueue.async { [weak self] in + self?.observableState.addObserver( + with: target, + sendStateOnSubscription: true, + queue: queue + ) { _, newState in + onChange(newState.value) + } + } + } + + func unsubscribeFeeFetchers(_ target: AnyObject) { + syncQueue.async { [weak self] in + self?.observableState.removeObserver(by: target) + } + } +} diff --git a/novawallet/Common/Services/AssetExchange/FeeCapability/AssetExchangeGraphFeeSupport.swift b/novawallet/Common/Services/AssetExchange/FeeCapability/AssetExchangeGraphFeeSupport.swift new file mode 100644 index 0000000000..f1a5698f1b --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/FeeCapability/AssetExchangeGraphFeeSupport.swift @@ -0,0 +1,40 @@ +import Foundation +import Operation_iOS + +protocol AssetExchangeFeeSupporting { + func canPayFee(inNonNative chainAsset: ChainAsset) -> Bool +} + +protocol AssetExchangeFeeSupportFetching { + var identifier: String { get } + + func createFeeSupportWrapper() -> CompoundOperationWrapper +} + +protocol AssetExchangeFeeSupportFetchersProviding { + func setup() + func throttle() + + func subscribeFeeFetchers( + _ target: AnyObject, + notifyingIn queue: DispatchQueue, + onChange: @escaping ([AssetExchangeFeeSupportFetching]) -> Void + ) + + func unsubscribeFeeFetchers(_ target: AnyObject) +} + +protocol AssetsExchangeFeeSupportProviding { + func setup() + func throttle() + + func subscribeFeeSupport( + _ target: AnyObject, + notifyingIn queue: DispatchQueue, + onChange: @escaping (AssetExchangeFeeSupporting?) -> Void + ) + + func unsubscribe(_ target: AnyObject) + + func fetchCurrentState(in queue: DispatchQueue, completionClosure: @escaping (AssetExchangeFeeSupporting?) -> Void) +} diff --git a/novawallet/Common/Services/AssetExchange/FeeCapability/AssetHubExchangeFeeSupportFetcher.swift b/novawallet/Common/Services/AssetExchange/FeeCapability/AssetHubExchangeFeeSupportFetcher.swift new file mode 100644 index 0000000000..9dc1776078 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/FeeCapability/AssetHubExchangeFeeSupportFetcher.swift @@ -0,0 +1,42 @@ +import Foundation +import Operation_iOS +import SubstrateSdk + +final class AssetHubExchangeFeeSupportFetcher { + let swapOperationFactory: AssetHubSwapOperationFactoryProtocol + let chain: ChainModel + + init( + chain: ChainModel, + swapOperationFactory: AssetHubSwapOperationFactoryProtocol + ) { + self.chain = chain + self.swapOperationFactory = swapOperationFactory + } +} + +extension AssetHubExchangeFeeSupportFetcher: AssetExchangeFeeSupportFetching { + var identifier: String { "asset-hub-\(chain.chainId)" } + + func createFeeSupportWrapper() -> CompoundOperationWrapper { + guard let utilityAssetId = chain.utilityChainAsset()?.chainAssetId else { + return .createWithError(ChainModelFetchError.noAsset(assetId: AssetModel.utilityAssetId)) + } + + let availableDirectionsWrapper = swapOperationFactory.availableDirections() + + let mappingOperation = ClosureOperation { + let availableDirections = try availableDirectionsWrapper.targetOperation.extractNoCancellableResultData() + + let supportedAssetIds = availableDirections + .filter { $0.value.contains(utilityAssetId) } + .keys + + return AssetExchangeFeeSupport(supportedAssets: Set(supportedAssetIds)) + } + + mappingOperation.addDependency(availableDirectionsWrapper.targetOperation) + + return availableDirectionsWrapper.insertingTail(operation: mappingOperation) + } +} diff --git a/novawallet/Common/Services/AssetExchange/FeeCapability/AssetsExchangeFeeSupportProvider.swift b/novawallet/Common/Services/AssetExchange/FeeCapability/AssetsExchangeFeeSupportProvider.swift new file mode 100644 index 0000000000..198d72a3af --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/FeeCapability/AssetsExchangeFeeSupportProvider.swift @@ -0,0 +1,140 @@ +import Foundation + +final class AssetsExchangeFeeSupportProvider { + let feeSupportFetchersProvider: AssetExchangeFeeSupportFetchersProviding + let operationQueue: OperationQueue + let logger: LoggerProtocol + + private let syncQueue: DispatchQueue + + private var observableState: Observable> = .init( + state: .init(value: nil) + ) + + private var feeSupporters: [String: AssetExchangeFeeSupporting] = [:] + private var feeFetchRequests: [String: CancellableCallStore] = [:] + + init( + feeSupportFetchersProvider: AssetExchangeFeeSupportFetchersProviding, + operationQueue: OperationQueue, + logger: LoggerProtocol + ) { + self.feeSupportFetchersProvider = feeSupportFetchersProvider + self.operationQueue = operationQueue + self.logger = logger + + syncQueue = DispatchQueue(label: "io.novawallet.exchangefeesupportprovider.\(UUID().uuidString)") + } +} + +private extension AssetsExchangeFeeSupportProvider { + private func clearCurrentRequests() { + feeFetchRequests.values.forEach { $0.cancel() } + } + + private func updateFeeSupport(for fetchers: [AssetExchangeFeeSupportFetching]) { + feeFetchRequests.values.forEach { $0.cancel() } + feeFetchRequests = [:] + + let oldFeeSupportIds = Set(feeSupporters.keys) + let newFeeSupportIds = Set(fetchers.map(\.identifier)) + + let idsToRemove = oldFeeSupportIds.subtracting(newFeeSupportIds) + + if !idsToRemove.isEmpty { + idsToRemove.forEach { feeSupporters[$0] = nil } + rebuildFeeSupport() + } + + fetchers.forEach { fetcher in + let callStore = CancellableCallStore() + feeFetchRequests[fetcher.identifier] = callStore + + let wrapper = fetcher.createFeeSupportWrapper() + + executeCancellable( + wrapper: wrapper, + inOperationQueue: operationQueue, + backingCallIn: callStore, + runningCallbackIn: syncQueue + ) { [weak self] result in + guard let self else { + return + } + + switch result { + case let .success(feeSupport): + logger.debug("Did receive fee support for \(fetcher.identifier).") + + feeSupporters[fetcher.identifier] = feeSupport + + rebuildFeeSupport() + case let .failure(error): + logger.error("Did receive error \(fetcher.identifier): \(error).") + } + } + } + } + + private func rebuildFeeSupport() { + let feeSupport = CompoundAssetExchangeFeeSupport(supporters: Array(feeSupporters.values)) + + observableState.state = .init(value: feeSupport) + } +} + +extension AssetsExchangeFeeSupportProvider: AssetsExchangeFeeSupportProviding { + func setup() { + feeSupportFetchersProvider.setup() + + feeSupportFetchersProvider.subscribeFeeFetchers( + self, + notifyingIn: syncQueue + ) { [weak self] fetchers in + self?.updateFeeSupport(for: fetchers) + } + } + + func throttle() { + feeSupportFetchersProvider.unsubscribeFeeFetchers(self) + + syncQueue.async { + self.clearCurrentRequests() + } + } + + func subscribeFeeSupport( + _ target: AnyObject, + notifyingIn queue: DispatchQueue, + onChange: @escaping (AssetExchangeFeeSupporting?) -> Void + ) { + syncQueue.async { [weak self] in + self?.observableState.addObserver( + with: target, + sendStateOnSubscription: true, + queue: queue + ) { _, newState in + onChange(newState.value) + } + } + } + + func unsubscribe(_ target: AnyObject) { + syncQueue.async { [weak self] in + self?.observableState.removeObserver(by: target) + } + } + + func fetchCurrentState( + in queue: DispatchQueue, + completionClosure: @escaping (AssetExchangeFeeSupporting?) -> Void + ) { + syncQueue.async { + let stateValue = self.observableState.state.value + + dispatchInQueueWhenPossible(queue) { + completionClosure(stateValue) + } + } + } +} diff --git a/novawallet/Common/Services/AssetExchange/FeeCapability/HydraExchangeFeeSupportFetcher.swift b/novawallet/Common/Services/AssetExchange/FeeCapability/HydraExchangeFeeSupportFetcher.swift new file mode 100644 index 0000000000..103fda8016 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/FeeCapability/HydraExchangeFeeSupportFetcher.swift @@ -0,0 +1,65 @@ +import Foundation +import Operation_iOS +import SubstrateSdk + +final class HydraExchangeFeeSupportFetcher { + let chain: ChainModel + let operationQueue: OperationQueue + let connection: JSONRPCEngine + let runtimeProvider: RuntimeProviderProtocol + let logger: LoggerProtocol + + init( + chain: ChainModel, + connection: JSONRPCEngine, + runtimeProvider: RuntimeProviderProtocol, + operationQueue: OperationQueue, + logger: LoggerProtocol + ) { + self.chain = chain + self.operationQueue = operationQueue + self.connection = connection + self.runtimeProvider = runtimeProvider + self.logger = logger + } +} + +extension HydraExchangeFeeSupportFetcher: AssetExchangeFeeSupportFetching { + var identifier: String { "hydra-fee-\(chain.chainId)" } + + func createFeeSupportWrapper() -> CompoundOperationWrapper { + let codingFactoryOperation = runtimeProvider.fetchCoderFactoryOperation() + + let keysFactory = StorageKeysOperationFactory(operationQueue: operationQueue) + let assetsFetchWrapper: CompoundOperationWrapper<[HydraDx.AssetsKey]> = keysFactory.createKeysFetchWrapper( + by: HydraDx.feeCurrenciesPath, + codingFactoryClosure: { try codingFactoryOperation.extractNoCancellableResultData() }, + connection: connection + ) + + assetsFetchWrapper.addDependency(operations: [codingFactoryOperation]) + + let mapOperation = ClosureOperation { + let allAssets = try assetsFetchWrapper.targetOperation.extractNoCancellableResultData() + let codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + + let remoteLocalMapping = try HydraDxTokenConverter.convertToRemoteLocalMapping( + remoteAssets: Set(allAssets.map(\.assetId)), + chain: self.chain, + codingFactory: codingFactory, + failureClosure: { self.logger.warning("Token \($0) conversion failed: \($1)") } + ) + + let localFeeAssetIds = Set(remoteLocalMapping.values) + + return AssetExchangeFeeSupport(supportedAssets: localFeeAssetIds) + } + + mapOperation.addDependency(codingFactoryOperation) + mapOperation.addDependency(assetsFetchWrapper.targetOperation) + + return assetsFetchWrapper + .insertingHead(operations: [codingFactoryOperation]) + .insertingTail(operation: mapOperation) + } +} diff --git a/novawallet/Common/Services/AssetExchange/FeeEstimating/AssetExchangeFeeEstimatingFactory.swift b/novawallet/Common/Services/AssetExchange/FeeEstimating/AssetExchangeFeeEstimatingFactory.swift new file mode 100644 index 0000000000..0dc543def9 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/FeeEstimating/AssetExchangeFeeEstimatingFactory.swift @@ -0,0 +1,25 @@ +import Foundation + +final class AssetExchangeFeeEstimatingFactory { + let graphProxy: AssetQuoteFactoryProtocol + let operationQueue: OperationQueue + + // we 10% buffer for fee since swaps to native asset especially volatile + let feeBuffer = BigRational.percent(of: 10) + + init(graphProxy: AssetQuoteFactoryProtocol, operationQueue: OperationQueue) { + self.graphProxy = graphProxy + self.operationQueue = operationQueue + } +} + +extension AssetExchangeFeeEstimatingFactory: ExtrinsicCustomFeeEstimatingFactoryProtocol { + func createCustomFeeEstimator(for chainAsset: ChainAsset) -> ExtrinsicFeeEstimating? { + ExtrinsicAssetConversionFeeEstimator( + chainAsset: chainAsset, + operationQueue: operationQueue, + quoteFactory: graphProxy, + feeBufferInPercentage: feeBuffer + ) + } +} diff --git a/novawallet/Common/Services/AssetExchange/FeeEstimating/AssetExchangeGraphProxy.swift b/novawallet/Common/Services/AssetExchange/FeeEstimating/AssetExchangeGraphProxy.swift new file mode 100644 index 0000000000..4060309410 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/FeeEstimating/AssetExchangeGraphProxy.swift @@ -0,0 +1,68 @@ +import Foundation +import Operation_iOS + +enum AssetExchangeGraphProxyError: Error { + case noGraph + case noRoute(AssetConversion.QuoteArgs) +} + +final class AssetExchangeGraphProxy { + private weak var actualGraph: AssetsExchangeGraphProtocol? + let operationQueue: OperationQueue + let pathCostEstimator: AssetsExchangePathCostEstimating + let logger: LoggerProtocol + let maxQuotePaths: Int + + init( + actualGraph: AssetsExchangeGraphProtocol? = nil, + maxQuotePaths: Int = AssetsExchange.maxQuotePaths, + pathCostEstimator: AssetsExchangePathCostEstimating, + operationQueue: OperationQueue, + logger: LoggerProtocol + ) { + self.actualGraph = actualGraph + self.maxQuotePaths = maxQuotePaths + self.pathCostEstimator = pathCostEstimator + self.operationQueue = operationQueue + self.logger = logger + } + + func install(graph: AssetsExchangeGraphProtocol) { + actualGraph = graph + } +} + +extension AssetExchangeGraphProxy: AssetQuoteFactoryProtocol { + func quote(for args: AssetConversion.QuoteArgs) -> CompoundOperationWrapper { + guard let actualGraph = actualGraph else { + return .createWithError(AssetExchangeGraphProxyError.noGraph) + } + + let possiblePaths = actualGraph.fetchPaths( + from: args.assetIn, + to: args.assetOut, + maxTopPaths: maxQuotePaths + ) + + let routeManager = AssetsExchangeRouteManager( + possiblePaths: possiblePaths, + pathCostEstimator: pathCostEstimator, + operationQueue: operationQueue, + logger: logger + ) + + let bestRouteWrapper = routeManager.fetchRoute(for: args.amount, direction: args.direction) + + let mappingOperation = ClosureOperation { + guard let route = try bestRouteWrapper.targetOperation.extractNoCancellableResultData() else { + throw AssetExchangeGraphProxyError.noRoute(args) + } + + return .init(args: args, amount: route.quote, context: nil) + } + + mappingOperation.addDependency(bestRouteWrapper.targetOperation) + + return bestRouteWrapper.insertingTail(operation: mappingOperation) + } +} diff --git a/novawallet/Common/Services/AssetExchange/HydraExchange/AssetsHydraExchangeEdge.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/AssetsHydraExchangeEdge.swift new file mode 100644 index 0000000000..9ee1abf16b --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/AssetsHydraExchangeEdge.swift @@ -0,0 +1,145 @@ +import Foundation +import Operation_iOS + +protocol AssetsHydraExchangeEdgeProtocol { + var routeComponent: HydraDx.RemoteSwapRoute.Component { get } +} + +class AssetsHydraExchangeEdge { + let origin: ChainAssetId + let destination: ChainAssetId + let remoteSwapPair: HydraDx.RemoteSwapPair + let host: HydraExchangeHostProtocol + + var type: AssetExchangeEdgeType { .hydraSwap } + + init( + origin: ChainAssetId, + destination: ChainAssetId, + remoteSwapPair: HydraDx.RemoteSwapPair, + host: HydraExchangeHostProtocol + ) { + self.origin = origin + self.destination = destination + self.remoteSwapPair = remoteSwapPair + self.host = host + } + + func appendToOperation( + _ operation: AssetExchangeAtomicOperationProtocol, + edge: any HydraExchangeAtomicOperation.Edge, + args: AssetExchangeAtomicOperationArgs + ) -> AssetExchangeAtomicOperationProtocol? { + guard + let hydraOperation = operation as? HydraExchangeAtomicOperation, + let lastEdge = hydraOperation.edges.last, + edge.origin == lastEdge.destination else { + return nil + } + + return HydraExchangeAtomicOperation( + host: hydraOperation.host, + operationArgs: hydraOperation.operationArgs.extending(with: args), + edges: hydraOperation.edges + [edge] + ) + } + + func shouldIgnoreFeeRequirement(after edge: any AssetExchangableGraphEdge) -> Bool { + type == edge.type + } + + func canPayNonNativeFeesInIntermediatePosition() -> Bool { + true + } + + func beginMetaOperation( + for amountIn: Balance, + amountOut: Balance + ) throws -> AssetExchangeMetaOperationProtocol { + guard let assetIn = host.chain.chainAsset(for: origin.assetId) else { + throw ChainModelFetchError.noAsset(assetId: origin.assetId) + } + + guard let assetOut = host.chain.chainAsset(for: destination.assetId) else { + throw ChainModelFetchError.noAsset(assetId: destination.assetId) + } + + return HydraExchangeMetaOperation( + assetIn: assetIn, + assetOut: assetOut, + amountIn: amountIn, + amountOut: amountOut + ) + } + + func appendToMetaOperation( + _ currentOperation: AssetExchangeMetaOperationProtocol, + amountIn _: Balance, + amountOut: Balance + ) throws -> AssetExchangeMetaOperationProtocol? { + guard + let hydraOperation = currentOperation as? HydraExchangeMetaOperation, + hydraOperation.assetOut.chainAssetId == origin else { + return nil + } + + guard let newAssetOut = host.chain.chainAsset(for: destination.assetId) else { + throw ChainModelFetchError.noAsset(assetId: destination.assetId) + } + + return HydraExchangeMetaOperation( + assetIn: currentOperation.assetIn, + assetOut: newAssetOut, + amountIn: hydraOperation.amountIn, + amountOut: amountOut + ) + } + + func beginOperationPrototype() throws -> AssetExchangeOperationPrototypeProtocol { + guard let assetIn = host.chain.chainAsset(for: origin.assetId) else { + throw ChainModelFetchError.noAsset(assetId: origin.assetId) + } + + guard let assetOut = host.chain.chainAsset(for: destination.assetId) else { + throw ChainModelFetchError.noAsset(assetId: destination.assetId) + } + + return HydraExchangeOperationPrototype(assetIn: assetIn, assetOut: assetOut, host: host) + } + + func appendToOperationPrototype( + _ currentPrototype: AssetExchangeOperationPrototypeProtocol + ) throws -> AssetExchangeOperationPrototypeProtocol? { + guard + let hydraOperation = currentPrototype as? HydraExchangeOperationPrototype, + hydraOperation.assetOut.chainAssetId == origin else { + return nil + } + + guard let newAssetOut = host.chain.chainAsset(for: destination.assetId) else { + throw ChainModelFetchError.noAsset(assetId: destination.assetId) + } + + return HydraExchangeOperationPrototype( + assetIn: currentPrototype.assetIn, + assetOut: newAssetOut, + host: host + ) + } +} + +private extension AssetExchangeAtomicOperationArgs { + func extending( + with newOperationArgs: AssetExchangeAtomicOperationArgs + ) -> AssetExchangeAtomicOperationArgs { + .init( + swapLimit: .init( + direction: swapLimit.direction, + amountIn: swapLimit.amountIn, + amountOut: newOperationArgs.swapLimit.amountOut, + slippage: newOperationArgs.swapLimit.slippage + ), + feeAsset: feeAsset + ) + } +} diff --git a/novawallet/Common/Services/AssetExchange/HydraExchange/AssetsHydraExchangeProvider.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/AssetsHydraExchangeProvider.swift new file mode 100644 index 0000000000..757a96bc12 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/AssetsHydraExchangeProvider.swift @@ -0,0 +1,278 @@ +import Foundation +import Operation_iOS +import SubstrateSdk + +final class AssetsHydraExchangeProvider: AssetsExchangeBaseProvider { + private var supportedChains: [ChainModel.Id: ChainModel]? + let selectedWallet: MetaAccountModel + let substrateStorageFacade: StorageFacadeProtocol + let userStorageFacade: StorageFacadeProtocol + let exchangeStateRegistrar: AssetsExchangeStateRegistring + + private var hosts: [ChainModel.Id: HydraExchangeHostProtocol] = [:] + + init( + selectedWallet: MetaAccountModel, + chainRegistry: ChainRegistryProtocol, + pathCostEstimator: AssetsExchangePathCostEstimating, + userStorageFacade: StorageFacadeProtocol, + substrateStorageFacade: StorageFacadeProtocol, + exchangeStateRegistrar: AssetsExchangeStateRegistring, + operationQueue: OperationQueue, + logger: LoggerProtocol + ) { + self.selectedWallet = selectedWallet + self.substrateStorageFacade = substrateStorageFacade + self.userStorageFacade = userStorageFacade + self.exchangeStateRegistrar = exchangeStateRegistrar + + super.init( + chainRegistry: chainRegistry, + pathCostEstimator: pathCostEstimator, + operationQueue: operationQueue, + syncQueue: DispatchQueue(label: "io.novawallet.hydraexchangeprovider.\(UUID().uuidString)"), + logger: logger + ) + } + + private func createOmnipoolExchange( + from host: HydraExchangeHostProtocol, + registeringStateIn stateProviderRegistrar: AssetsExchangeStateRegistring + ) -> AssetsHydraOmnipoolExchange { + let flowState = HydraOmnipoolFlowState( + account: host.selectedAccount, + chain: host.chain, + connection: host.connection, + runtimeProvider: host.runtimeService, + notificationsRegistrar: exchangeStateRegistrar, + operationQueue: host.operationQueue + ) + + stateProviderRegistrar.addStateProvider(flowState) + + return AssetsHydraOmnipoolExchange( + host: host, + tokensFactory: HydraOmnipoolTokensFactory( + chain: host.chain, + runtimeService: host.runtimeService, + connection: host.connection, + operationQueue: host.operationQueue + ), + quoteFactory: HydraOmnipoolQuoteFactory(flowState: flowState), + logger: logger + ) + } + + private func createStableswapExchange( + from host: HydraExchangeHostProtocol, + registeringStateIn stateProviderRegistrar: AssetsExchangeStateRegistring + ) -> AssetsHydraStableswapExchange { + let flowState = HydraStableswapFlowState( + account: host.selectedAccount, + chain: host.chain, + connection: host.connection, + runtimeProvider: host.runtimeService, + notificationsRegistrar: exchangeStateRegistrar, + operationQueue: host.operationQueue + ) + + stateProviderRegistrar.addStateProvider(flowState) + + return AssetsHydraStableswapExchange( + host: host, + swapFactory: .init( + chain: host.chain, + runtimeService: host.runtimeService, + connection: host.connection, + operationQueue: host.operationQueue + ), + quoteFactory: HydraStableswapQuoteFactory(flowState: flowState), + logger: logger + ) + } + + private func createXYKExchange( + from host: HydraExchangeHostProtocol, + registeringStateIn stateProviderRegistrar: AssetsExchangeStateRegistring + ) -> AssetsHydraXYKExchange { + let flowState = HydraXYKFlowState( + account: host.selectedAccount, + chain: host.chain, + connection: host.connection, + runtimeProvider: host.runtimeService, + notificationsRegistrar: exchangeStateRegistrar, + operationQueue: host.operationQueue + ) + + stateProviderRegistrar.addStateProvider(flowState) + + return AssetsHydraXYKExchange( + host: host, + tokensFactory: .init( + chain: host.chain, + runtimeService: host.runtimeService, + connection: host.connection, + operationQueue: host.operationQueue + ), + quoteFactory: .init(flowState: flowState), + logger: logger + ) + } + + // swiftlint:disable:next function_body_length + private func setupHost( + for chain: ChainModel, + account: ChainAccountResponse, + connection: JSONRPCEngine, + runtimeService: RuntimeProviderProtocol + ) -> HydraExchangeHostProtocol { + if let host = hosts[chain.chainId] { + return host + } + + let serviceFactory = ExtrinsicServiceFactory( + runtimeRegistry: runtimeService, + engine: connection, + operationQueue: operationQueue, + userStorageFacade: userStorageFacade, + substrateStorageFacade: substrateStorageFacade + ) + + let customFeeEstimatingFactory = AssetExchangeFeeEstimatingFactory( + graphProxy: graphProxy, + operationQueue: operationQueue + ) + + let extrinsicOperationFactory = serviceFactory.createOperationFactory( + account: account, + chain: chain, + customFeeEstimatingFactory: customFeeEstimatingFactory + ) + + let extrinsicService = serviceFactory.createService( + account: account, + chain: chain, + customFeeEstimatingFactory: customFeeEstimatingFactory + ) + + let submissionMonitorFactory = ExtrinsicSubmissionMonitorFactory( + submissionService: extrinsicService, + statusService: ExtrinsicStatusService( + connection: connection, + runtimeProvider: runtimeService, + eventsQueryFactory: BlockEventsQueryFactory(operationQueue: operationQueue, logger: logger) + ), + operationQueue: operationQueue + ) + + let signingWrapper = SigningWrapperFactory().createSigningWrapper( + for: account.metaId, + accountResponse: account + ) + + let swapParamsService = HydraSwapParamsService( + accountId: account.accountId, + connection: connection, + runtimeProvider: runtimeService, + operationQueue: operationQueue + ) + + swapParamsService.setup() + + let extrinsicParamsFactory = HydraExchangeExtrinsicParamsFactory( + chain: chain, + swapService: swapParamsService, + runtimeProvider: runtimeService + ) + + let host = HydraExchangeHost( + chain: chain, + selectedAccount: account, + submissionMonitorFactory: submissionMonitorFactory, + extrinsicOperationFactory: extrinsicOperationFactory, + extrinsicParamsFactory: extrinsicParamsFactory, + runtimeService: runtimeService, + connection: connection, + signingWrapper: signingWrapper, + executionTimeEstimator: AssetExchangeTimeEstimator(chainRegistry: chainRegistry), + operationQueue: operationQueue, + logger: logger + ) + + hosts[chain.chainId] = host + + return host + } + + private func updateStateIfNeeded() { + guard let supportedChains else { + return + } + + let exchanges: [AssetsExchangeProtocol] = supportedChains.values.flatMap { chain in + guard + let selectedAccount = selectedWallet.fetch(for: chain.accountRequest()), + let runtimeService = chainRegistry.getRuntimeProvider(for: chain.chainId), + let connection = chainRegistry.getConnection(for: chain.chainId) else { + logger.warning("Account or connection/runtime unavailable for \(chain.name)") + return [AssetsExchangeProtocol]() + } + + let swapHost = setupHost( + for: chain, + account: selectedAccount, + connection: connection, + runtimeService: runtimeService + ) + + let omnipoolExchange = createOmnipoolExchange(from: swapHost, registeringStateIn: exchangeStateRegistrar) + + let stableswapExchange = createStableswapExchange(from: swapHost, registeringStateIn: exchangeStateRegistrar) + + let xykExchange = createXYKExchange(from: swapHost, registeringStateIn: exchangeStateRegistrar) + + return [omnipoolExchange, stableswapExchange, xykExchange] + } + + updateState(with: exchanges) + } + + private func handleChains(changes: [DataProviderChange]) -> Bool { + let updatedChains = changes.reduce(into: supportedChains ?? [:]) { accum, change in + switch change { + case let .insert(newItem), let .update(newItem): + accum[newItem.chainId] = newItem.hasSwapHydra ? newItem : nil + case let .delete(deletedIdentifier): + accum[deletedIdentifier] = nil + } + } + + guard supportedChains != updatedChains else { + return false + } + + supportedChains = updatedChains + + return true + } + + // MARK: Subsclass + + override func performSetup() { + chainRegistry.chainsSubscribe( + self, + runningInQueue: syncQueue, + filterStrategy: nil + ) { [weak self] changes in + guard let self, handleChains(changes: changes) else { + return + } + + updateStateIfNeeded() + } + } + + override func performThrottle() { + chainRegistry.chainsUnsubscribe(self) + } +} diff --git a/novawallet/Common/Services/AssetExchange/HydraExchange/HydraExchangeAtomicOperation.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/HydraExchangeAtomicOperation.swift new file mode 100644 index 0000000000..eacfa5e23d --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/HydraExchangeAtomicOperation.swift @@ -0,0 +1,199 @@ +import Foundation +import Operation_iOS + +enum HydraExchangeAtomicOperationError: Error { + case noRoute + case noEventsInResult +} + +final class HydraExchangeAtomicOperation { + typealias Edge = AssetsHydraExchangeEdgeProtocol & AssetExchangableGraphEdge + + let host: HydraExchangeHostProtocol + let edges: [any Edge] + let operationArgs: AssetExchangeAtomicOperationArgs + + var assetIn: ChainAssetId? { + edges.first?.origin + } + + var assetOut: ChainAssetId? { + edges.last?.destination + } + + var chainId: ChainModel.Id? { + assetIn?.chainId + } + + init( + host: HydraExchangeHostProtocol, + operationArgs: AssetExchangeAtomicOperationArgs, + edges: [any Edge] + ) { + self.host = host + self.operationArgs = operationArgs + self.edges = edges + } + + private func createExtrinsicParamsWrapper( + for swapLimit: AssetExchangeSwapLimit + ) -> CompoundOperationWrapper { + guard let assetIn, let assetOut else { + return .createWithError(HydraExchangeAtomicOperationError.noRoute) + } + + return OperationCombiningService.compoundNonOptionalWrapper( + operationQueue: host.operationQueue + ) { + let callArgs = AssetConversion.CallArgs( + assetIn: assetIn, + amountIn: swapLimit.amountIn, + assetOut: assetOut, + amountOut: swapLimit.amountOut, + receiver: self.host.selectedAccount.accountId, + direction: swapLimit.direction, + slippage: swapLimit.slippage + ) + + let routeComponents = self.edges.map(\.routeComponent) + let route = HydraDx.RemoteSwapRoute(components: routeComponents) + + return self.host.extrinsicParamsFactory.createOperationWrapper(for: route, callArgs: callArgs) + } + } + + private func createFeeWrapper() -> CompoundOperationWrapper { + let paramsWrapper = createExtrinsicParamsWrapper(for: operationArgs.swapLimit) + + let feeWrapper = OperationCombiningService.compoundNonOptionalWrapper( + operationQueue: host.operationQueue + ) { + let params = try paramsWrapper.targetOperation.extractNoCancellableResultData() + + let feeWrapper = self.host.extrinsicOperationFactory.estimateFeeOperation({ builder in + try HydraExchangeExtrinsicConverter.addingOperation( + from: params, + builder: builder + ) + }, payingIn: self.operationArgs.feeAsset) + + return feeWrapper + } + + feeWrapper.addDependency(wrapper: paramsWrapper) + + return feeWrapper.insertingHead(operations: paramsWrapper.allOperations) + } +} + +extension HydraExchangeAtomicOperation: AssetExchangeAtomicOperationProtocol { + func executeWrapper(for swapLimit: AssetExchangeSwapLimit) -> CompoundOperationWrapper { + let paramsWrapper = createExtrinsicParamsWrapper(for: swapLimit) + + let executionWrapper = OperationCombiningService.compoundNonOptionalWrapper( + operationQueue: host.operationQueue + ) { + let params = try paramsWrapper.targetOperation.extractNoCancellableResultData() + + let submittionWrapper = self.host.submissionMonitorFactory.submitAndMonitorWrapper( + extrinsicBuilderClosure: { builder in + try HydraExchangeExtrinsicConverter.addingOperation( + from: params, + builder: builder + ) + }, + payingIn: self.operationArgs.feeAsset, + signer: self.host.signingWrapper, + matchingEvents: HydraSwapEventsMatcher() + ) + + let codingFactoryOperation = self.host.runtimeService.fetchCoderFactoryOperation() + + let monitorOperation = ClosureOperation { + let submittionResult = try submittionWrapper.targetOperation.extractNoCancellableResultData() + let codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + + switch submittionResult { + case let .success(executionResult): + let eventParser = AssetsHydraExchangeDepositParser(logger: self.host.logger) + + self.host.logger.debug("Execution success: \(executionResult.interestedEvents)") + + guard let amountOut = eventParser.extractDeposit( + from: executionResult.interestedEvents, + using: codingFactory + ) else { + throw HydraExchangeAtomicOperationError.noEventsInResult + } + + self.host.logger.debug("Arrived amount: \(String(amountOut))") + + return amountOut + case let .failure(executionFailure): + throw executionFailure.error + } + } + + monitorOperation.addDependency(submittionWrapper.targetOperation) + monitorOperation.addDependency(codingFactoryOperation) + + return submittionWrapper + .insertingHead(operations: [codingFactoryOperation]) + .insertingTail(operation: monitorOperation) + } + + executionWrapper.addDependency(wrapper: paramsWrapper) + + return executionWrapper.insertingHead(operations: paramsWrapper.allOperations) + } + + func estimateFee() -> CompoundOperationWrapper { + let feeWrapper = createFeeWrapper() + + let mappingOperation = ClosureOperation { + let extrinsicFee = try feeWrapper.targetOperation.extractNoCancellableResultData() + + return AssetExchangeOperationFee(extrinsicFee: extrinsicFee, args: self.operationArgs) + } + + mappingOperation.addDependency(feeWrapper.targetOperation) + + return feeWrapper.insertingTail(operation: mappingOperation) + } + + func requiredAmountToGetAmountOut( + _ amountOutClosure: @escaping () throws -> Balance + ) -> CompoundOperationWrapper { + let quoteWrapper: CompoundOperationWrapper? = edges.reversed().reduce(nil) { prevWrapper, edge in + let quoteWrapper: CompoundOperationWrapper = OperationCombiningService.compoundNonOptionalWrapper( + operationManager: OperationManager(operationQueue: self.host.operationQueue) + ) { + if let prevWrapper { + let amountOut = try prevWrapper.targetOperation.extractNoCancellableResultData() + return edge.quote(amount: amountOut, direction: .buy) + } else { + let amountOut = try amountOutClosure() + return edge.quote(amount: amountOut, direction: .buy) + } + } + + if let prevWrapper { + quoteWrapper.addDependency(wrapper: prevWrapper) + + return quoteWrapper.insertingHead(operations: prevWrapper.allOperations) + } else { + return quoteWrapper + } + } + + guard let quoteWrapper else { + return .createWithError(HydraExchangeAtomicOperationError.noRoute) + } + + return quoteWrapper + } + + var swapLimit: AssetExchangeSwapLimit { + operationArgs.swapLimit + } +} diff --git a/novawallet/Common/Services/AssetExchange/HydraExchange/HydraExchangeExtrinsicConverter.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/HydraExchangeExtrinsicConverter.swift new file mode 100644 index 0000000000..8b7781f937 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/HydraExchangeExtrinsicConverter.swift @@ -0,0 +1,65 @@ +import Foundation +import SubstrateSdk + +enum HydraExchangeExtrinsicConverter { + static func addingOperation( + from params: HydraExchangeSwapParams, + builder: ExtrinsicBuilderProtocol + ) throws -> ExtrinsicBuilderProtocol { + var currentBuilder = builder + + if let updateReferralCall = params.updateReferral { + currentBuilder = try currentBuilder.adding(call: updateReferralCall.runtimeCall()) + } + + switch params.swap { + case let .omniSell(call): + currentBuilder = try currentBuilder.adding(call: call.runtimeCall()) + case let .omniBuy(call): + currentBuilder = try currentBuilder.adding(call: call.runtimeCall()) + case let .routedSell(call): + currentBuilder = try currentBuilder.adding(call: call.runtimeCall()) + case let .routedBuy(call): + currentBuilder = try currentBuilder.adding(call: call.runtimeCall()) + } + + return currentBuilder + } + + static func isOmnipoolSwap(route: HydraDx.RemoteSwapRoute) -> Bool { + guard route.components.count == 1 else { + return false + } + + if case .omnipool = route.components[0].type { + return true + } else { + return false + } + } + + static func convertRouteToTrade(_ route: HydraDx.RemoteSwapRoute) -> [HydraRouter.Trade] { + route.components.map { component in + switch component.type { + case .omnipool: + return HydraRouter.Trade( + pool: .omnipool, + assetIn: component.assetIn, + assetOut: component.assetOut + ) + case let .stableswap(poolAsset): + return HydraRouter.Trade( + pool: .stableswap(poolAsset), + assetIn: component.assetIn, + assetOut: component.assetOut + ) + case .xyk: + return HydraRouter.Trade( + pool: .xyk, + assetIn: component.assetIn, + assetOut: component.assetOut + ) + } + } + } +} diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraExtrinsicOperationFactory.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/HydraExchangeExtrinsicParamsFactory.swift similarity index 80% rename from novawallet/Modules/AssetConversion/Service/HydraDx/HydraExtrinsicOperationFactory.swift rename to novawallet/Common/Services/AssetExchange/HydraExchange/HydraExchangeExtrinsicParamsFactory.swift index c6350dc11a..41fdbe60da 100644 --- a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraExtrinsicOperationFactory.swift +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/HydraExchangeExtrinsicParamsFactory.swift @@ -1,9 +1,8 @@ import Foundation import Operation_iOS -struct HydraSwapParams { +struct HydraExchangeSwapParams { struct Params { - let newFeeCurrency: ChainAssetId let referral: AccountId? var shouldSetReferral: Bool { @@ -23,14 +22,14 @@ struct HydraSwapParams { let swap: Operation } -protocol HydraExtrinsicOperationFactoryProtocol { +protocol HydraExchangeExtrinsicParamsFactoryProtocol { func createOperationWrapper( - for feeAsset: ChainAsset, + for route: HydraDx.RemoteSwapRoute, callArgs: AssetConversion.CallArgs - ) -> CompoundOperationWrapper + ) -> CompoundOperationWrapper } -final class HydraExtrinsicOperationFactory { +final class HydraExchangeExtrinsicParamsFactory { let chain: ChainModel let swapService: HydraSwapParamsService let runtimeProvider: RuntimeCodingServiceProtocol @@ -50,7 +49,7 @@ final class HydraExtrinsicOperationFactory { remoteAssetOut: HydraDx.AssetId, callArgs: AssetConversion.CallArgs, route: HydraDx.RemoteSwapRoute - ) -> HydraSwapParams.Operation { + ) -> HydraExchangeSwapParams.Operation { switch callArgs.direction { case .sell: let amountOutMin = callArgs.amountOut - callArgs.slippage.mul(value: callArgs.amountOut) @@ -102,29 +101,22 @@ final class HydraExtrinsicOperationFactory { } private func createSwapParams( - from params: HydraSwapParams.Params, + from params: HydraExchangeSwapParams.Params, remoteAssetIn: HydraDx.AssetId, remoteAssetOut: HydraDx.AssetId, + route: HydraDx.RemoteSwapRoute, callArgs: AssetConversion.CallArgs - ) throws -> HydraSwapParams { + ) throws -> HydraExchangeSwapParams { let referralCall: HydraDx.LinkReferralCodeCall? if params.shouldSetReferral { - guard let code = HydraConstants.novaReferralCode.data(using: .utf8) else { - throw CommonError.dataCorruption - } + let code = try HydraConstants.novaReferralCode.data(using: .utf8).mapOrThrow(CommonError.dataCorruption) referralCall = .init(code: code) } else { referralCall = nil } - guard let context = callArgs.context else { - throw CommonError.dataCorruption - } - - let route: HydraDx.RemoteSwapRoute = try JsonStringify.decodeFromString(context) - let operation = createOperation( for: remoteAssetIn, remoteAssetOut: remoteAssetOut, @@ -132,7 +124,7 @@ final class HydraExtrinsicOperationFactory { route: route ) - return HydraSwapParams( + return HydraExchangeSwapParams( params: params, updateReferral: referralCall, swap: operation @@ -142,14 +134,14 @@ final class HydraExtrinsicOperationFactory { private func createSwapOperationWrapper( assetIn: ChainAsset, assetOut: ChainAsset, - feeAsset: ChainAsset, + route: HydraDx.RemoteSwapRoute, callArgs: AssetConversion.CallArgs - ) -> CompoundOperationWrapper { + ) -> CompoundOperationWrapper { let codingFactoryOperation = runtimeProvider.fetchCoderFactoryOperation() let swapParamsOperation = swapService.createFetchOperation() - let mergeOperation = ClosureOperation { + let mergeOperation = ClosureOperation { let codingFactory = try codingFactoryOperation.extractNoCancellableResultData() let swapParams = try swapParamsOperation.extractNoCancellableResultData() @@ -163,15 +155,13 @@ final class HydraExtrinsicOperationFactory { codingFactory: codingFactory ).remoteAssetId - let params = HydraSwapParams.Params( - newFeeCurrency: feeAsset.chainAssetId, - referral: swapParams.referralLink - ) + let params = HydraExchangeSwapParams.Params(referral: swapParams.referralLink) return try self.createSwapParams( from: params, remoteAssetIn: remoteAssetIn, remoteAssetOut: remoteAssetOut, + route: route, callArgs: callArgs ) } @@ -186,19 +176,19 @@ final class HydraExtrinsicOperationFactory { } } -extension HydraExtrinsicOperationFactory: HydraExtrinsicOperationFactoryProtocol { +extension HydraExchangeExtrinsicParamsFactory: HydraExchangeExtrinsicParamsFactoryProtocol { func createOperationWrapper( - for feeAsset: ChainAsset, + for route: HydraDx.RemoteSwapRoute, callArgs: AssetConversion.CallArgs - ) -> CompoundOperationWrapper { + ) -> CompoundOperationWrapper { guard let assetIn = chain.asset(for: callArgs.assetIn.assetId) else { - return CompoundOperationWrapper.createWithError( + return .createWithError( ChainModelFetchError.noAsset(assetId: callArgs.assetIn.assetId) ) } guard let assetOut = chain.asset(for: callArgs.assetOut.assetId) else { - return CompoundOperationWrapper.createWithError( + return .createWithError( ChainModelFetchError.noAsset(assetId: callArgs.assetOut.assetId) ) } @@ -206,7 +196,7 @@ extension HydraExtrinsicOperationFactory: HydraExtrinsicOperationFactoryProtocol return createSwapOperationWrapper( assetIn: .init(chain: chain, asset: assetIn), assetOut: .init(chain: chain, asset: assetOut), - feeAsset: feeAsset, + route: route, callArgs: callArgs ) } diff --git a/novawallet/Common/Services/AssetExchange/HydraExchange/HydraExchangeHost.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/HydraExchangeHost.swift new file mode 100644 index 0000000000..41757a4791 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/HydraExchangeHost.swift @@ -0,0 +1,57 @@ +import Foundation +import SubstrateSdk + +protocol HydraExchangeHostProtocol { + var chain: ChainModel { get } + var selectedAccount: ChainAccountResponse { get } + var submissionMonitorFactory: ExtrinsicSubmitMonitorFactoryProtocol { get } + var extrinsicOperationFactory: ExtrinsicOperationFactoryProtocol { get } + var extrinsicParamsFactory: HydraExchangeExtrinsicParamsFactoryProtocol { get } + var signingWrapper: SigningWrapperProtocol { get } + var runtimeService: RuntimeProviderProtocol { get } + var connection: JSONRPCEngine { get } + var executionTimeEstimator: AssetExchangeTimeEstimating { get } + var operationQueue: OperationQueue { get } + var logger: LoggerProtocol { get } +} + +final class HydraExchangeHost: HydraExchangeHostProtocol { + let chain: ChainModel + let selectedAccount: ChainAccountResponse + + let submissionMonitorFactory: ExtrinsicSubmitMonitorFactoryProtocol + let extrinsicOperationFactory: ExtrinsicOperationFactoryProtocol + let extrinsicParamsFactory: HydraExchangeExtrinsicParamsFactoryProtocol + let executionTimeEstimator: AssetExchangeTimeEstimating + let runtimeService: RuntimeProviderProtocol + let connection: JSONRPCEngine + let signingWrapper: SigningWrapperProtocol + let operationQueue: OperationQueue + let logger: LoggerProtocol + + init( + chain: ChainModel, + selectedAccount: ChainAccountResponse, + submissionMonitorFactory: ExtrinsicSubmitMonitorFactoryProtocol, + extrinsicOperationFactory: ExtrinsicOperationFactoryProtocol, + extrinsicParamsFactory: HydraExchangeExtrinsicParamsFactoryProtocol, + runtimeService: RuntimeProviderProtocol, + connection: JSONRPCEngine, + signingWrapper: SigningWrapperProtocol, + executionTimeEstimator: AssetExchangeTimeEstimating, + operationQueue: OperationQueue, + logger: LoggerProtocol + ) { + self.chain = chain + self.selectedAccount = selectedAccount + self.submissionMonitorFactory = submissionMonitorFactory + self.extrinsicOperationFactory = extrinsicOperationFactory + self.extrinsicParamsFactory = extrinsicParamsFactory + self.runtimeService = runtimeService + self.connection = connection + self.signingWrapper = signingWrapper + self.executionTimeEstimator = executionTimeEstimator + self.operationQueue = operationQueue + self.logger = logger + } +} diff --git a/novawallet/Common/Services/AssetExchange/HydraExchange/HydraExchangeMetaOperation.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/HydraExchangeMetaOperation.swift new file mode 100644 index 0000000000..10ce0f3a8b --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/HydraExchangeMetaOperation.swift @@ -0,0 +1,7 @@ +import Foundation + +class HydraExchangeMetaOperation: AssetExchangeBaseMetaOperation {} + +extension HydraExchangeMetaOperation: AssetExchangeMetaOperationProtocol { + var label: AssetExchangeMetaOperationLabel { .swap } +} diff --git a/novawallet/Common/Services/AssetExchange/HydraExchange/HydraExchangeOperationPrototype.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/HydraExchangeOperationPrototype.swift new file mode 100644 index 0000000000..a06ed73bdf --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/HydraExchangeOperationPrototype.swift @@ -0,0 +1,26 @@ +import Foundation +import Operation_iOS + +final class HydraExchangeOperationPrototype: AssetExchangeBaseOperationPrototype { + let host: HydraExchangeHostProtocol + + init(assetIn: ChainAsset, assetOut: ChainAsset, host: HydraExchangeHostProtocol) { + self.host = host + + super.init(assetIn: assetIn, assetOut: assetOut) + } +} + +extension HydraExchangeOperationPrototype: AssetExchangeOperationPrototypeProtocol { + func estimatedCostInUsdt(using converter: AssetExchageUsdtConverting) throws -> Decimal { + guard let nativeAsset = assetIn.chain.utilityChainAsset() else { + throw ChainModelFetchError.noAsset(assetId: AssetModel.utilityAssetId) + } + + return converter.convertToUsdt(the: nativeAsset, decimalAmount: 0.5) ?? 0 + } + + func estimatedExecutionTimeWrapper() -> CompoundOperationWrapper { + host.executionTimeEstimator.totalTimeWrapper(for: [host.chain.chainId]) + } +} diff --git a/novawallet/Common/Services/AssetExchange/HydraExchange/HydraSwapEventParser.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/HydraSwapEventParser.swift new file mode 100644 index 0000000000..ede621555d --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/HydraSwapEventParser.swift @@ -0,0 +1,41 @@ +import Foundation + +final class AssetsHydraExchangeDepositParser { + let logger: LoggerProtocol + + init(logger: LoggerProtocol) { + self.logger = logger + } + + func extractDeposit(from events: [Event], using codingFactory: RuntimeCoderFactoryProtocol) -> Balance? { + guard let event = events.last else { + return nil + } + + do { + let codingPath = codingFactory.metadata.createEventCodingPath(from: event) + + switch codingPath { + case HydraRouter.routeExecutedPath: + let parsedEvent: HydraRouter.RouteExecutedEvent = try ExtrinsicExtraction.getEventParams( + from: event, + context: codingFactory.createRuntimeJsonContext() + ) + + return parsedEvent.amountOut + case HydraOmnipool.sellExecutedPath, HydraOmnipool.buyExecutedPath: + let parsedEvent: HydraOmnipool.SwapExecuted = try ExtrinsicExtraction.getEventParams( + from: event, + context: codingFactory.createRuntimeJsonContext() + ) + + return parsedEvent.amountOut + default: + return nil + } + } catch { + logger.error("Event parsing error: \(error)") + return nil + } + } +} diff --git a/novawallet/Common/Services/AssetExchange/HydraExchange/HydraSwapEventsMatcher.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/HydraSwapEventsMatcher.swift new file mode 100644 index 0000000000..0b3d9c552b --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/HydraSwapEventsMatcher.swift @@ -0,0 +1,14 @@ +import Foundation + +final class HydraSwapEventsMatcher: ExtrinsicEventsMatching { + func match(event: Event, using codingFactory: RuntimeCoderFactoryProtocol) -> Bool { + codingFactory.metadata.eventMatches( + event, + oneOf: [ + HydraRouter.routeExecutedPath, + HydraOmnipool.sellExecutedPath, + HydraOmnipool.buyExecutedPath + ] + ) + } +} diff --git a/novawallet/Common/Services/AssetExchange/HydraExchange/Omnipool/AssetsHydraOmnipoolExchange.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/Omnipool/AssetsHydraOmnipoolExchange.swift new file mode 100644 index 0000000000..64dd4b5e86 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/Omnipool/AssetsHydraOmnipoolExchange.swift @@ -0,0 +1,73 @@ +import Foundation +import Operation_iOS + +final class AssetsHydraOmnipoolExchange { + let tokensFactory: HydraOmnipoolTokensFactory + let quoteFactory: HydraOmnipoolQuoteFactory + let host: HydraExchangeHostProtocol + let logger: LoggerProtocol + + init( + host: HydraExchangeHostProtocol, + tokensFactory: HydraOmnipoolTokensFactory, + quoteFactory: HydraOmnipoolQuoteFactory, + logger: LoggerProtocol + ) { + self.tokensFactory = tokensFactory + self.quoteFactory = quoteFactory + self.host = host + self.logger = logger + } +} + +extension AssetsHydraOmnipoolExchange: AssetsExchangeProtocol { + func availableDirectSwapConnections() -> CompoundOperationWrapper<[any AssetExchangableGraphEdge]> { + let codingFactoryOpertion = host.runtimeService.fetchCoderFactoryOperation() + let remoteAssetsWrapper = tokensFactory.fetchAllRemoteAssets() + + let mappingOperation = ClosureOperation<[any AssetExchangableGraphEdge]> { + let codingFactory = try codingFactoryOpertion.extractNoCancellableResultData() + let remoteAssets = try remoteAssetsWrapper.targetOperation.extractNoCancellableResultData() + + let remoteLocalMapping = try HydraDxTokenConverter.convertToRemoteLocalMapping( + remoteAssets: remoteAssets, + chain: self.host.chain, + codingFactory: codingFactory, + failureClosure: { self.logger.warning("Token \($0) conversion failed: \($1)") } + ) + + return remoteAssets.flatMap { remoteAssetIn in + guard let localAssetIn = remoteLocalMapping[remoteAssetIn] else { + self.logger.error("Skipped remote in \(remoteAssetIn) as no mapping found") + return [AnyAssetExchangeEdge]() + } + + let otherAssets = remoteAssets.subtracting([remoteAssetIn]) + + return otherAssets.compactMap { remoteAssetOut in + guard let localAssetOut = remoteLocalMapping[remoteAssetOut] else { + self.logger.error("Skipped remote out \(remoteAssetOut) as no mapping found") + return nil + } + + let edge = HydraOmnipoolExchangeEdge( + origin: localAssetIn, + destination: localAssetOut, + remoteSwapPair: .init(assetIn: remoteAssetIn, assetOut: remoteAssetOut), + host: self.host, + quoteFactory: self.quoteFactory + ) + + return AnyAssetExchangeEdge(edge) + } + } + } + + mappingOperation.addDependency(remoteAssetsWrapper.targetOperation) + mappingOperation.addDependency(codingFactoryOpertion) + + return remoteAssetsWrapper + .insertingHead(operations: [codingFactoryOpertion]) + .insertingTail(operation: mappingOperation) + } +} diff --git a/novawallet/Common/Services/AssetExchange/HydraExchange/Omnipool/HydraOmnipoolExchangeEdge.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/Omnipool/HydraOmnipoolExchangeEdge.swift new file mode 100644 index 0000000000..48e25a9525 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/Omnipool/HydraOmnipoolExchangeEdge.swift @@ -0,0 +1,70 @@ +import Foundation +import Operation_iOS + +final class HydraOmnipoolExchangeEdge: AssetsHydraExchangeEdge { + let quoteFactory: HydraOmnipoolQuoteFactory + + init( + origin: ChainAssetId, + destination: ChainAssetId, + remoteSwapPair: HydraDx.RemoteSwapPair, + host: HydraExchangeHostProtocol, + quoteFactory: HydraOmnipoolQuoteFactory + ) { + self.quoteFactory = quoteFactory + + super.init( + origin: origin, + destination: destination, + remoteSwapPair: remoteSwapPair, + host: host + ) + } +} + +extension HydraOmnipoolExchangeEdge: AssetsHydraExchangeEdgeProtocol { + var routeComponent: HydraDx.RemoteSwapRoute.Component { + .init( + assetIn: remoteSwapPair.assetIn, + assetOut: remoteSwapPair.assetOut, + type: .omnipool + ) + } +} + +extension HydraOmnipoolExchangeEdge: AssetExchangableGraphEdge { + var weight: Int { AssetsExchange.defaultEdgeWeight - 2 } + + func quote( + amount: Balance, + direction: AssetConversion.Direction + ) -> CompoundOperationWrapper { + quoteFactory.quote( + for: .init( + assetIn: remoteSwapPair.assetIn, + assetOut: remoteSwapPair.assetOut, + amount: amount, + direction: direction + ) + ) + } + + func beginOperation(for args: AssetExchangeAtomicOperationArgs) throws -> AssetExchangeAtomicOperationProtocol { + HydraExchangeAtomicOperation( + host: host, + operationArgs: args, + edges: [self] + ) + } + + func appendToOperation( + _ operation: AssetExchangeAtomicOperationProtocol, + args: AssetExchangeAtomicOperationArgs + ) -> AssetExchangeAtomicOperationProtocol? { + appendToOperation( + operation, + edge: self, + args: args + ) + } +} diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/Omnipool/HydraOmnipoolFlowState.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/Omnipool/HydraOmnipoolFlowState.swift similarity index 73% rename from novawallet/Modules/AssetConversion/Service/HydraDx/Omnipool/HydraOmnipoolFlowState.swift rename to novawallet/Common/Services/AssetExchange/HydraExchange/Omnipool/HydraOmnipoolFlowState.swift index 556d984995..17ee32505f 100644 --- a/novawallet/Modules/AssetConversion/Service/HydraDx/Omnipool/HydraOmnipoolFlowState.swift +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/Omnipool/HydraOmnipoolFlowState.swift @@ -8,6 +8,7 @@ final class HydraOmnipoolFlowState { let connection: JSONRPCEngine let runtimeProvider: RuntimeProviderProtocol let operationQueue: OperationQueue + let notificationsRegistrar: AssetsExchangeStateRegistring? let mutex = NSLock() @@ -18,17 +19,22 @@ final class HydraOmnipoolFlowState { chain: ChainModel, connection: JSONRPCEngine, runtimeProvider: RuntimeProviderProtocol, + notificationsRegistrar: AssetsExchangeStateRegistring?, operationQueue: OperationQueue ) { self.account = account self.chain = chain self.connection = connection self.runtimeProvider = runtimeProvider + self.notificationsRegistrar = notificationsRegistrar self.operationQueue = operationQueue } deinit { - quoteStateServices.values.forEach { $0.throttle() } + quoteStateServices.values.forEach { + notificationsRegistrar?.deregisterStateService($0) + $0.throttle() + } } } @@ -50,7 +56,11 @@ extension HydraOmnipoolFlowState { mutex.unlock() } - quoteStateServices.values.forEach { $0.throttle() } + quoteStateServices.values.forEach { + notificationsRegistrar?.deregisterStateService($0) + $0.throttle() + } + quoteStateServices = [:] } @@ -74,11 +84,18 @@ extension HydraOmnipoolFlowState { operationQueue: operationQueue ) - quoteStateServices[swapPair]?.throttle() quoteStateServices[swapPair] = newService newService.setup() + notificationsRegistrar?.registerStateService(newService) + return newService } } + +extension HydraOmnipoolFlowState: AssetsExchangeStateProviding { + func throttleStateServices() { + resetServices() + } +} diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/Omnipool/HydraOmnipoolQuoteFactory.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/Omnipool/HydraOmnipoolQuoteFactory.swift similarity index 100% rename from novawallet/Modules/AssetConversion/Service/HydraDx/Omnipool/HydraOmnipoolQuoteFactory.swift rename to novawallet/Common/Services/AssetExchange/HydraExchange/Omnipool/HydraOmnipoolQuoteFactory.swift diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/Omnipool/HydraOmnipoolQuoteParams.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/Omnipool/HydraOmnipoolQuoteParams.swift similarity index 100% rename from novawallet/Modules/AssetConversion/Service/HydraDx/Omnipool/HydraOmnipoolQuoteParams.swift rename to novawallet/Common/Services/AssetExchange/HydraExchange/Omnipool/HydraOmnipoolQuoteParams.swift diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/Omnipool/HydraOmnipoolQuoteParamsService.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/Omnipool/HydraOmnipoolQuoteParamsService.swift similarity index 98% rename from novawallet/Modules/AssetConversion/Service/HydraDx/Omnipool/HydraOmnipoolQuoteParamsService.swift rename to novawallet/Common/Services/AssetExchange/HydraExchange/Omnipool/HydraOmnipoolQuoteParamsService.swift index 57c9fb12fc..798960f2fd 100644 --- a/novawallet/Modules/AssetConversion/Service/HydraDx/Omnipool/HydraOmnipoolQuoteParamsService.swift +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/Omnipool/HydraOmnipoolQuoteParamsService.swift @@ -42,7 +42,7 @@ final class HydraOmnipoolQuoteParamsService: ObservableSubscriptionSyncService CompoundOperationWrapper> { + func fetchAllRemoteAssets() -> CompoundOperationWrapper> { let codingFactoryOperation = runtimeService.fetchCoderFactoryOperation() let hubAssetIdOperation = PrimitiveConstantOperation.operation( diff --git a/novawallet/Common/Services/AssetExchange/HydraExchange/Stableswap/AssetsHydraStableswapExchange.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/Stableswap/AssetsHydraStableswapExchange.swift new file mode 100644 index 0000000000..41cd0d3ea8 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/Stableswap/AssetsHydraStableswapExchange.swift @@ -0,0 +1,81 @@ +import Foundation +import Operation_iOS + +final class AssetsHydraStableswapExchange { + let swapFactory: HydraStableswapTokensFactory + let quoteFactory: HydraStableswapQuoteFactory + let host: HydraExchangeHostProtocol + let logger: LoggerProtocol + + init( + host: HydraExchangeHostProtocol, + swapFactory: HydraStableswapTokensFactory, + quoteFactory: HydraStableswapQuoteFactory, + logger: LoggerProtocol + ) { + self.host = host + self.swapFactory = swapFactory + self.quoteFactory = quoteFactory + self.logger = logger + } +} + +extension AssetsHydraStableswapExchange: AssetsExchangeProtocol { + func availableDirectSwapConnections() -> CompoundOperationWrapper<[any AssetExchangableGraphEdge]> { + let codingFactoryOpertion = host.runtimeService.fetchCoderFactoryOperation() + let allPoolsWrapper = swapFactory.fetchRemotePools() + + let mappingOperation = ClosureOperation<[any AssetExchangableGraphEdge]> { + let allPools = try allPoolsWrapper.targetOperation.extractNoCancellableResultData() + let codingFactory = try codingFactoryOpertion.extractNoCancellableResultData() + + let allRemoteAssets = Set(allPools.flatMap(\.value) + allPools.keys) + + let remoteLocalMapping = try HydraDxTokenConverter.convertToRemoteLocalMapping( + remoteAssets: allRemoteAssets, + chain: self.host.chain, + codingFactory: codingFactory, + failureClosure: { self.logger.warning("Token \($0) conversion failed: \($1)") } + ) + + return allPools.flatMap { keyValue in + let remotePoolAsset = keyValue.key + let remotePoolAssets = Set(keyValue.value + [remotePoolAsset]) + + return remotePoolAssets.flatMap { remoteAssetIn in + guard let localAssetIn = remoteLocalMapping[remoteAssetIn] else { + self.logger.warning("Skipped remote in \(remoteAssetIn) as no mapping found") + return [AnyAssetExchangeEdge]() + } + + let otherAssets = remotePoolAssets.subtracting([remoteAssetIn]) + + return otherAssets.compactMap { remoteAssetOut in + guard let localAssetOut = remoteLocalMapping[remoteAssetOut] else { + self.logger.warning("Skipped remote out \(remoteAssetOut) as no mapping found") + return nil + } + + let edge = HydraStableswapExchangeEdge( + origin: localAssetIn, + destination: localAssetOut, + remoteSwapPair: .init(assetIn: remoteAssetIn, assetOut: remoteAssetOut), + poolAsset: remotePoolAsset, + host: self.host, + quoteFactory: self.quoteFactory + ) + + return AnyAssetExchangeEdge(edge) + } + } + } + } + + mappingOperation.addDependency(codingFactoryOpertion) + mappingOperation.addDependency(allPoolsWrapper.targetOperation) + + return allPoolsWrapper + .insertingHead(operations: [codingFactoryOpertion]) + .insertingTail(operation: mappingOperation) + } +} diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/Stableswap/HydraStableswapApi.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/Stableswap/HydraStableswapApi.swift similarity index 100% rename from novawallet/Modules/AssetConversion/Service/HydraDx/Stableswap/HydraStableswapApi.swift rename to novawallet/Common/Services/AssetExchange/HydraExchange/Stableswap/HydraStableswapApi.swift diff --git a/novawallet/Common/Services/AssetExchange/HydraExchange/Stableswap/HydraStableswapExchangeEdge.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/Stableswap/HydraStableswapExchangeEdge.swift new file mode 100644 index 0000000000..b0feb7732b --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/Stableswap/HydraStableswapExchangeEdge.swift @@ -0,0 +1,74 @@ +import Foundation +import Operation_iOS + +final class HydraStableswapExchangeEdge: AssetsHydraExchangeEdge { + let quoteFactory: HydraStableswapQuoteFactory + let poolAsset: HydraDx.AssetId + + init( + origin: ChainAssetId, + destination: ChainAssetId, + remoteSwapPair: HydraDx.RemoteSwapPair, + poolAsset: HydraDx.AssetId, + host: HydraExchangeHostProtocol, + quoteFactory: HydraStableswapQuoteFactory + ) { + self.quoteFactory = quoteFactory + self.poolAsset = poolAsset + + super.init( + origin: origin, + destination: destination, + remoteSwapPair: remoteSwapPair, + host: host + ) + } +} + +extension HydraStableswapExchangeEdge: AssetsHydraExchangeEdgeProtocol { + var routeComponent: HydraDx.RemoteSwapRoute.Component { + .init( + assetIn: remoteSwapPair.assetIn, + assetOut: remoteSwapPair.assetOut, + type: .stableswap(poolAsset) + ) + } +} + +extension HydraStableswapExchangeEdge: AssetExchangableGraphEdge { + var weight: Int { AssetsExchange.defaultEdgeWeight - 2 } + + func quote( + amount: Balance, + direction: AssetConversion.Direction + ) -> CompoundOperationWrapper { + quoteFactory.quote( + for: .init( + assetIn: remoteSwapPair.assetIn, + assetOut: remoteSwapPair.assetOut, + poolAsset: poolAsset, + amount: amount, + direction: direction + ) + ) + } + + func beginOperation(for args: AssetExchangeAtomicOperationArgs) throws -> AssetExchangeAtomicOperationProtocol { + HydraExchangeAtomicOperation( + host: host, + operationArgs: args, + edges: [self] + ) + } + + func appendToOperation( + _ operation: AssetExchangeAtomicOperationProtocol, + args: AssetExchangeAtomicOperationArgs + ) -> AssetExchangeAtomicOperationProtocol? { + appendToOperation( + operation, + edge: self, + args: args + ) + } +} diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/Stableswap/HydraStableswapFlowState.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/Stableswap/HydraStableswapFlowState.swift similarity index 74% rename from novawallet/Modules/AssetConversion/Service/HydraDx/Stableswap/HydraStableswapFlowState.swift rename to novawallet/Common/Services/AssetExchange/HydraExchange/Stableswap/HydraStableswapFlowState.swift index 068129a098..22e1da1db6 100644 --- a/novawallet/Modules/AssetConversion/Service/HydraDx/Stableswap/HydraStableswapFlowState.swift +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/Stableswap/HydraStableswapFlowState.swift @@ -8,6 +8,7 @@ final class HydraStableswapFlowState { let connection: JSONRPCEngine let runtimeProvider: RuntimeProviderProtocol let operationQueue: OperationQueue + let notificationsRegistrar: AssetsExchangeStateRegistring? let mutex = NSLock() @@ -18,17 +19,22 @@ final class HydraStableswapFlowState { chain: ChainModel, connection: JSONRPCEngine, runtimeProvider: RuntimeProviderProtocol, + notificationsRegistrar: AssetsExchangeStateRegistring?, operationQueue: OperationQueue ) { self.account = account self.chain = chain self.connection = connection self.runtimeProvider = runtimeProvider + self.notificationsRegistrar = notificationsRegistrar self.operationQueue = operationQueue } deinit { - quoteStateServices.values.forEach { $0.throttle() } + quoteStateServices.values.forEach { + notificationsRegistrar?.deregisterStateService($0) + $0.throttle() + } } } @@ -50,7 +56,11 @@ extension HydraStableswapFlowState { mutex.unlock() } - quoteStateServices.values.forEach { $0.throttle() } + quoteStateServices.values.forEach { + notificationsRegistrar?.deregisterStateService($0) + $0.throttle() + } + quoteStateServices = [:] } @@ -77,11 +87,18 @@ extension HydraStableswapFlowState { operationQueue: operationQueue ) - quoteStateServices[poolPair]?.throttle() quoteStateServices[poolPair] = newService newService.setup() + notificationsRegistrar?.registerStateService(newService) + return newService } } + +extension HydraStableswapFlowState: AssetsExchangeStateProviding { + func throttleStateServices() { + resetServices() + } +} diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/Stableswap/HydraStableswapPoolService.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/Stableswap/HydraStableswapPoolService.swift similarity index 97% rename from novawallet/Modules/AssetConversion/Service/HydraDx/Stableswap/HydraStableswapPoolService.swift rename to novawallet/Common/Services/AssetExchange/HydraExchange/Stableswap/HydraStableswapPoolService.swift index c1fedaaca2..320e1fe7ee 100644 --- a/novawallet/Modules/AssetConversion/Service/HydraDx/Stableswap/HydraStableswapPoolService.swift +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/Stableswap/HydraStableswapPoolService.swift @@ -66,7 +66,7 @@ final class HydraStableswapPoolService: ObservableSubscriptionSyncService CompoundOperationWrapper<[ChainAssetId: Set]> { + private func fetchAllLocalPairs( + for chain: ChainModel + ) -> CompoundOperationWrapper<[ChainAssetId: Set]> { let codingFactoryOperation = runtimeService.fetchCoderFactoryOperation() let remotePoolsWrapper = fetchAllPools(dependingOn: codingFactoryOperation) @@ -161,7 +163,7 @@ final class HydraStableSwapsTokensFactory { } } -extension HydraStableSwapsTokensFactory: HydraPoolTokensFactoryProtocol { +extension HydraStableswapTokensFactory: HydraPoolTokensFactoryProtocol { func availableDirections() -> CompoundOperationWrapper<[ChainAssetId: Set]> { fetchAllLocalPairs(for: chain) } @@ -185,4 +187,13 @@ extension HydraStableSwapsTokensFactory: HydraPoolTokensFactoryProtocol { func fetchAllLocalPoolAssets() -> CompoundOperationWrapper> { fetchAllLocalPoolAssets(for: chain) } + + func fetchRemotePools() -> CompoundOperationWrapper<[HydraDx.AssetId: [HydraDx.AssetId]]> { + let codingFactoryOperation = runtimeService.fetchCoderFactoryOperation() + let fetchWrapper = fetchAllPools(dependingOn: codingFactoryOperation) + + fetchWrapper.addDependency(operations: [codingFactoryOperation]) + + return fetchWrapper.insertingHead(operations: [codingFactoryOperation]) + } } diff --git a/novawallet/Common/Services/AssetExchange/HydraExchange/XYKPool/AssetsHydraXYKExchange.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/XYKPool/AssetsHydraXYKExchange.swift new file mode 100644 index 0000000000..193cca7f7e --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/XYKPool/AssetsHydraXYKExchange.swift @@ -0,0 +1,77 @@ +import Foundation +import Operation_iOS + +final class AssetsHydraXYKExchange { + let host: HydraExchangeHostProtocol + let tokensFactory: HydraXYKPoolTokensFactory + let quoteFactory: HydraXYKSwapQuoteFactory + let logger: LoggerProtocol + + init( + host: HydraExchangeHostProtocol, + tokensFactory: HydraXYKPoolTokensFactory, + quoteFactory: HydraXYKSwapQuoteFactory, + logger: LoggerProtocol + ) { + self.host = host + self.tokensFactory = tokensFactory + self.quoteFactory = quoteFactory + self.logger = logger + } +} + +extension AssetsHydraXYKExchange: AssetsExchangeProtocol { + func availableDirectSwapConnections() -> CompoundOperationWrapper<[any AssetExchangableGraphEdge]> { + let codingFactoryOperation = host.runtimeService.fetchCoderFactoryOperation() + let remotePairsWrapper = tokensFactory.fetchAllRemotePairsWrapper(dependingOn: codingFactoryOperation) + + remotePairsWrapper.addDependency(operations: [codingFactoryOperation]) + + let mappingOperation = ClosureOperation<[any AssetExchangableGraphEdge]> { + let codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + let remotePairs = try remotePairsWrapper.targetOperation.extractNoCancellableResultData() + let remoteAssets = Set(remotePairs.values.flatMap { [$0.asset1, $0.asset2] }) + + let remoteLocalMapping = try HydraDxTokenConverter.convertToRemoteLocalMapping( + remoteAssets: remoteAssets, + chain: self.host.chain, + codingFactory: codingFactory, + failureClosure: { self.logger.error("Token \($0) conversion failed: \($1)") } + ) + + let edges: [AnyAssetExchangeEdge] = remotePairs.values.flatMap { remotePair in + guard + let localAsset1 = remoteLocalMapping[remotePair.asset1], + let localAsset2 = remoteLocalMapping[remotePair.asset2] else { + return [AnyAssetExchangeEdge]() + } + + let edge1 = AssetsHydraXYKExchangeEdge( + origin: localAsset1, + destination: localAsset2, + remoteSwapPair: .init(assetIn: remotePair.asset1, assetOut: remotePair.asset2), + host: self.host, + quoteFactory: self.quoteFactory + ) + + let edge2 = AssetsHydraXYKExchangeEdge( + origin: localAsset2, + destination: localAsset1, + remoteSwapPair: .init(assetIn: remotePair.asset2, assetOut: remotePair.asset1), + host: self.host, + quoteFactory: self.quoteFactory + ) + + return [AnyAssetExchangeEdge(edge1), AnyAssetExchangeEdge(edge2)] + } + + return edges + } + + mappingOperation.addDependency(remotePairsWrapper.targetOperation) + + return remotePairsWrapper + .insertingHead(operations: [codingFactoryOperation]) + .insertingTail(operation: mappingOperation) + } +} diff --git a/novawallet/Common/Services/AssetExchange/HydraExchange/XYKPool/HydraXYKExchangeEdge.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/XYKPool/HydraXYKExchangeEdge.swift new file mode 100644 index 0000000000..9bd981cb3d --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/XYKPool/HydraXYKExchangeEdge.swift @@ -0,0 +1,70 @@ +import Foundation +import Operation_iOS + +final class AssetsHydraXYKExchangeEdge: AssetsHydraExchangeEdge { + let quoteFactory: HydraXYKSwapQuoteFactory + + init( + origin: ChainAssetId, + destination: ChainAssetId, + remoteSwapPair: HydraDx.RemoteSwapPair, + host: HydraExchangeHostProtocol, + quoteFactory: HydraXYKSwapQuoteFactory + ) { + self.quoteFactory = quoteFactory + + super.init( + origin: origin, + destination: destination, + remoteSwapPair: remoteSwapPair, + host: host + ) + } +} + +extension AssetsHydraXYKExchangeEdge: AssetsHydraExchangeEdgeProtocol { + var routeComponent: HydraDx.RemoteSwapRoute.Component { + .init( + assetIn: remoteSwapPair.assetIn, + assetOut: remoteSwapPair.assetOut, + type: .xyk + ) + } +} + +extension AssetsHydraXYKExchangeEdge: AssetExchangableGraphEdge { + var weight: Int { AssetsExchange.defaultEdgeWeight - 1 } + + func quote( + amount: Balance, + direction: AssetConversion.Direction + ) -> CompoundOperationWrapper { + quoteFactory.quote( + for: .init( + assetIn: remoteSwapPair.assetIn, + assetOut: remoteSwapPair.assetOut, + amount: amount, + direction: direction + ) + ) + } + + func beginOperation(for args: AssetExchangeAtomicOperationArgs) throws -> AssetExchangeAtomicOperationProtocol { + HydraExchangeAtomicOperation( + host: host, + operationArgs: args, + edges: [self] + ) + } + + func appendToOperation( + _ operation: AssetExchangeAtomicOperationProtocol, + args: AssetExchangeAtomicOperationArgs + ) -> AssetExchangeAtomicOperationProtocol? { + appendToOperation( + operation, + edge: self, + args: args + ) + } +} diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/XYKPool/HydraXYKFlowState.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/XYKPool/HydraXYKFlowState.swift similarity index 73% rename from novawallet/Modules/AssetConversion/Service/HydraDx/XYKPool/HydraXYKFlowState.swift rename to novawallet/Common/Services/AssetExchange/HydraExchange/XYKPool/HydraXYKFlowState.swift index beed2f8870..5980872c4b 100644 --- a/novawallet/Modules/AssetConversion/Service/HydraDx/XYKPool/HydraXYKFlowState.swift +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/XYKPool/HydraXYKFlowState.swift @@ -8,6 +8,7 @@ final class HydraXYKFlowState { let connection: JSONRPCEngine let runtimeProvider: RuntimeProviderProtocol let operationQueue: OperationQueue + let notificationsRegistrar: AssetsExchangeStateRegistring? let mutex = NSLock() @@ -18,17 +19,22 @@ final class HydraXYKFlowState { chain: ChainModel, connection: JSONRPCEngine, runtimeProvider: RuntimeProviderProtocol, + notificationsRegistrar: AssetsExchangeStateRegistring?, operationQueue: OperationQueue ) { self.account = account self.chain = chain self.connection = connection self.runtimeProvider = runtimeProvider + self.notificationsRegistrar = notificationsRegistrar self.operationQueue = operationQueue } deinit { - quoteStateServices.values.forEach { $0.throttle() } + quoteStateServices.values.forEach { + notificationsRegistrar?.deregisterStateService($0) + $0.throttle() + } } } @@ -50,7 +56,11 @@ extension HydraXYKFlowState { mutex.unlock() } - quoteStateServices.values.forEach { $0.throttle() } + quoteStateServices.values.forEach { + notificationsRegistrar?.deregisterStateService($0) + $0.throttle() + } + quoteStateServices = [:] } @@ -74,11 +84,18 @@ extension HydraXYKFlowState { operationQueue: operationQueue ) - quoteStateServices[swapPair]?.throttle() quoteStateServices[swapPair] = newService newService.setup() + notificationsRegistrar?.registerStateService(newService) + return newService } } + +extension HydraXYKFlowState: AssetsExchangeStateProviding { + func throttleStateServices() { + resetServices() + } +} diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/XYKPool/HydraXYKPoolTokensFactory.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/XYKPool/HydraXYKPoolTokensFactory.swift similarity index 98% rename from novawallet/Modules/AssetConversion/Service/HydraDx/XYKPool/HydraXYKPoolTokensFactory.swift rename to novawallet/Common/Services/AssetExchange/HydraExchange/XYKPool/HydraXYKPoolTokensFactory.swift index af2ce87af1..610ebdc085 100644 --- a/novawallet/Modules/AssetConversion/Service/HydraDx/XYKPool/HydraXYKPoolTokensFactory.swift +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/XYKPool/HydraXYKPoolTokensFactory.swift @@ -20,7 +20,7 @@ final class HydraXYKPoolTokensFactory { self.operationQueue = operationQueue } - private func fetchAllRemotePairsWrapper( + func fetchAllRemotePairsWrapper( dependingOn codingFactoryOperation: BaseOperation ) -> CompoundOperationWrapper<[AccountIdKey: HydraXYK.PoolAssets]> { let requestFactory = StorageRequestFactory( diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/XYKPool/HydraXYKQuoteParams.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/XYKPool/HydraXYKQuoteParams.swift similarity index 100% rename from novawallet/Modules/AssetConversion/Service/HydraDx/XYKPool/HydraXYKQuoteParams.swift rename to novawallet/Common/Services/AssetExchange/HydraExchange/XYKPool/HydraXYKQuoteParams.swift diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/XYKPool/HydraXYKQuoteParamsService.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/XYKPool/HydraXYKQuoteParamsService.swift similarity index 98% rename from novawallet/Modules/AssetConversion/Service/HydraDx/XYKPool/HydraXYKQuoteParamsService.swift rename to novawallet/Common/Services/AssetExchange/HydraExchange/XYKPool/HydraXYKQuoteParamsService.swift index d9f5049a8f..bbc88ece2c 100644 --- a/novawallet/Modules/AssetConversion/Service/HydraDx/XYKPool/HydraXYKQuoteParamsService.swift +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/XYKPool/HydraXYKQuoteParamsService.swift @@ -42,7 +42,7 @@ final class HydraXYKQuoteParamsService: ObservableSubscriptionSyncService Decimal? + func convertToAssetDecimalFromUsdt(amount: Decimal, asset: ChainAsset) -> Decimal? +} + +extension AssetExchageUsdtConverting { + func convertToUsdt(the asset: ChainAsset, amountInPlank: Balance) -> Decimal? { + let decimalAmount = amountInPlank.decimal(assetInfo: asset.assetDisplayInfo) + + return convertToUsdt(the: asset, decimalAmount: decimalAmount) + } + + func convertToAssetInPlankFromUsdt(amount: Decimal, asset: ChainAsset) -> Balance? { + let decimalAmount = convertToAssetDecimalFromUsdt(amount: amount, asset: asset) + + return decimalAmount?.toSubstrateAmount(precision: asset.assetDisplayInfo.assetPrecision) + } +} + +final class AssetExchageUsdtConverter { + let priceStore: AssetExchangePriceStoring + let usdtTiedAsset: ChainAssetId + + init(priceStore: AssetExchangePriceStoring, usdtTiedAsset: ChainAssetId) { + self.priceStore = priceStore + self.usdtTiedAsset = usdtTiedAsset + } +} + +extension AssetExchageUsdtConverter: AssetExchageUsdtConverting { + func convertToUsdt(the asset: ChainAsset, decimalAmount: Decimal) -> Decimal? { + guard + let usdtPriceRate = priceStore.fetchPrice(for: usdtTiedAsset)?.decimalRate, + let assetPriceRate = priceStore.fetchPrice(for: asset.chainAssetId)?.decimalRate, + usdtPriceRate > 0 else { + return nil + } + + return decimalAmount * assetPriceRate / usdtPriceRate + } + + func convertToAssetDecimalFromUsdt(amount: Decimal, asset: ChainAsset) -> Decimal? { + guard + let usdtPriceRate = priceStore.fetchPrice(for: usdtTiedAsset)?.decimalRate, + let assetPriceRate = priceStore.fetchPrice(for: asset.chainAssetId)?.decimalRate, + assetPriceRate > 0 else { + return nil + } + + return amount * usdtPriceRate / assetPriceRate + } +} diff --git a/novawallet/Common/Services/AssetExchange/Price/AssetExchangePrice.swift b/novawallet/Common/Services/AssetExchange/Price/AssetExchangePrice.swift new file mode 100644 index 0000000000..b700b99770 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/Price/AssetExchangePrice.swift @@ -0,0 +1,6 @@ +import Foundation + +protocol AssetExchangePriceStoring { + func getCurrencyId() -> Int? + func fetchPrice(for chainAssetId: ChainAssetId) -> PriceData? +} diff --git a/novawallet/Common/Services/AssetExchange/Price/AssetExchangePriceStore.swift b/novawallet/Common/Services/AssetExchange/Price/AssetExchangePriceStore.swift new file mode 100644 index 0000000000..6585748e13 --- /dev/null +++ b/novawallet/Common/Services/AssetExchange/Price/AssetExchangePriceStore.swift @@ -0,0 +1,26 @@ +import Foundation + +final class AssetExchangePriceStore { + @Atomic(defaultValue: [:]) private var store: [ChainAssetId: PriceData] + + init(assetListObservable: AssetListModelObservable, updateQueue: DispatchQueue = .global()) { + store = (try? assetListObservable.state.value.priceResult?.get()) ?? [:] + + assetListObservable.addObserver( + with: self, + queue: updateQueue + ) { [weak self] _, newState in + self?.store = (try? newState.value.priceResult?.get()) ?? [:] + } + } +} + +extension AssetExchangePriceStore: AssetExchangePriceStoring { + func fetchPrice(for chainAssetId: ChainAssetId) -> PriceData? { + store[chainAssetId] + } + + func getCurrencyId() -> Int? { + store.values.first?.currencyId + } +} diff --git a/novawallet/Common/Services/ChainRegistry/ChainRegistry+AsyncWait.swift b/novawallet/Common/Services/ChainRegistry/ChainRegistry+AsyncWait.swift index 3b1a254a94..7d1a1f4ee1 100644 --- a/novawallet/Common/Services/ChainRegistry/ChainRegistry+AsyncWait.swift +++ b/novawallet/Common/Services/ChainRegistry/ChainRegistry+AsyncWait.swift @@ -30,4 +30,63 @@ extension ChainRegistryProtocol { return CompoundOperationWrapper(targetOperation: operation) } + + func asyncWaitChainOrErrorWrapper( + for chainId: ChainModel.Id, + workQueue: DispatchQueue = .global() + ) -> CompoundOperationWrapper { + let wrapper = asyncWaitChainWrapper(for: chainId, workQueue: workQueue) + + let mappingOperation = ClosureOperation { + try wrapper.targetOperation.extractNoCancellableResultData().mapOrThrow( + ChainRegistryError.noChain(chainId) + ) + } + + mappingOperation.addDependency(wrapper.targetOperation) + + return wrapper.insertingTail(operation: mappingOperation) + } + + func asyncWaitChainAssetOrError( + for chainAssetId: ChainAssetId, + workQueue: DispatchQueue = .global() + ) -> CompoundOperationWrapper { + let chainWrapper = asyncWaitChainWrapper(for: chainAssetId.chainId, workQueue: workQueue) + + let mappingOperation = ClosureOperation { + let chain = try chainWrapper.targetOperation.extractNoCancellableResultData().mapOrThrow( + ChainRegistryError.noChain(chainAssetId.chainId) + ) + + return try chain.chainAsset(for: chainAssetId.assetId).mapOrThrow( + ChainRegistryError.noChainAsset(chainAssetId) + ) + } + + mappingOperation.addDependency(chainWrapper.targetOperation) + + return chainWrapper.insertingTail(operation: mappingOperation) + } + + func asyncWaitUtilityAssetOrError( + for chainId: ChainModel.Id, + workQueue: DispatchQueue = .global() + ) -> CompoundOperationWrapper { + let chainWrapper = asyncWaitChainWrapper(for: chainId, workQueue: workQueue) + + let mappingOperation = ClosureOperation { + let chain = try chainWrapper.targetOperation.extractNoCancellableResultData().mapOrThrow( + ChainRegistryError.noChain(chainId) + ) + + return try chain.utilityChainAsset().mapOrThrow( + ChainRegistryError.noUtilityAsset(chainId) + ) + } + + mappingOperation.addDependency(chainWrapper.targetOperation) + + return chainWrapper.insertingTail(operation: mappingOperation) + } } diff --git a/novawallet/Common/Services/ChainRegistry/ChainRegistry+Get.swift b/novawallet/Common/Services/ChainRegistry/ChainRegistry+Get.swift new file mode 100644 index 0000000000..ab273b2400 --- /dev/null +++ b/novawallet/Common/Services/ChainRegistry/ChainRegistry+Get.swift @@ -0,0 +1,27 @@ +import Foundation + +extension ChainRegistryProtocol { + func getConnectionOrError(for chainId: ChainModel.Id) throws -> ChainConnection { + guard let connection = getConnection(for: chainId) else { + throw ChainRegistryError.connectionUnavailable + } + + return connection + } + + func getRuntimeProviderOrError(for chainId: ChainModel.Id) throws -> RuntimeProviderProtocol { + guard let runtimeProvider = getRuntimeProvider(for: chainId) else { + throw ChainRegistryError.runtimeMetadaUnavailable + } + + return runtimeProvider + } + + func getChainOrError(for chainId: ChainModel.Id) throws -> ChainModel { + guard let chain = getChain(for: chainId) else { + throw ChainRegistryError.noChain(chainId) + } + + return chain + } +} diff --git a/novawallet/Common/Services/ChainRegistry/ChainRegistryError.swift b/novawallet/Common/Services/ChainRegistry/ChainRegistryError.swift index a7741c3d1f..22f921399e 100644 --- a/novawallet/Common/Services/ChainRegistry/ChainRegistryError.swift +++ b/novawallet/Common/Services/ChainRegistry/ChainRegistryError.swift @@ -4,4 +4,6 @@ enum ChainRegistryError: Error { case connectionUnavailable case runtimeMetadaUnavailable case noChain(ChainModel.Id) + case noChainAsset(ChainAssetId) + case noUtilityAsset(ChainModel.Id) } diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicService.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicService.swift index 896f1e4909..f759ba0cea 100644 --- a/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicService.swift +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicService.swift @@ -263,10 +263,16 @@ final class ExtrinsicService { notificationClosure: @escaping ExtrinsicSubscriptionStatusClosure ) { do { + let extrinsicHash = try Data(hexString: extrinsic).blake2b32().toHex(includePrefix: true) let updateClosure: (ExtrinsicSubscriptionUpdate) -> Void = { update in let status = update.params.result + let model = ExtrinsicStatusUpdate( + extrinsicHash: extrinsicHash, + extrinsicStatus: status + ) + queue.async { - notificationClosure(.success(status)) + notificationClosure(.success(model)) } } diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicServiceFactory.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicServiceFactory.swift index dcada1f2c2..8f56264002 100644 --- a/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicServiceFactory.swift +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicServiceFactory.swift @@ -9,11 +9,25 @@ protocol ExtrinsicServiceFactoryProtocol { extensions: [ExtrinsicSignedExtending] ) -> ExtrinsicServiceProtocol + func createService( + account: ChainAccountResponse, + chain: ChainModel, + extensions: [ExtrinsicSignedExtending], + customFeeEstimatingFactory: ExtrinsicCustomFeeEstimatingFactoryProtocol + ) -> ExtrinsicServiceProtocol + func createOperationFactory( account: ChainAccountResponse, chain: ChainModel, extensions: [ExtrinsicSignedExtending] ) -> ExtrinsicOperationFactoryProtocol + + func createOperationFactory( + account: ChainAccountResponse, + chain: ChainModel, + extensions: [ExtrinsicSignedExtending], + customFeeEstimatingFactory: ExtrinsicCustomFeeEstimatingFactoryProtocol + ) -> ExtrinsicOperationFactoryProtocol } extension ExtrinsicServiceFactoryProtocol { @@ -31,14 +45,13 @@ extension ExtrinsicServiceFactoryProtocol { func createService( account: ChainAccountResponse, chain: ChainModel, - feeAssetConversionId: AssetConversionPallet.AssetId + customFeeEstimatingFactory: ExtrinsicCustomFeeEstimatingFactoryProtocol ) -> ExtrinsicServiceProtocol { createService( account: account, chain: chain, - extensions: ExtrinsicSignedExtensionFacade().createFactory(for: chain.chainId).createExtensions( - payingFeeIn: feeAssetConversionId - ) + extensions: ExtrinsicSignedExtensionFacade().createFactory(for: chain.chainId).createExtensions(), + customFeeEstimatingFactory: customFeeEstimatingFactory ) } @@ -53,6 +66,19 @@ extension ExtrinsicServiceFactoryProtocol { ) } + func createOperationFactory( + account: ChainAccountResponse, + chain: ChainModel, + customFeeEstimatingFactory: ExtrinsicCustomFeeEstimatingFactoryProtocol + ) -> ExtrinsicOperationFactoryProtocol { + createOperationFactory( + account: account, + chain: chain, + extensions: ExtrinsicSignedExtensionFacade().createFactory(for: chain.chainId).createExtensions(), + customFeeEstimatingFactory: customFeeEstimatingFactory + ) + } + func createOperationFactory( account: ChainAccountResponse, chain: ChainModel, @@ -109,17 +135,54 @@ extension ExtrinsicServiceFactory: ExtrinsicServiceFactoryProtocol { userStorageFacade: userStorageFacade ) - let feeEstimatingWrapperFactory = ExtrinsicFeeEstimatingWrapperFactory( + let extrinsicFeeHost = ExtrinsicFeeEstimatorHost( account: account, chain: chain, - runtimeService: runtimeRegistry, connection: engine, + runtimeProvider: runtimeRegistry, + userStorageFacade: userStorageFacade, + substrateStorageFacade: substrateStorageFacade, operationQueue: operationQueue ) let feeEstimationRegistry = ExtrinsicFeeEstimationRegistry( chain: chain, - estimatingWrapperFactory: feeEstimatingWrapperFactory, + estimatingWrapperFactory: ExtrinsicFeeEstimatingWrapperFactory( + host: extrinsicFeeHost, + customFeeEstimatorFactory: AssetConversionFeeEstimatingFactory(host: extrinsicFeeHost) + ), + feeInstallingWrapperFactory: ExtrinsicFeeInstallingWrapperFactory( + customFeeInstallerFactory: AssetConversionFeeInstallingFactory(host: extrinsicFeeHost) + ) + ) + + return ExtrinsicService( + chain: chain, + runtimeRegistry: runtimeRegistry, + senderResolvingFactory: senderResolvingFactory, + metadataHashOperationFactory: metadataHashOperationFactory, + feeEstimationRegistry: feeEstimationRegistry, + extensions: extensions, + engine: engine, + operationManager: OperationManager(operationQueue: operationQueue) + ) + } + + func createService( + account: ChainAccountResponse, + chain: ChainModel, + extensions: [ExtrinsicSignedExtending], + customFeeEstimatingFactory: ExtrinsicCustomFeeEstimatingFactoryProtocol + ) -> ExtrinsicServiceProtocol { + let senderResolvingFactory = ExtrinsicSenderResolutionFactory( + chainAccount: account, + chain: chain, + userStorageFacade: userStorageFacade + ) + + let extrinsicFeeHost = ExtrinsicFeeEstimatorHost( + account: account, + chain: chain, connection: engine, runtimeProvider: runtimeRegistry, userStorageFacade: userStorageFacade, @@ -127,6 +190,17 @@ extension ExtrinsicServiceFactory: ExtrinsicServiceFactoryProtocol { operationQueue: operationQueue ) + let feeEstimationRegistry = ExtrinsicFeeEstimationRegistry( + chain: chain, + estimatingWrapperFactory: ExtrinsicFeeEstimatingWrapperFactory( + host: extrinsicFeeHost, + customFeeEstimatorFactory: customFeeEstimatingFactory + ), + feeInstallingWrapperFactory: ExtrinsicFeeInstallingWrapperFactory( + customFeeInstallerFactory: AssetConversionFeeInstallingFactory(host: extrinsicFeeHost) + ) + ) + return ExtrinsicService( chain: chain, runtimeRegistry: runtimeRegistry, @@ -149,16 +223,56 @@ extension ExtrinsicServiceFactory: ExtrinsicServiceFactoryProtocol { chain: chain, userStorageFacade: userStorageFacade ) - let feeEstimatingWrapperFactory = ExtrinsicFeeEstimatingWrapperFactory( + + let extrinsicFeeHost = ExtrinsicFeeEstimatorHost( account: account, chain: chain, - runtimeService: runtimeRegistry, connection: engine, + runtimeProvider: runtimeRegistry, + userStorageFacade: userStorageFacade, + substrateStorageFacade: substrateStorageFacade, operationQueue: operationQueue ) + let feeEstimationRegistry = ExtrinsicFeeEstimationRegistry( chain: chain, - estimatingWrapperFactory: feeEstimatingWrapperFactory, + estimatingWrapperFactory: ExtrinsicFeeEstimatingWrapperFactory( + host: extrinsicFeeHost, + customFeeEstimatorFactory: AssetConversionFeeEstimatingFactory(host: extrinsicFeeHost) + ), + feeInstallingWrapperFactory: ExtrinsicFeeInstallingWrapperFactory( + customFeeInstallerFactory: AssetConversionFeeInstallingFactory(host: extrinsicFeeHost) + ) + ) + + return ExtrinsicOperationFactory( + chain: chain, + runtimeRegistry: runtimeRegistry, + customExtensions: extensions, + engine: engine, + feeEstimationRegistry: feeEstimationRegistry, + metadataHashOperationFactory: metadataHashOperationFactory, + senderResolvingFactory: senderResolvingFactory, + blockHashOperationFactory: BlockHashOperationFactory(), + operationManager: OperationManager(operationQueue: operationQueue) + ) + } + + func createOperationFactory( + account: ChainAccountResponse, + chain: ChainModel, + extensions: [ExtrinsicSignedExtending], + customFeeEstimatingFactory: ExtrinsicCustomFeeEstimatingFactoryProtocol + ) -> ExtrinsicOperationFactoryProtocol { + let senderResolvingFactory = ExtrinsicSenderResolutionFactory( + chainAccount: account, + chain: chain, + userStorageFacade: userStorageFacade + ) + + let extrinsicFeeHost = ExtrinsicFeeEstimatorHost( + account: account, + chain: chain, connection: engine, runtimeProvider: runtimeRegistry, userStorageFacade: userStorageFacade, @@ -166,6 +280,17 @@ extension ExtrinsicServiceFactory: ExtrinsicServiceFactoryProtocol { operationQueue: operationQueue ) + let feeEstimationRegistry = ExtrinsicFeeEstimationRegistry( + chain: chain, + estimatingWrapperFactory: ExtrinsicFeeEstimatingWrapperFactory( + host: extrinsicFeeHost, + customFeeEstimatorFactory: customFeeEstimatingFactory + ), + feeInstallingWrapperFactory: ExtrinsicFeeInstallingWrapperFactory( + customFeeInstallerFactory: AssetConversionFeeInstallingFactory(host: extrinsicFeeHost) + ) + ) + return ExtrinsicOperationFactory( chain: chain, runtimeRegistry: runtimeRegistry, diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicServiceTypes.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicServiceTypes.swift index 953788662b..7ba99c4786 100644 --- a/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicServiceTypes.swift +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicServiceTypes.swift @@ -67,7 +67,7 @@ typealias ExtrinsicSubmitClosure = (SubmitExtrinsicResult) -> Void typealias ExtrinsicSubmitIndexedClosure = (SubmitIndexedExtrinsicResult) -> Void typealias ExtrinsicSubscriptionIdClosure = (UInt16) -> Bool -typealias ExtrinsicSubscriptionStatusClosure = (Result) -> Void +typealias ExtrinsicSubscriptionStatusClosure = (Result) -> Void typealias ExtrinsicBuilderClosure = (ExtrinsicBuilderProtocol) throws -> (ExtrinsicBuilderProtocol) typealias ExtrinsicBuilderIndexedClosure = (ExtrinsicBuilderProtocol, Int) throws -> (ExtrinsicBuilderProtocol) diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicStatusService/BlockEventsQueryFactory.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicStatusService/BlockEventsQueryFactory.swift new file mode 100644 index 0000000000..98e9202c41 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicStatusService/BlockEventsQueryFactory.swift @@ -0,0 +1,121 @@ +import Foundation +import Operation_iOS +import SubstrateSdk + +protocol BlockEventsQueryFactoryProtocol { + func queryBlockDetailsWrapper( + from connection: JSONRPCEngine, + runtimeProvider: RuntimeProviderProtocol, + blockHash: Data + ) -> CompoundOperationWrapper +} + +final class BlockEventsQueryFactory { + let storageRequestFactory: StorageRequestFactoryProtocol + let eventsRepository: SubstrateEventsRepositoryProtocol + let logger: LoggerProtocol + + init( + operationQueue: OperationQueue, + eventsRepository: SubstrateEventsRepositoryProtocol = SubstrateEventsRepository(), + logger: LoggerProtocol = Logger.shared + ) { + storageRequestFactory = StorageRequestFactory( + remoteFactory: StorageKeyFactory(), + operationManager: OperationManager(operationQueue: operationQueue) + ) + + self.eventsRepository = eventsRepository + + self.logger = logger + } + + private func createEventsWrapper( + dependingOn coderFactoryOperation: BaseOperation, + connection: JSONRPCEngine, + blockHash: Data + ) -> CompoundOperationWrapper> { + storageRequestFactory.queryItem( + engine: connection, + factory: { + try coderFactoryOperation.extractNoCancellableResultData() + }, + storagePath: SystemPallet.eventsPath, + at: blockHash + ) + } + + private func createBlockFetchOperation( + for connection: JSONRPCEngine, + blockHash: Data + ) -> JSONRPCOperation<[String], SignedBlock> { + JSONRPCOperation( + engine: connection, + method: RPCMethod.getChainBlock, + parameters: [blockHash.toHex(includePrefix: true)] + ) + } + + private func createParsingExtrinsicEventsOperation( + dependingOn eventsOperation: BaseOperation>, + blockOperation: BaseOperation, + repository: SubstrateEventsRepositoryProtocol, + logger: LoggerProtocol + ) -> BaseOperation { + ClosureOperation { + let block = try blockOperation.extractNoCancellableResultData().block + + logger.debug("Block received: \(block)") + + let eventRecords = try eventsOperation.extractNoCancellableResultData().value ?? [] + + logger.debug("Events received: \(eventRecords)") + + let extrinsicsWithEvents = repository.getExtrinsicsEvents(from: block, eventRecords: eventRecords) + let inherentEvents = repository.getInherentEvents(from: eventRecords) + + return SubstrateBlockDetails( + extrinsicsWithEvents: extrinsicsWithEvents, + inherentsEvents: inherentEvents + ) + } + } +} + +extension BlockEventsQueryFactory: BlockEventsQueryFactoryProtocol { + func queryBlockDetailsWrapper( + from connection: JSONRPCEngine, + runtimeProvider: RuntimeProviderProtocol, + blockHash: Data + ) -> CompoundOperationWrapper { + let codingFactoryOperation = runtimeProvider.fetchCoderFactoryOperation() + + let eventsWrapper = createEventsWrapper( + dependingOn: codingFactoryOperation, + connection: connection, + blockHash: blockHash + ) + + eventsWrapper.addDependency(operations: [codingFactoryOperation]) + + let blockFetchOperation = createBlockFetchOperation( + for: connection, + blockHash: blockHash + ) + + let parsingOperation = createParsingExtrinsicEventsOperation( + dependingOn: eventsWrapper.targetOperation, + blockOperation: blockFetchOperation, + repository: eventsRepository, + logger: logger + ) + + parsingOperation.addDependency(eventsWrapper.targetOperation) + parsingOperation.addDependency(blockFetchOperation) + + return eventsWrapper + .insertingHead(operations: [codingFactoryOperation]) + .insertingTail(operation: blockFetchOperation) + .insertingTail(operation: parsingOperation) + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicStatusService/ExtrinsicEventsMatching.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicStatusService/ExtrinsicEventsMatching.swift new file mode 100644 index 0000000000..3e1fb285f6 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicStatusService/ExtrinsicEventsMatching.swift @@ -0,0 +1,22 @@ +import Foundation +import SubstrateSdk + +enum ExtrinsicEventsMatcherError: Error { + case eventCodingPathFailed +} + +protocol ExtrinsicEventsMatching { + func match(event: Event, using codingFactory: RuntimeCoderFactoryProtocol) -> Bool +} + +struct ExtrinsicSuccessEventMatcher: ExtrinsicEventsMatching { + func match(event: Event, using codingFactory: RuntimeCoderFactoryProtocol) -> Bool { + codingFactory.metadata.eventMatches(event, path: SystemPallet.extrinsicSuccessEventPath) + } +} + +struct ExtrinsicFailureEventMatcher: ExtrinsicEventsMatching { + func match(event: Event, using codingFactory: RuntimeCoderFactoryProtocol) -> Bool { + codingFactory.metadata.eventMatches(event, path: SystemPallet.extrinsicFailedEventPath) + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicStatusService/ExtrinsicStatusService.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicStatusService/ExtrinsicStatusService.swift new file mode 100644 index 0000000000..d18802f90f --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicStatusService/ExtrinsicStatusService.swift @@ -0,0 +1,136 @@ +import Foundation +import Operation_iOS +import SubstrateSdk + +protocol ExtrinsicStatusServiceProtocol { + func fetchExtrinsicStatusForHash( + _ extrinsicHash: String, + inBlock blockHash: String, + matchingEvents: ExtrinsicEventsMatching? + ) -> CompoundOperationWrapper +} + +enum ExtrinsicStatusServiceError: Error { + case extrinsicNotFound(Data) + case terminateEventNotFound(SubstrateExtrinsicEvents) +} + +final class ExtrinsicStatusService { + let connection: JSONRPCEngine + let runtimeProvider: RuntimeProviderProtocol + let eventsQueryFactory: BlockEventsQueryFactoryProtocol + + init( + connection: JSONRPCEngine, + runtimeProvider: RuntimeProviderProtocol, + eventsQueryFactory: BlockEventsQueryFactoryProtocol + ) { + self.connection = connection + self.runtimeProvider = runtimeProvider + self.eventsQueryFactory = eventsQueryFactory + } + + private func createMatchingWrapper( + dependingOn queryOperation: BaseOperation, + runtimeProvider: RuntimeProviderProtocol, + extrinsicHash: Data, + blockHash: BlockHash, + interstedEventsMatcher: ExtrinsicEventsMatching? + ) -> CompoundOperationWrapper { + let codingFactoryOperation = runtimeProvider.fetchCoderFactoryOperation() + + let mappingOperation = ClosureOperation { + let codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + let extrinsicEventsList = try queryOperation.extractNoCancellableResultData().extrinsicsWithEvents + + guard let extrinsicEvents = extrinsicEventsList + .first(where: { $0.extrinsicHash == extrinsicHash }) else { + throw ExtrinsicStatusServiceError.extrinsicNotFound(extrinsicHash) + } + + let successMatcher = ExtrinsicSuccessEventMatcher() + + if extrinsicEvents.eventRecords.contains( + where: { successMatcher.match(event: $0.event, using: codingFactory) } + ) { + let extString = extrinsicHash.toHex(includePrefix: true) + + let events: [Event] = if let interstedEventsMatcher { + extrinsicEvents.eventRecords.filter { + interstedEventsMatcher.match( + event: $0.event, + using: codingFactory + ) + }.map(\.event) + } else { + [] + } + + return .success( + .init( + extrinsicHash: extString, + blockHash: blockHash, + interestedEvents: events + ) + ) + } + + let failMatcher = ExtrinsicFailureEventMatcher() + + if let failureEvent = extrinsicEvents.eventRecords.first( + where: { failMatcher.match(event: $0.event, using: codingFactory) } + )?.event { + let dispatchError = try failureEvent.params.map( + to: ExtrinsicFailedEventParams.self, + with: codingFactory.createRuntimeJsonContext().toRawContext() + ).dispatchError + + let extString = extrinsicHash.toHex(includePrefix: true) + return .failure(.init(extrinsicHash: extString, blockHash: blockHash, error: dispatchError)) + } + + throw ExtrinsicStatusServiceError.terminateEventNotFound(extrinsicEvents) + } + + mappingOperation.addDependency(codingFactoryOperation) + + return CompoundOperationWrapper( + targetOperation: mappingOperation, + dependencies: [codingFactoryOperation] + ) + } +} + +extension ExtrinsicStatusService: ExtrinsicStatusServiceProtocol { + func fetchExtrinsicStatusForHash( + _ extrinsicHash: String, + inBlock blockHash: String, + matchingEvents: ExtrinsicEventsMatching? + ) -> CompoundOperationWrapper { + do { + let extHashData = try Data(hexString: extrinsicHash) + let blockHashData = try Data(hexString: blockHash) + + let eventsQueryWrapper = eventsQueryFactory.queryBlockDetailsWrapper( + from: connection, + runtimeProvider: runtimeProvider, + blockHash: blockHashData + ) + + let statusWrapper = createMatchingWrapper( + dependingOn: eventsQueryWrapper.targetOperation, + runtimeProvider: runtimeProvider, + extrinsicHash: extHashData, + blockHash: blockHash, + interstedEventsMatcher: matchingEvents + ) + + statusWrapper.addDependency(wrapper: eventsQueryWrapper) + + return statusWrapper.insertingHead(operations: eventsQueryWrapper.allOperations) + + } catch { + return CompoundOperationWrapper.createWithError(error) + } + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicStatusService/ExtrinsicSubmissionMonitor.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicStatusService/ExtrinsicSubmissionMonitor.swift new file mode 100644 index 0000000000..045f7a4326 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicStatusService/ExtrinsicSubmissionMonitor.swift @@ -0,0 +1,106 @@ +import Foundation +import Operation_iOS + +protocol ExtrinsicSubmitMonitorFactoryProtocol { + func submitAndMonitorWrapper( + extrinsicBuilderClosure: @escaping ExtrinsicBuilderClosure, + payingIn feeAssetId: ChainAssetId?, + signer: SigningWrapperProtocol, + matchingEvents: ExtrinsicEventsMatching? + ) -> CompoundOperationWrapper +} + +final class ExtrinsicSubmissionMonitorFactory { + struct SubmissionResult { + let blockHash: String + let extrinsicHash: String + } + + let submissionService: ExtrinsicServiceProtocol + let statusService: ExtrinsicStatusServiceProtocol + let operationQueue: OperationQueue + let processingQueue = DispatchQueue(label: "io.web3citizenship.extrinsic.monitor.\(UUID().uuidString)") + + init( + submissionService: ExtrinsicServiceProtocol, + statusService: ExtrinsicStatusServiceProtocol, + operationQueue: OperationQueue + ) { + self.submissionService = submissionService + self.statusService = statusService + self.operationQueue = operationQueue + } +} + +extension ExtrinsicSubmissionMonitorFactory: ExtrinsicSubmitMonitorFactoryProtocol { + func submitAndMonitorWrapper( + extrinsicBuilderClosure: @escaping ExtrinsicBuilderClosure, + payingIn feeAssetId: ChainAssetId?, + signer: SigningWrapperProtocol, + matchingEvents: ExtrinsicEventsMatching? + ) -> CompoundOperationWrapper { + var subscriptionId: UInt16? + + let submissionOperation = AsyncClosureOperation(operationClosure: { completionClosure in + self.submissionService.submitAndWatch( + extrinsicBuilderClosure, + payingIn: feeAssetId, + signer: signer, + runningIn: self.processingQueue, + subscriptionIdClosure: { identifier in + subscriptionId = identifier + + return true + }, + notificationClosure: { result in + switch result { + case let .success(model): + if let blockHash = model.getInBlockOrFinalizedHash() { + if let subscriptionId { + self.submissionService.cancelExtrinsicWatch(for: subscriptionId) + } + + let response = SubmissionResult( + blockHash: blockHash, + extrinsicHash: model.extrinsicHash + ) + + completionClosure(.success(response)) + } + case let .failure(error): + if let subscriptionId { + self.submissionService.cancelExtrinsicWatch(for: subscriptionId) + } + + completionClosure(.failure(error)) + } + } + ) + }, cancelationClosure: { + self.processingQueue.async { + guard let subscriptionId else { + return + } + + self.submissionService.cancelExtrinsicWatch(for: subscriptionId) + } + }) + + let statusWrapper: CompoundOperationWrapper = OperationCombiningService + .compoundNonOptionalWrapper( + operationManager: OperationManager(operationQueue: operationQueue) + ) { + let response = try submissionOperation.extractNoCancellableResultData() + + return self.statusService.fetchExtrinsicStatusForHash( + response.extrinsicHash, + inBlock: response.blockHash, + matchingEvents: matchingEvents + ) + } + + statusWrapper.addDependency(operations: [submissionOperation]) + + return statusWrapper.insertingHead(operations: [submissionOperation]) + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicStatusService/SubstrateEventsRepository.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicStatusService/SubstrateEventsRepository.swift new file mode 100644 index 0000000000..d151cb709a --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicStatusService/SubstrateEventsRepository.swift @@ -0,0 +1,49 @@ +import Foundation +import SubstrateSdk + +protocol SubstrateEventsRepositoryProtocol { + func getInherentEvents(from eventRecords: [EventRecord]) -> SubstrateInherentsEvents + + func getExtrinsicsEvents( + from block: Block, + eventRecords: [EventRecord] + ) -> [SubstrateExtrinsicEvents] +} + +final class SubstrateEventsRepository: SubstrateEventsRepositoryProtocol { + func getInherentEvents(from eventRecords: [EventRecord]) -> SubstrateInherentsEvents { + .init( + initialization: eventRecords.filter { $0.phase.isInitialization }.map(\.event), + finalization: eventRecords.filter { $0.phase.isFinalization }.map(\.event) + ) + } + + func getExtrinsicsEvents(from block: Block, eventRecords: [EventRecord]) -> [SubstrateExtrinsicEvents] { + let eventsByExtrinsicIndex = eventRecords.reduce( + into: [ExtrinsicIndex: [EventRecord]]() + ) { accum, record in + guard let extrinsicIndex = record.extrinsicIndex else { + return + } + + let currentEvents = accum[extrinsicIndex] ?? [] + accum[extrinsicIndex] = currentEvents + [record] + } + + return block.extrinsics.enumerated().compactMap { index, hexExtrinsic in + do { + let data = try Data(hexString: hexExtrinsic) + let extrinsicHash = try data.blake2b32() + + return SubstrateExtrinsicEvents( + extrinsicHash: extrinsicHash, + extrinsicData: data, + eventRecords: eventsByExtrinsicIndex[ExtrinsicIndex(index)] ?? [] + ) + + } catch { + return nil + } + } + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicStatusService/SubstrateExtrinsicEvents.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicStatusService/SubstrateExtrinsicEvents.swift new file mode 100644 index 0000000000..2bf7c00467 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicStatusService/SubstrateExtrinsicEvents.swift @@ -0,0 +1,20 @@ +import Foundation +import SubstrateSdk + +struct SubstrateBlockDetails { + let extrinsicsWithEvents: SubstrateExtrinsicsEvents + let inherentsEvents: SubstrateInherentsEvents +} + +struct SubstrateExtrinsicEvents { + let extrinsicHash: Data + let extrinsicData: Data + let eventRecords: [EventRecord] +} + +typealias SubstrateExtrinsicsEvents = [SubstrateExtrinsicEvents] + +struct SubstrateInherentsEvents { + let initialization: [Event] + let finalization: [Event] +} diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicStatusService/SubstrateExtrinsicStatus.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicStatusService/SubstrateExtrinsicStatus.swift new file mode 100644 index 0000000000..846d6f3d55 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicStatusService/SubstrateExtrinsicStatus.swift @@ -0,0 +1,18 @@ +import Foundation + +enum SubstrateExtrinsicStatus { + struct SuccessExtrinsic { + let extrinsicHash: ExtrinsicHash + let blockHash: BlockHash + let interestedEvents: [Event] + } + + struct FailedExtrinsic { + let extrinsicHash: ExtrinsicHash + let blockHash: BlockHash + let error: DispatchExtrinsicError + } + + case success(SuccessExtrinsic) + case failure(FailedExtrinsic) +} diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicCustomFeeEstimatingFactoryProtocol.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicCustomFeeEstimatingFactoryProtocol.swift new file mode 100644 index 0000000000..2ffafe2ff5 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicCustomFeeEstimatingFactoryProtocol.swift @@ -0,0 +1,6 @@ +import Foundation +import Operation_iOS + +protocol ExtrinsicCustomFeeEstimatingFactoryProtocol { + func createCustomFeeEstimator(for chainAsset: ChainAsset) -> ExtrinsicFeeEstimating? +} diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicCustomFeeInstallingWrapperFactory.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicCustomFeeInstallingWrapperFactory.swift new file mode 100644 index 0000000000..d3d0215130 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicCustomFeeInstallingWrapperFactory.swift @@ -0,0 +1,9 @@ +import Foundation +import Operation_iOS + +protocol ExtrinsicCustomFeeInstallingFactoryProtocol { + func createCustomFeeInstallerWrapper( + chainAsset: ChainAsset, + accountClosure: @escaping () throws -> ChainAccountResponse + ) -> CompoundOperationWrapper +} diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicFeeEstimatingWrapperFactory.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicFeeEstimatingWrapperFactory.swift index 9020ec61cd..66dfbcd45d 100644 --- a/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicFeeEstimatingWrapperFactory.swift +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicFeeEstimatingWrapperFactory.swift @@ -10,44 +10,29 @@ protocol ExtrinsicFeeEstimatingWrapperFactoryProtocol { asset: AssetModel, extrinsicCreatingResultClosure: @escaping () throws -> ExtrinsicsCreationResult ) -> CompoundOperationWrapper - - func createHydraFeeEstimatingWrapper( - asset: AssetModel, - flowStateStore: HydraFlowStateStore, - extrinsicCreatingResultClosure: @escaping () throws -> ExtrinsicsCreationResult - ) -> CompoundOperationWrapper } final class ExtrinsicFeeEstimatingWrapperFactory: ExtrinsicFeeEstimatingWrapperFactoryProtocol { - let account: ChainAccountResponse - let chain: ChainModel - let runtimeService: RuntimeProviderProtocol - let connection: JSONRPCEngine - let operationQueue: OperationQueue + let host: ExtrinsicFeeEstimatorHostProtocol + let customFeeEstimatorFactory: ExtrinsicCustomFeeEstimatingFactoryProtocol init( - account: ChainAccountResponse, - chain: ChainModel, - runtimeService: RuntimeProviderProtocol, - connection: JSONRPCEngine, - operationQueue: OperationQueue + host: ExtrinsicFeeEstimatorHostProtocol, + customFeeEstimatorFactory: ExtrinsicCustomFeeEstimatingFactoryProtocol ) { - self.account = account - self.chain = chain - self.runtimeService = runtimeService - self.connection = connection - self.operationQueue = operationQueue + self.host = host + self.customFeeEstimatorFactory = customFeeEstimatorFactory } func createNativeFeeEstimatingWrapper( extrinsicCreatingResultClosure: @escaping () throws -> ExtrinsicsCreationResult ) -> CompoundOperationWrapper { ExtrinsicNativeFeeEstimator( - chain: chain, - operationQueue: operationQueue + chain: host.chain, + operationQueue: host.operationQueue ).createFeeEstimatingWrapper( - connection: connection, - runtimeService: runtimeService, + connection: host.connection, + runtimeService: host.runtimeProvider, extrinsicCreatingResultClosure: extrinsicCreatingResultClosure ) } @@ -56,41 +41,21 @@ final class ExtrinsicFeeEstimatingWrapperFactory: ExtrinsicFeeEstimatingWrapperF asset: AssetModel, extrinsicCreatingResultClosure: @escaping () throws -> ExtrinsicsCreationResult ) -> CompoundOperationWrapper { - ExtrinsicAssetsCustomFeeEstimator( - chainAsset: .init(chain: chain, asset: asset), - operationQueue: operationQueue - ).createFeeEstimatingWrapper( - connection: connection, - runtimeService: runtimeService, - extrinsicCreatingResultClosure: extrinsicCreatingResultClosure - ) - } - - func createHydraFeeEstimatingWrapper( - asset: AssetModel, - flowStateStore: HydraFlowStateStore, - extrinsicCreatingResultClosure: @escaping () throws -> ExtrinsicsCreationResult - ) -> CompoundOperationWrapper { - let chainAsset = ChainAsset(chain: chain, asset: asset) - - do { - let flowState = try flowStateStore.setupFlowState( - account: account, - chain: chain, - queue: operationQueue + let chainAsset = ChainAsset(chain: host.chain, asset: asset) + + guard + let customFeeEstimator = customFeeEstimatorFactory.createCustomFeeEstimator( + for: chainAsset + ) else { + return .createWithError( + ExtrinsicFeeEstimationRegistryError.unexpectedChainAssetId(chainAsset.chainAssetId) ) - - return HydraExtrinsicAssetsCustomFeeEstimator( - chainAsset: chainAsset, - flowState: flowState, - operationQueue: operationQueue - ).createFeeEstimatingWrapper( - connection: connection, - runtimeService: runtimeService, - extrinsicCreatingResultClosure: extrinsicCreatingResultClosure - ) - } catch { - return CompoundOperationWrapper.createWithError(error) } + + return customFeeEstimator.createFeeEstimatingWrapper( + connection: host.connection, + runtimeService: host.runtimeProvider, + extrinsicCreatingResultClosure: extrinsicCreatingResultClosure + ) } } diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicFeeEstimationRegistry.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicFeeEstimationRegistry.swift index 1cc4ac3901..c5c363098b 100644 --- a/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicFeeEstimationRegistry.swift +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicFeeEstimationRegistry.swift @@ -9,51 +9,16 @@ enum ExtrinsicFeeEstimationRegistryError: Error { final class ExtrinsicFeeEstimationRegistry { let chain: ChainModel let estimatingWrapperFactory: ExtrinsicFeeEstimatingWrapperFactoryProtocol - let connection: JSONRPCEngine - let runtimeProvider: RuntimeProviderProtocol - let userStorageFacade: StorageFacadeProtocol - let substrateStorageFacade: StorageFacadeProtocol - let operationQueue: OperationQueue - - /// We need to keep HydraFlowStates alive until the flow is complete, - /// so we collect strong references received during HydraFlowStore updates. - var existingFlowsStates: [HydraFlowState] = [] - - private lazy var flowStateStore: HydraFlowStateStore = { - let store = HydraFlowStateStore.getShared( - for: connection, - runtimeProvider: runtimeProvider, - userStorageFacade: userStorageFacade, - substrateStorageFacade: substrateStorageFacade - ) - - store.subscribeForChangesUpdates(self) - - return store - }() + let feeInstallingWrapperFactory: ExtrinsicFeeInstallingWrapperFactoryProtocol init( chain: ChainModel, estimatingWrapperFactory: ExtrinsicFeeEstimatingWrapperFactoryProtocol, - connection: JSONRPCEngine, - runtimeProvider: RuntimeProviderProtocol, - userStorageFacade: StorageFacadeProtocol, - substrateStorageFacade: StorageFacadeProtocol, - operationQueue: OperationQueue + feeInstallingWrapperFactory: ExtrinsicFeeInstallingWrapperFactoryProtocol ) { self.chain = chain self.estimatingWrapperFactory = estimatingWrapperFactory - self.connection = connection - self.runtimeProvider = runtimeProvider - self.userStorageFacade = userStorageFacade - self.substrateStorageFacade = substrateStorageFacade - self.operationQueue = operationQueue - } -} - -extension ExtrinsicFeeEstimationRegistry: HydraFlowStateStoreSubscriber { - func flowStateStoreDidUpdate(_ newStates: [HydraFlowState]) { - existingFlowsStates = newStates + self.feeInstallingWrapperFactory = feeInstallingWrapperFactory } } @@ -79,14 +44,12 @@ extension ExtrinsicFeeEstimationRegistry: ExtrinsicFeeEstimationRegistring { return createFeeEstimatingWrapper( for: asset, - chainAssetId: chainAssetId, extrinsicCreatingResultClosure: extrinsicCreatingResultClosure ) } func createFeeEstimatingWrapper( for asset: AssetModel, - chainAssetId: ChainAssetId, extrinsicCreatingResultClosure: @escaping () throws -> ExtrinsicsCreationResult ) -> CompoundOperationWrapper { guard !asset.isUtility else { @@ -95,26 +58,16 @@ extension ExtrinsicFeeEstimationRegistry: ExtrinsicFeeEstimationRegistring { ) } - return switch AssetType(rawType: asset.type) { + switch AssetType(rawType: asset.type) { case .none: - estimatingWrapperFactory.createNativeFeeEstimatingWrapper( - extrinsicCreatingResultClosure: extrinsicCreatingResultClosure - ) - case .orml where chain.hasHydrationTransferFees: - estimatingWrapperFactory.createHydraFeeEstimatingWrapper( - asset: asset, - flowStateStore: flowStateStore, + return estimatingWrapperFactory.createNativeFeeEstimatingWrapper( extrinsicCreatingResultClosure: extrinsicCreatingResultClosure ) - case .statemine where chain.hasAssetHubTransferFees: - estimatingWrapperFactory.createCustomFeeEstimatingWrapper( + case .equilibrium, .evmNative, .evmAsset, .orml, .statemine: + return estimatingWrapperFactory.createCustomFeeEstimatingWrapper( asset: asset, extrinsicCreatingResultClosure: extrinsicCreatingResultClosure ) - case .equilibrium, .evmNative, .evmAsset, .orml, .statemine: - .createWithError( - ExtrinsicFeeEstimationRegistryError.unexpectedChainAssetId(chainAssetId) - ) } } @@ -123,103 +76,39 @@ extension ExtrinsicFeeEstimationRegistry: ExtrinsicFeeEstimationRegistring { accountClosure: @escaping () throws -> ChainAccountResponse ) -> CompoundOperationWrapper { guard let chainAssetId else { - return CompoundOperationWrapper.createWithResult(ExtrinsicNativeFeeInstaller()) + return feeInstallingWrapperFactory.createNativeFeeInstallerWrapper(accountClosure: accountClosure) } - do { - guard - chainAssetId.chainId == chain.chainId, - let asset = chain.asset(for: chainAssetId.assetId) - else { - throw ExtrinsicFeeEstimationRegistryError.unexpectedChainAssetId(chainAssetId) - } - - return createFeeInstallerWrapper( - accountClosure: accountClosure, - chainAsset: ChainAsset(chain: chain, asset: asset) + guard + chainAssetId.chainId == chain.chainId, + let asset = chain.chainAsset(for: chainAssetId.assetId) + else { + return .createWithError( + ExtrinsicFeeEstimationRegistryError.unexpectedChainAssetId(chainAssetId) ) - } catch { - return CompoundOperationWrapper.createWithError(error) } + + return createFeeInstallerWrapper(chainAsset: asset, accountClosure: accountClosure) } func createFeeInstallerWrapper( - accountClosure: @escaping () throws -> ChainAccountResponse, - chainAsset: ChainAsset + chainAsset: ChainAsset, + accountClosure: @escaping () throws -> ChainAccountResponse ) -> CompoundOperationWrapper { guard !chainAsset.isUtilityAsset else { - return CompoundOperationWrapper.createWithResult(ExtrinsicNativeFeeInstaller()) + return feeInstallingWrapperFactory.createNativeFeeInstallerWrapper(accountClosure: accountClosure) } - return switch AssetType(rawType: chainAsset.asset.type) { + switch AssetType(rawType: chainAsset.asset.type) { case .none: - CompoundOperationWrapper.createWithResult(ExtrinsicNativeFeeInstaller()) - case .statemine where chain.hasAssetHubTransferFees: - CompoundOperationWrapper.createWithResult( - ExtrinsicAssetConversionFeeInstaller( - feeAsset: chainAsset - ) + return feeInstallingWrapperFactory.createNativeFeeInstallerWrapper( + accountClosure: accountClosure ) - case .orml where chain.hasHydrationTransferFees: - createHydraFeeInstallingWrapper( - accountClosure: accountClosure, - chainAsset: chainAsset - ) - case .orml, .statemine, .equilibrium, .evmNative, .evmAsset: - .createWithError( - ExtrinsicFeeEstimationRegistryError.unexpectedChainAssetId(chainAsset.chainAssetId) - ) - } - } - - private func createHydraFeeInstallingWrapper( - accountClosure: @escaping () throws -> ChainAccountResponse, - chainAsset: ChainAsset - ) -> CompoundOperationWrapper { - let swapStateWrapper = OperationCombiningService.compoundNonOptionalWrapper( - operationManager: OperationManager(operationQueue: operationQueue) - ) { [weak self] in - guard let self else { - throw BaseOperationError.parentOperationCancelled - } - - let account = try accountClosure() - - let swapStateFetchOperation = try flowStateStore.setupFlowState( - account: account, - chain: chainAsset.chain, - queue: operationQueue - ) - .setupSwapService() - .createFetchOperation() - - return CompoundOperationWrapper(targetOperation: swapStateFetchOperation) - } - - return createHydraFeeInstallingWrapper( - using: swapStateWrapper, - chainAsset: chainAsset - ) - } - - private func createHydraFeeInstallingWrapper( - using swapStateWrapper: CompoundOperationWrapper, - chainAsset: ChainAsset - ) -> CompoundOperationWrapper { - let operation = ClosureOperation { - let swapState = try swapStateWrapper.targetOperation.extractNoCancellableResultData() - - return HydraExtrinsicFeeInstaller( - feeAsset: chainAsset, - swapState: swapState + case .equilibrium, .evmNative, .evmAsset, .orml, .statemine: + return feeInstallingWrapperFactory.createCustomFeeInstallerWrapper( + chainAsset: chainAsset, + accountClosure: accountClosure ) } - - operation.addDependency(swapStateWrapper.targetOperation) - - return CompoundOperationWrapper( - targetOperation: operation, - dependencies: swapStateWrapper.allOperations - ) } } diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicFeeInstallingWrapperFactory.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicFeeInstallingWrapperFactory.swift new file mode 100644 index 0000000000..d099e9570c --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicFeeInstallingWrapperFactory.swift @@ -0,0 +1,39 @@ +import Foundation +import Operation_iOS + +protocol ExtrinsicFeeInstallingWrapperFactoryProtocol { + func createNativeFeeInstallerWrapper( + accountClosure: @escaping () throws -> ChainAccountResponse + ) -> CompoundOperationWrapper + + func createCustomFeeInstallerWrapper( + chainAsset: ChainAsset, + accountClosure: @escaping () throws -> ChainAccountResponse + ) -> CompoundOperationWrapper +} + +final class ExtrinsicFeeInstallingWrapperFactory { + let customFeeInstallerFactory: ExtrinsicCustomFeeInstallingFactoryProtocol + + init(customFeeInstallerFactory: ExtrinsicCustomFeeInstallingFactoryProtocol) { + self.customFeeInstallerFactory = customFeeInstallerFactory + } +} + +extension ExtrinsicFeeInstallingWrapperFactory: ExtrinsicFeeInstallingWrapperFactoryProtocol { + func createNativeFeeInstallerWrapper( + accountClosure _: @escaping () throws -> ChainAccountResponse + ) -> CompoundOperationWrapper { + CompoundOperationWrapper.createWithResult(ExtrinsicNativeFeeInstaller()) + } + + func createCustomFeeInstallerWrapper( + chainAsset: ChainAsset, + accountClosure: @escaping () throws -> ChainAccountResponse + ) -> CompoundOperationWrapper { + customFeeInstallerFactory.createCustomFeeInstallerWrapper( + chainAsset: chainAsset, + accountClosure: accountClosure + ) + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/FeeViaSwap/AssetConversionFeeEstimatingFactory.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/FeeViaSwap/AssetConversionFeeEstimatingFactory.swift new file mode 100644 index 0000000000..ad48f8e873 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/FeeViaSwap/AssetConversionFeeEstimatingFactory.swift @@ -0,0 +1,55 @@ +import Foundation +import Operation_iOS +import SubstrateSdk + +final class AssetConversionFeeEstimatingFactory { + let host: ExtrinsicFeeEstimatorHostProtocol + + private var hydraFlowState: HydraFlowState? + + init(host: ExtrinsicFeeEstimatorHostProtocol) { + self.host = host + } + + private func setupHydraFlowState() -> HydraFlowState { + if let hydraFlowState { + return hydraFlowState + } + + let hydraFlowState = AssetConversionFeeSharedStateStore.getOrCreateHydra(for: host) + self.hydraFlowState = hydraFlowState + + return hydraFlowState + } +} + +extension AssetConversionFeeEstimatingFactory: ExtrinsicCustomFeeEstimatingFactoryProtocol { + func createCustomFeeEstimator(for chainAsset: ChainAsset) -> ExtrinsicFeeEstimating? { + switch AssetType(rawType: chainAsset.asset.type) { + case .orml where chainAsset.chain.hasHydrationFees: + let hydraState = setupHydraFlowState() + let hydraQuoteFactory = HydraQuoteFactory(flowState: hydraState) + + return ExtrinsicAssetConversionFeeEstimator( + chainAsset: chainAsset, + operationQueue: host.operationQueue, + quoteFactory: hydraQuoteFactory + ) + case .statemine where chainAsset.chain.hasAssetHubFees: + let assetHubQuoteFactory = AssetHubSwapOperationFactory( + chain: host.chain, + runtimeService: host.runtimeProvider, + connection: host.connection, + operationQueue: host.operationQueue + ) + + return ExtrinsicAssetConversionFeeEstimator( + chainAsset: chainAsset, + operationQueue: host.operationQueue, + quoteFactory: assetHubQuoteFactory + ) + case .none, .equilibrium, .evmNative, .evmAsset, .orml, .statemine: + return nil + } + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/FeeViaSwap/AssetConversionFeeInstallingFactory.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/FeeViaSwap/AssetConversionFeeInstallingFactory.swift new file mode 100644 index 0000000000..ef33b07833 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/FeeViaSwap/AssetConversionFeeInstallingFactory.swift @@ -0,0 +1,75 @@ +import Foundation +import Operation_iOS + +final class AssetConversionFeeInstallingFactory { + let host: ExtrinsicFeeEstimatorHostProtocol + + private var hydraFeeCurrencyService: HydraSwapFeeCurrencyService? + + init(host: ExtrinsicFeeEstimatorHostProtocol) { + self.host = host + } + + private func setupHydraFeeCurrencyService(for account: ChainAccountResponse) -> HydraSwapFeeCurrencyService { + let hydraFeeCurrencyService = AssetConversionFeeSharedStateStore.getOrCreateHydraFeeCurrencyService( + for: host, + payerAccountId: account.accountId + ) + + self.hydraFeeCurrencyService = hydraFeeCurrencyService + + return hydraFeeCurrencyService + } + + private func createHydraFeeInstallingWrapper( + chainAsset: ChainAsset, + accountClosure: @escaping () throws -> ChainAccountResponse + ) -> CompoundOperationWrapper { + let swapStateWrapper = OperationCombiningService.compoundNonOptionalWrapper( + operationQueue: host.operationQueue + ) { + let account = try accountClosure() + + let feeCurrencyService = self.setupHydraFeeCurrencyService(for: account) + + let swapStateFetchOperation = feeCurrencyService.createFetchOperation() + + return CompoundOperationWrapper(targetOperation: swapStateFetchOperation) + } + + let mappingOperation = ClosureOperation { + let swapState = try swapStateWrapper.targetOperation.extractNoCancellableResultData() + + return HydraExtrinsicFeeInstaller(feeAsset: chainAsset, swapState: swapState) + } + + mappingOperation.addDependency(swapStateWrapper.targetOperation) + + return swapStateWrapper.insertingTail(operation: mappingOperation) + } +} + +extension AssetConversionFeeInstallingFactory: ExtrinsicCustomFeeInstallingFactoryProtocol { + func createCustomFeeInstallerWrapper( + chainAsset: ChainAsset, + accountClosure: @escaping () throws -> ChainAccountResponse + ) -> CompoundOperationWrapper { + switch AssetType(rawType: chainAsset.asset.type) { + case .statemine where chainAsset.chain.hasAssetHubFees: + CompoundOperationWrapper.createWithResult( + ExtrinsicAssetConversionFeeInstaller( + feeAsset: chainAsset + ) + ) + case .orml where chainAsset.chain.hasHydrationFees: + createHydraFeeInstallingWrapper( + chainAsset: chainAsset, + accountClosure: accountClosure + ) + case .none, .orml, .statemine, .equilibrium, .evmNative, .evmAsset: + .createWithError( + ExtrinsicFeeEstimationRegistryError.unexpectedChainAssetId(chainAsset.chainAssetId) + ) + } + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/FeeViaSwap/AssetConversionFeeSharedStateStore.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/FeeViaSwap/AssetConversionFeeSharedStateStore.swift new file mode 100644 index 0000000000..db54e36798 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/FeeViaSwap/AssetConversionFeeSharedStateStore.swift @@ -0,0 +1,71 @@ +import Foundation +import SubstrateSdk + +private struct StateKey: Hashable { + let chainId: ChainModel.Id + let accountId: AccountId +} + +enum AssetConversionFeeSharedStateStore { + private static var states: [StateKey: WeakWrapper] = [:] + private static var feeServices: [StateKey: WeakWrapper] = [:] + private static let mutex = NSLock() + + static func getOrCreateHydra(for host: ExtrinsicFeeEstimatorHostProtocol) -> HydraFlowState { + mutex.lock() + + defer { + mutex.unlock() + } + + let state = StateKey(chainId: host.chain.chainId, accountId: host.account.accountId) + + if let flowState = states[state]?.target as? HydraFlowState { + return flowState + } + + let flowState = HydraFlowState( + account: host.account, + chain: host.chain, + connection: host.connection, + runtimeProvider: host.runtimeProvider, + userStorageFacade: host.userStorageFacade, + substrateStorageFacade: host.substrateStorageFacade, + operationQueue: host.operationQueue + ) + + states[state] = WeakWrapper(target: flowState) + + return flowState + } + + static func getOrCreateHydraFeeCurrencyService( + for host: ExtrinsicFeeEstimatorHostProtocol, + payerAccountId: AccountId + ) -> HydraSwapFeeCurrencyService { + mutex.lock() + + defer { + mutex.unlock() + } + + let state = StateKey(chainId: host.chain.chainId, accountId: payerAccountId) + + if let service = feeServices[state]?.target as? HydraSwapFeeCurrencyService { + return service + } + + let service = HydraSwapFeeCurrencyService( + payerAccountId: payerAccountId, + connection: host.connection, + runtimeProvider: host.runtimeProvider, + operationQueue: host.operationQueue + ) + + feeServices[state] = WeakWrapper(target: service) + + service.setup() + + return service + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicAssetConversionFeeInstaller.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/FeeViaSwap/AssetHub/ExtrinsicAssetConversionFeeInstaller.swift similarity index 100% rename from novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicAssetConversionFeeInstaller.swift rename to novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/FeeViaSwap/AssetHub/ExtrinsicAssetConversionFeeInstaller.swift diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicAssetConversionFeeEstimator.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/FeeViaSwap/ExtrinsicAssetConversionFeeEstimator.swift similarity index 75% rename from novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicAssetConversionFeeEstimator.swift rename to novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/FeeViaSwap/ExtrinsicAssetConversionFeeEstimator.swift index f95e1c453a..ec8d133ca5 100644 --- a/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicAssetConversionFeeEstimator.swift +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/FeeViaSwap/ExtrinsicAssetConversionFeeEstimator.swift @@ -2,20 +2,26 @@ import Foundation import Operation_iOS import SubstrateSdk -final class ExtrinsicAssetsCustomFeeEstimator { +final class ExtrinsicAssetConversionFeeEstimator { let chainAsset: ChainAsset let operationQueue: OperationQueue + let quoteFactory: AssetQuoteFactoryProtocol + let feeBufferInPercentage: BigRational init( chainAsset: ChainAsset, - operationQueue: OperationQueue + operationQueue: OperationQueue, + quoteFactory: AssetQuoteFactoryProtocol, + feeBufferInPercentage: BigRational = BigRational.percent(of: 0) // no overestimation by default ) { self.chainAsset = chainAsset self.operationQueue = operationQueue + self.quoteFactory = quoteFactory + self.feeBufferInPercentage = feeBufferInPercentage } } -extension ExtrinsicAssetsCustomFeeEstimator: ExtrinsicFeeEstimating { +extension ExtrinsicAssetConversionFeeEstimator: ExtrinsicFeeEstimating { func createFeeEstimatingWrapper( connection: JSONRPCEngine, runtimeService: RuntimeCodingServiceProtocol, @@ -37,13 +43,6 @@ extension ExtrinsicAssetsCustomFeeEstimator: ExtrinsicFeeEstimating { extrinsicCreatingResultClosure: extrinsicCreatingResultClosure ) - let quoteFactory = AssetHubSwapOperationFactory( - chain: chainAsset.chain, - runtimeService: runtimeService, - connection: connection, - operationQueue: operationQueue - ) - let conversionOperation: BaseOperation<[AssetConversion.Quote]> = OperationCombiningService( operationManager: OperationManager(operationQueue: operationQueue) ) { @@ -57,7 +56,7 @@ extension ExtrinsicAssetsCustomFeeEstimator: ExtrinsicFeeEstimating { direction: .buy ) - return quoteFactory.quote(for: quoteArgs) + return self.quoteFactory.quote(for: quoteArgs) } }.longrunOperation() @@ -68,7 +67,9 @@ extension ExtrinsicAssetsCustomFeeEstimator: ExtrinsicFeeEstimating { let quotes = try conversionOperation.extractNoCancellableResultData() let items = zip(nativeFees, quotes).map { pair in - ExtrinsicFee(amount: pair.1.amountIn, payer: pair.0.payer, weight: pair.0.weight) + let amountIn = pair.1.amountIn + let amountInWithBuffer = amountIn + self.feeBufferInPercentage.mul(value: amountIn) + return ExtrinsicFee(amount: amountInWithBuffer, payer: pair.0.payer, weight: pair.0.weight) } return ExtrinsicFeeEstimationResult(items: items) diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/FeeViaSwap/ExtrinsicFeeEstimatorHost.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/FeeViaSwap/ExtrinsicFeeEstimatorHost.swift new file mode 100644 index 0000000000..b5053546e6 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/FeeViaSwap/ExtrinsicFeeEstimatorHost.swift @@ -0,0 +1,41 @@ +import Foundation +import SubstrateSdk +import Operation_iOS + +protocol ExtrinsicFeeEstimatorHostProtocol { + var account: ChainAccountResponse { get } + var chain: ChainModel { get } + var connection: JSONRPCEngine { get } + var runtimeProvider: RuntimeProviderProtocol { get } + var userStorageFacade: StorageFacadeProtocol { get } + var substrateStorageFacade: StorageFacadeProtocol { get } + var operationQueue: OperationQueue { get } +} + +final class ExtrinsicFeeEstimatorHost: ExtrinsicFeeEstimatorHostProtocol { + let account: ChainAccountResponse + let chain: ChainModel + let connection: JSONRPCEngine + let runtimeProvider: RuntimeProviderProtocol + let userStorageFacade: StorageFacadeProtocol + let substrateStorageFacade: StorageFacadeProtocol + let operationQueue: OperationQueue + + init( + account: ChainAccountResponse, + chain: ChainModel, + connection: JSONRPCEngine, + runtimeProvider: RuntimeProviderProtocol, + userStorageFacade: StorageFacadeProtocol, + substrateStorageFacade: StorageFacadeProtocol, + operationQueue: OperationQueue + ) { + self.account = account + self.chain = chain + self.connection = connection + self.runtimeProvider = runtimeProvider + self.userStorageFacade = userStorageFacade + self.substrateStorageFacade = substrateStorageFacade + self.operationQueue = operationQueue + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/Hydra/HydraExtrinsicFeeInstaller.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/FeeViaSwap/Hydra/HydraExtrinsicFeeInstaller.swift similarity index 79% rename from novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/Hydra/HydraExtrinsicFeeInstaller.swift rename to novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/FeeViaSwap/Hydra/HydraExtrinsicFeeInstaller.swift index b86b71e9f3..8e61784fec 100644 --- a/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/Hydra/HydraExtrinsicFeeInstaller.swift +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/FeeViaSwap/Hydra/HydraExtrinsicFeeInstaller.swift @@ -3,11 +3,11 @@ import SubstrateSdk final class HydraExtrinsicFeeInstaller { let feeAsset: ChainAsset - let swapState: HydraDx.SwapRemoteState + let swapState: HydraDx.SwapFeeCurrencyState init( feeAsset: ChainAsset, - swapState: HydraDx.SwapRemoteState + swapState: HydraDx.SwapFeeCurrencyState ) { self.feeAsset = feeAsset self.swapState = swapState @@ -15,7 +15,7 @@ final class HydraExtrinsicFeeInstaller { } extension HydraExtrinsicFeeInstaller { - struct TransferFeeInstallingCalls { + struct FeeInstallingCalls { let setCurrencyCall: HydraDx.SetCurrencyCall? let revertCurrencyCall: HydraDx.SetCurrencyCall? } @@ -31,7 +31,7 @@ extension HydraExtrinsicFeeInstaller: ExtrinsicFeeInstalling { codingFactory: coderFactory ) - let calls = createTransferFeeCalls(using: assetId) + let calls = createFeeCalls(using: assetId) guard let setCurrencyCall = calls.setCurrencyCall, @@ -45,7 +45,7 @@ extension HydraExtrinsicFeeInstaller: ExtrinsicFeeInstalling { .adding(call: revertCurrencyCall.runtimeCall()) } - private func createTransferFeeCalls(using assetId: HydraDx.LocalRemoteAssetId) -> TransferFeeInstallingCalls { + private func createFeeCalls(using assetId: HydraDx.LocalRemoteAssetId) -> FeeInstallingCalls { let setCurrencyCall: HydraDx.SetCurrencyCall? = { let currentFeeAssetId = swapState.feeCurrency ?? HydraDx.nativeAssetId @@ -64,9 +64,6 @@ extension HydraExtrinsicFeeInstaller: ExtrinsicFeeInstalling { return .init(currency: HydraDx.nativeAssetId) }() - return TransferFeeInstallingCalls( - setCurrencyCall: setCurrencyCall, - revertCurrencyCall: revertCurrencyCall - ) + return FeeInstallingCalls(setCurrencyCall: setCurrencyCall, revertCurrencyCall: revertCurrencyCall) } } diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/Hydra/HydraExtrinsicAssetsCustomFeeEstimator.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/Hydra/HydraExtrinsicAssetsCustomFeeEstimator.swift deleted file mode 100644 index 7c78b8a627..0000000000 --- a/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/Hydra/HydraExtrinsicAssetsCustomFeeEstimator.swift +++ /dev/null @@ -1,82 +0,0 @@ -import Foundation -import Operation_iOS -import SubstrateSdk - -final class HydraExtrinsicAssetsCustomFeeEstimator { - let chainAsset: ChainAsset - let flowState: HydraFlowState - let operationQueue: OperationQueue - - init( - chainAsset: ChainAsset, - flowState: HydraFlowState, - operationQueue: OperationQueue - ) { - self.chainAsset = chainAsset - self.flowState = flowState - self.operationQueue = operationQueue - } -} - -extension HydraExtrinsicAssetsCustomFeeEstimator: ExtrinsicFeeEstimating { - func createFeeEstimatingWrapper( - connection: JSONRPCEngine, - runtimeService: RuntimeCodingServiceProtocol, - extrinsicCreatingResultClosure: @escaping () throws -> ExtrinsicsCreationResult - ) -> CompoundOperationWrapper { - guard - let nativeAssetId = chainAsset.chain.utilityChainAssetId() else { - return CompoundOperationWrapper.createWithError(ExtrinsicFeeEstimatingError.brokenFee) - } - - let assetOutId = chainAsset.chainAssetId - - let nativeEstimator = ExtrinsicNativeFeeEstimator( - chain: chainAsset.chain, - operationQueue: operationQueue - ).createFeeEstimatingWrapper( - connection: connection, - runtimeService: runtimeService, - extrinsicCreatingResultClosure: extrinsicCreatingResultClosure - ) - - let quoteFactory = HydraQuoteFactory(flowState: flowState) - - let conversionOperation: BaseOperation<[AssetConversion.Quote]> = OperationCombiningService( - operationManager: OperationManager(operationQueue: operationQueue) - ) { - let nativeFees = try nativeEstimator.targetOperation.extractNoCancellableResultData().items - - return nativeFees.map { nativeFee in - let quoteArgs = AssetConversion.QuoteArgs( - assetIn: assetOutId, - assetOut: nativeAssetId, - amount: nativeFee.amount, - direction: .buy - ) - - return quoteFactory.quote(for: quoteArgs) - } - }.longrunOperation() - - conversionOperation.addDependency(nativeEstimator.targetOperation) - - let mapOperation = ClosureOperation { - let nativeFees = try nativeEstimator.targetOperation.extractNoCancellableResultData().items - let quotes = try conversionOperation.extractNoCancellableResultData() - - let items = zip(nativeFees, quotes).map { pair in - ExtrinsicFee(amount: pair.1.amountIn, payer: pair.0.payer, weight: pair.0.weight) - } - - return ExtrinsicFeeEstimationResult(items: items) - } - - mapOperation.addDependency(conversionOperation) - - return CompoundOperationWrapper( - targetOperation: mapOperation, - dependencies: nativeEstimator.allOperations + [conversionOperation] - ) - } -} diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicFee.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/Model/ExtrinsicFee.swift similarity index 100% rename from novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicFee.swift rename to novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/Model/ExtrinsicFee.swift diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicFeeProxy.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/Model/ExtrinsicFeeProxy.swift similarity index 100% rename from novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicFeeProxy.swift rename to novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/Model/ExtrinsicFeeProxy.swift diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/MultiExtrinsicFeeProxy.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/Model/MultiExtrinsicFeeProxy.swift similarity index 100% rename from novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/MultiExtrinsicFeeProxy.swift rename to novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/Model/MultiExtrinsicFeeProxy.swift diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/TransactionFeeProxy.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/Model/TransactionFeeProxy.swift similarity index 100% rename from novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/TransactionFeeProxy.swift rename to novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/Model/TransactionFeeProxy.swift diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicNativeFeeEstimator.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/Native/ExtrinsicNativeFeeEstimator.swift similarity index 100% rename from novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicNativeFeeEstimator.swift rename to novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/Native/ExtrinsicNativeFeeEstimator.swift diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicNativeFeeInstaller.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/Native/ExtrinsicNativeFeeInstaller.swift similarity index 100% rename from novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/ExtrinsicNativeFeeInstaller.swift rename to novawallet/Common/Services/ExtrinsicService/Substrate/FeeManaging/Native/ExtrinsicNativeFeeInstaller.swift diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/TokenDepositEventMatching/NativeTokenDepositEventMatcher.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/TokenDepositEventMatching/NativeTokenDepositEventMatcher.swift new file mode 100644 index 0000000000..cefea2f62b --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/TokenDepositEventMatching/NativeTokenDepositEventMatcher.swift @@ -0,0 +1,32 @@ +import Foundation +import SubstrateSdk + +final class NativeTokenDepositEventMatcher: TokenDepositEventMatching { + let logger: LoggerProtocol + + init(logger: LoggerProtocol) { + self.logger = logger + } + + func matchDeposit( + event: Event, + using codingFactory: RuntimeCoderFactoryProtocol + ) -> TokenDepositEvent? { + do { + guard codingFactory.metadata.eventMatches(event, path: BalancesPallet.balancesMinted) else { + return nil + } + + let mintedEvent = try event.params.map( + to: BalancesPallet.MintedEvent.self, + with: codingFactory.createRuntimeJsonContext().toRawContext() + ) + + return TokenDepositEvent(accountId: mintedEvent.accountId, amount: mintedEvent.amount) + } catch { + logger.error("Parsing failed: \(error)") + + return nil + } + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/TokenDepositEventMatching/PalletAssetsTokenDepositEventMatcher.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/TokenDepositEventMatching/PalletAssetsTokenDepositEventMatcher.swift new file mode 100644 index 0000000000..5a5506f3bf --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/TokenDepositEventMatching/PalletAssetsTokenDepositEventMatcher.swift @@ -0,0 +1,48 @@ +import Foundation + +final class PalletAssetsTokenDepositEventMatcher { + let extras: StatemineAssetExtras + let logger: LoggerProtocol + + init(extras: StatemineAssetExtras, logger: LoggerProtocol) { + self.extras = extras + self.logger = logger + } +} + +extension PalletAssetsTokenDepositEventMatcher: TokenDepositEventMatching { + func matchDeposit( + event: Event, + using codingFactory: RuntimeCoderFactoryProtocol + ) -> TokenDepositEvent? { + do { + guard codingFactory.metadata.eventMatches( + event, + path: PalletAssets.issuedPath(for: extras.palletName) + ) else { + return nil + } + + let mintedEvent = try event.params.map( + to: PalletAssets.IssuedEvent.self, + with: codingFactory.createRuntimeJsonContext().toRawContext() + ) + + let assetIdAsString = try StatemineAssetSerializer.encode( + assetId: mintedEvent.assetId, + palletName: extras.palletName, + codingFactory: codingFactory + ) + + guard extras.assetId == assetIdAsString else { + return nil + } + + return TokenDepositEvent(accountId: mintedEvent.accountId, amount: mintedEvent.amount) + } catch { + logger.error("Parsing failed \(error)") + + return nil + } + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/TokenDepositEventMatching/TokenDepositEventMatcherFactory.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/TokenDepositEventMatching/TokenDepositEventMatcherFactory.swift new file mode 100644 index 0000000000..f4e114f2fe --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/TokenDepositEventMatching/TokenDepositEventMatcherFactory.swift @@ -0,0 +1,38 @@ +import Foundation + +protocol TokenDepositEventMatcherFactoryProtocol { + func createMatcher(for chainAsset: ChainAsset) -> TokenDepositEventMatching? +} + +final class TokenDepositEventMatcherFactory: TokenDepositEventMatcherFactoryProtocol { + let logger: LoggerProtocol + + init(logger: LoggerProtocol) { + self.logger = logger + } + + func createMatcher(for chainAsset: ChainAsset) -> TokenDepositEventMatching? { + try? CustomAssetMapper( + type: chainAsset.asset.type, + typeExtras: chainAsset.asset.typeExtras + ).mapAssetWithExtras( + nativeHandler: { + NativeTokenDepositEventMatcher(logger: logger) + }, + statemineHandler: { extras in + PalletAssetsTokenDepositEventMatcher(extras: extras, logger: logger) + }, + ormlHandler: { extras in + TokensPalletDepositEventMatcher(extras: extras, logger: logger) + }, evmHandler: { _ in + nil + }, + evmNativeHandler: { + nil + }, + equilibriumHandler: { _ in + nil + } + ) + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/TokenDepositEventMatching/TokenDepositEventMatching.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/TokenDepositEventMatching/TokenDepositEventMatching.swift new file mode 100644 index 0000000000..006838f8e4 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/TokenDepositEventMatching/TokenDepositEventMatching.swift @@ -0,0 +1,13 @@ +import Foundation + +struct TokenDepositEvent { + let accountId: AccountId + let amount: Balance +} + +protocol TokenDepositEventMatching { + func matchDeposit( + event: Event, + using codingFactory: RuntimeCoderFactoryProtocol + ) -> TokenDepositEvent? +} diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/TokenDepositEventMatching/TokensPalletDepositEventMatcher.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/TokenDepositEventMatching/TokensPalletDepositEventMatcher.swift new file mode 100644 index 0000000000..6fd2ad7de5 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/TokenDepositEventMatching/TokensPalletDepositEventMatcher.swift @@ -0,0 +1,45 @@ +import Foundation +import SubstrateSdk + +final class TokensPalletDepositEventMatcher { + let extras: OrmlTokenExtras + let logger: LoggerProtocol + + init(extras: OrmlTokenExtras, logger: LoggerProtocol) { + self.extras = extras + self.logger = logger + } +} + +extension TokensPalletDepositEventMatcher: TokenDepositEventMatching { + func matchDeposit( + event: Event, + using codingFactory: RuntimeCoderFactoryProtocol + ) -> TokenDepositEvent? { + do { + guard codingFactory.metadata.eventMatches(event, path: TokensPallet.depositedEventPath) else { + return nil + } + + let depositedEvent = try event.params.map( + to: TokensPallet.DepositedEvent.self, + with: codingFactory.createRuntimeJsonContext().toRawContext() + ) + + let encoder = codingFactory.createEncoder() + try encoder.append(json: depositedEvent.currencyId, type: extras.currencyIdType) + let eventAssetId = try encoder.encode() + let assetId = try Data(hexString: extras.currencyIdScale) + + guard eventAssetId == assetId else { + return nil + } + + return TokenDepositEvent(accountId: depositedEvent.recepient, amount: depositedEvent.amount) + } catch { + logger.error("Parsing failed \(error)") + + return nil + } + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/XcmDepositMonitoringService.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/XcmDepositMonitoringService.swift new file mode 100644 index 0000000000..96a45a9076 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/XcmDepositMonitoringService.swift @@ -0,0 +1,310 @@ +import Foundation +import Operation_iOS +import SubstrateSdk + +protocol XcmDepositMonitoringServiceProtocol { + func useMonitoringWrapper() -> CompoundOperationWrapper +} + +enum XcmDepositMonitoringServiceError: Error { + case unsupportedAsset(ChainAsset) + case timeout + case throttled +} + +final class XcmDepositMonitoringService { + let connection: JSONRPCEngine + let runtimeProvider: RuntimeProviderProtocol + let operationQueue: OperationQueue + let workingQueue: DispatchQueue + let logger: LoggerProtocol + let blockEventsQueryFactory: BlockEventsQueryFactoryProtocol + let tokenDepositEventMatchingFactory: TokenDepositEventMatcherFactoryProtocol + let accountId: AccountId + let chainAsset: ChainAsset + let timeout: TimeInterval + + private var subscription: WalletRemoteSubscriptionProtocol? + + private var state: TokenDepositEvent? + private var notificationClosure: ((Result) -> Void)? + private var scheduler: SchedulerProtocol? + private var detectionCallsStore: [Data: CancellableCallStore] = [:] + private let mutex = NSLock() + + init( + accountId: AccountId, + chainAsset: ChainAsset, + timeout: TimeInterval = 90, + connection: JSONRPCEngine, + runtimeProvider: RuntimeProviderProtocol, + operationQueue: OperationQueue, + workingQueue: DispatchQueue, + logger: LoggerProtocol + ) { + self.accountId = accountId + self.chainAsset = chainAsset + self.timeout = timeout + self.connection = connection + self.runtimeProvider = runtimeProvider + self.operationQueue = operationQueue + self.workingQueue = workingQueue + self.logger = logger + + blockEventsQueryFactory = BlockEventsQueryFactory(operationQueue: operationQueue, logger: logger) + tokenDepositEventMatchingFactory = TokenDepositEventMatcherFactory(logger: logger) + } + + private func notifyAboutStateIfNeeded() { + if let state { + let closureToNotify = notificationClosure + notificationClosure = nil + + workingQueue.async { + closureToNotify?(.success(state)) + } + } + } + + private func notifyTimeout() { + let closureToNotify = notificationClosure + notificationClosure = nil + + workingQueue.async { + closureToNotify?(.failure(XcmDepositMonitoringServiceError.timeout)) + } + } + + private func notifyCancelled() { + guard notificationClosure != nil else { + return + } + + let closureToNotify = notificationClosure + notificationClosure = nil + + workingQueue.async { + closureToNotify?(.failure(XcmDepositMonitoringServiceError.throttled)) + } + } + + private func clearTimeoutScheduler() { + scheduler?.cancel() + scheduler = nil + } + + private func setupTimeoutScheduler() { + scheduler = Scheduler(with: self, callbackQueue: workingQueue) + scheduler?.notifyAfter(timeout) + } + + private func fetchBlockAndDetectDeposit( + for hash: Data, + accountId: AccountId, + eventMatcher: TokenDepositEventMatching + ) { + let codingFactoryOperation = runtimeProvider.fetchCoderFactoryOperation() + + let blockDetailsWrapper = blockEventsQueryFactory.queryBlockDetailsWrapper( + from: connection, + runtimeProvider: runtimeProvider, + blockHash: hash + ) + + let detector = XcmTokensArrivalDetector(logger: logger) + + let matchingOperation = ClosureOperation { + let blockDetails = try blockDetailsWrapper.targetOperation.extractNoCancellableResultData() + let codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + + return detector.searchForXcmArrival( + in: blockDetails, + eventMatcher: eventMatcher, + accountId: accountId, + codingFactory: codingFactory + ) + } + + matchingOperation.addDependency(codingFactoryOperation) + matchingOperation.addDependency(blockDetailsWrapper.targetOperation) + + let totalWrapper = blockDetailsWrapper + .insertingHead(operations: [codingFactoryOperation]) + .insertingTail(operation: matchingOperation) + + let callStore = CancellableCallStore() + detectionCallsStore[hash] = callStore + + executeCancellable( + wrapper: totalWrapper, + inOperationQueue: operationQueue, + backingCallIn: callStore, + runningCallbackIn: workingQueue, + mutex: mutex + ) { [weak self] result in + guard let self else { + return + } + + detectionCallsStore[hash] = nil + + switch result { + case let .success(deposit): + if let deposit { + logger.debug("Received deposit") + state = deposit + notifyAboutStateIfNeeded() + } else { + logger.debug("No deposit in the block") + } + case let .failure(error): + logger.debug("Block processing failed: \(error)") + } + } + } + + // MARK: Protected interface + + private func setupNotificationClosure(_ closure: @escaping (Result) -> Void) { + mutex.lock() + + defer { + mutex.unlock() + } + + if let state { + workingQueue.async { + closure(.success(state)) + } + return + } + + if subscription == nil { + workingQueue.async { + closure(.failure(XcmDepositMonitoringServiceError.unsupportedAsset(self.chainAsset))) + } + return + } + + notificationClosure = closure + + setupTimeoutScheduler() + } + + private func setupIfNeeded() { + mutex.lock() + + defer { + mutex.unlock() + } + + guard subscription == nil else { + return + } + + guard let eventMatcher = tokenDepositEventMatchingFactory.createMatcher(for: chainAsset) else { + logger.error("Unsupported asset: \(chainAsset.asset.symbol)") + return + } + + subscription = WalletRemoteSubscription( + runtimeProvider: runtimeProvider, + connection: connection, + operationQueue: operationQueue + ) + + subscription?.subscribeBalance( + for: accountId, + chainAsset: chainAsset, + callbackQueue: workingQueue + ) { [weak self] result in + guard let self else { + return + } + + mutex.lock() + + defer { + mutex.unlock() + } + + switch result { + case let .success(update): + guard let blockHash = update.blockHash else { + logger.debug("No block found in update") + return + } + + logger.debug("\(accountId.toHex()) Checking block \(blockHash.toHex()) in \(chainAsset.chain.name)") + + fetchBlockAndDetectDeposit( + for: blockHash, + accountId: accountId, + eventMatcher: eventMatcher + ) + case let .failure(error): + logger.error("Remote subscription failed: \(error)") + } + } + } + + private func throttle() { + mutex.lock() + + defer { + mutex.unlock() + } + + guard subscription != nil else { + return + } + + subscription?.unsubscribe() + subscription = nil + + detectionCallsStore.values.forEach { $0.cancel() } + detectionCallsStore = [:] + + notifyCancelled() + } +} + +extension XcmDepositMonitoringService: SchedulerDelegate { + func didTrigger(scheduler _: SchedulerProtocol) { + mutex.lock() + + defer { + mutex.unlock() + } + + notifyTimeout() + + scheduler = nil + } +} + +extension XcmDepositMonitoringService: XcmDepositMonitoringServiceProtocol { + func useMonitoringWrapper() -> CompoundOperationWrapper { + setupIfNeeded() + + let operation = AsyncClosureOperation( + operationClosure: { completion in + self.setupNotificationClosure { [weak self] result in + self?.throttle() + + switch result { + case let .success(deposit): + completion(.success(deposit.amount)) + case let .failure(error): + completion(.failure(error)) + } + } + + }, cancelationClosure: { [weak self] in + self?.throttle() + } + ) + + return CompoundOperationWrapper(targetOperation: operation) + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/XcmTokensArrivalDetector.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/XcmTokensArrivalDetector.swift new file mode 100644 index 0000000000..cc6c86cade --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/XcmTokensArrivalDetector.swift @@ -0,0 +1,111 @@ +import Foundation +import SubstrateSdk + +final class XcmTokensArrivalDetector { + let logger: LoggerProtocol + + init(logger: LoggerProtocol) { + self.logger = logger + } +} + +private extension XcmTokensArrivalDetector { + func detectDepositIn( + events: [Event], + eventMatcher: TokenDepositEventMatching, + accountId: AccountId, + codingFactory: RuntimeCoderFactoryProtocol + ) -> TokenDepositEvent? { + for event in events { + if + let deposit = eventMatcher.matchDeposit(event: event, using: codingFactory), + deposit.accountId == accountId { + return deposit + } + } + + return nil + } +} + +extension XcmTokensArrivalDetector { + func searchForXcmArrivalInInherents( + in inherentEvents: SubstrateInherentsEvents, + eventMatcher: TokenDepositEventMatching, + accountId: AccountId, + codingFactory: RuntimeCoderFactoryProtocol + ) -> TokenDepositEvent? { + detectDepositIn( + events: inherentEvents.initialization + inherentEvents.finalization, + eventMatcher: eventMatcher, + accountId: accountId, + codingFactory: codingFactory + ) + } + + func searchForXcmArrivalInSetValidationData( + in extrinsicsEvents: SubstrateExtrinsicsEvents, + eventMatcher: TokenDepositEventMatching, + accountId: AccountId, + codingFactory: RuntimeCoderFactoryProtocol + ) -> TokenDepositEvent? { + let interestedCallPath = CallCodingPath(moduleName: "ParachainSystem", callName: "set_validation_data") + + let matchingEvents: [Event] = extrinsicsEvents.flatMap { extrinsicEvents in + do { + let decoder = try codingFactory.createDecoder(from: extrinsicEvents.extrinsicData) + + let extrinsic: Extrinsic = try decoder.read(of: GenericType.extrinsic.name) + + let call = try ExtrinsicExtraction.getCall( + from: extrinsic.call, + context: codingFactory.createRuntimeJsonContext() + ) + + let callPath = CallCodingPath(moduleName: call.moduleName, callName: call.callName) + + if callPath == interestedCallPath { + logger.debug("Set validation data detected in \(extrinsicEvents.extrinsicHash.toHex())") + + return extrinsicEvents.eventRecords.map(\.event) + } else { + return [Event]() + } + } catch { + logger.error("Extrinsic processing failed: \(error)") + + return [Event]() + } + } + + return detectDepositIn( + events: matchingEvents, + eventMatcher: eventMatcher, + accountId: accountId, + codingFactory: codingFactory + ) + } + + func searchForXcmArrival( + in blockDetails: SubstrateBlockDetails, + eventMatcher: TokenDepositEventMatching, + accountId: AccountId, + codingFactory: RuntimeCoderFactoryProtocol + ) -> TokenDepositEvent? { + if let deposit = searchForXcmArrivalInInherents( + in: blockDetails.inherentsEvents, + eventMatcher: eventMatcher, + accountId: accountId, + codingFactory: codingFactory + ) { + return deposit + } + + return searchForXcmArrivalInSetValidationData( + in: blockDetails.extrinsicsWithEvents, + eventMatcher: eventMatcher, + accountId: accountId, + codingFactory: codingFactory + ) + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/XcmTransactService.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/XcmTransactService.swift new file mode 100644 index 0000000000..7c949e2688 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/XcmTransactService.swift @@ -0,0 +1,96 @@ +import Foundation +import Operation_iOS +import SubstrateSdk + +protocol XcmTransactServiceProtocol { + func transferAndWaitArrivalWrapper( + _ transferRequest: XcmTransferRequest, + destinationChainAsset: ChainAsset, + xcmTransfers: XcmTransfers, + signer: SigningWrapperProtocol + ) -> CompoundOperationWrapper +} + +final class XcmTransactService { + let chainRegistry: ChainRegistryProtocol + let transferService: XcmTransferServiceProtocol + let workingQueue: DispatchQueue + let operationQueue: OperationQueue + let logger: LoggerProtocol + + init( + chainRegistry: ChainRegistryProtocol, + transferService: XcmTransferServiceProtocol, + workingQueue: DispatchQueue, + operationQueue: OperationQueue, + logger: LoggerProtocol + ) { + self.chainRegistry = chainRegistry + self.transferService = transferService + self.workingQueue = workingQueue + self.operationQueue = operationQueue + self.logger = logger + } +} + +extension XcmTransactService: XcmTransactServiceProtocol { + func transferAndWaitArrivalWrapper( + _ transferRequest: XcmTransferRequest, + destinationChainAsset: ChainAsset, + xcmTransfers: XcmTransfers, + signer: SigningWrapperProtocol + ) -> CompoundOperationWrapper { + do { + let destinationChainId = destinationChainAsset.chain.chainId + let connection = try chainRegistry.getConnectionOrError(for: destinationChainId) + let runtimeProvider = try chainRegistry.getRuntimeProviderOrError(for: destinationChainId) + + let monitoringService = XcmDepositMonitoringService( + accountId: transferRequest.unweighted.destination.accountId, + chainAsset: destinationChainAsset, + connection: connection, + runtimeProvider: runtimeProvider, + operationQueue: operationQueue, + workingQueue: workingQueue, + logger: logger + ) + + let monitoringWrapper = monitoringService.useMonitoringWrapper() + + let submittionOperation = AsyncClosureOperation { completion in + self.transferService.submit( + request: transferRequest, + xcmTransfers: xcmTransfers, + signer: signer, + runningIn: self.workingQueue + ) { result in + // cancel monitoring in case transaction submission failed + if case .failure = result { + monitoringWrapper.cancel() + } + + completion(result) + } + } + + let mappingOperation = ClosureOperation { + _ = try submittionOperation.extractNoCancellableResultData() + + let arrivedAmount = try monitoringWrapper.targetOperation.extractNoCancellableResultData() + + self.logger.debug("Arrived amount: \(String(arrivedAmount))") + + return arrivedAmount + } + + mappingOperation.addDependency(monitoringWrapper.targetOperation) + mappingOperation.addDependency(submittionOperation) + + let dependencies = monitoringWrapper.allOperations + [submittionOperation] + + return CompoundOperationWrapper(targetOperation: mappingOperation, dependencies: dependencies) + } catch { + return .createWithError(error) + } + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/XcmTransferService+Protocol.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/XcmTransferService+Protocol.swift index 1b5ebc20aa..09338ec955 100644 --- a/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/XcmTransferService+Protocol.swift +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/XcmTransferService+Protocol.swift @@ -81,10 +81,10 @@ extension XcmTransferService: XcmTransferServiceProtocol { chainAccount: chainAccount ) - let feeWrapper = operationFactory.estimateFeeOperation { builder in + let feeWrapper = operationFactory.estimateFeeOperation({ builder in let callClosure = try callBuilderWrapper.targetOperation.extractNoCancellableResultData().0 return try callClosure(builder) - } + }, payingIn: request.originFeeAsset) feeWrapper.addDependency(wrapper: callBuilderWrapper) @@ -272,7 +272,7 @@ extension XcmTransferService: XcmTransferServiceProtocol { let submitWrapper = operationFactory.submit({ builder in let callClosure = try callBuilderWrapper.targetOperation.extractNoCancellableResultData().0 return try callClosure(builder) - }, signer: signer) + }, signer: signer, payingIn: request.originFeeAsset) submitWrapper.addDependency(wrapper: callBuilderWrapper) diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/XcmTransferService.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/XcmTransferService.swift index 53ff9c199d..9ec8bafb1d 100644 --- a/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/XcmTransferService.swift +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/XcmTransferService.swift @@ -15,9 +15,11 @@ final class XcmTransferService { let wallet: MetaAccountModel let chainRegistry: ChainRegistryProtocol - let senderResolutionFacade: ExtrinsicSenderResolutionFacadeProtocol let operationQueue: OperationQueue let metadataHashOperationFactory: MetadataHashOperationFactoryProtocol + let userStorageFacade: StorageFacadeProtocol + let substrateStorageFacade: StorageFacadeProtocol + let customFeeEstimatingFactory: ExtrinsicCustomFeeEstimatingFactoryProtocol? private(set) lazy var xcmFactory = XcmTransferFactory() private(set) lazy var xcmPalletQueryFactory = XcmPalletMetadataQueryFactory() @@ -26,15 +28,19 @@ final class XcmTransferService { init( wallet: MetaAccountModel, chainRegistry: ChainRegistryProtocol, - senderResolutionFacade: ExtrinsicSenderResolutionFacadeProtocol, metadataHashOperationFactory: MetadataHashOperationFactoryProtocol, - operationQueue: OperationQueue + userStorageFacade: StorageFacadeProtocol, + substrateStorageFacade: StorageFacadeProtocol, + operationQueue: OperationQueue, + customFeeEstimatingFactory: ExtrinsicCustomFeeEstimatingFactoryProtocol? = nil ) { self.wallet = wallet self.chainRegistry = chainRegistry - self.senderResolutionFacade = senderResolutionFacade self.metadataHashOperationFactory = metadataHashOperationFactory + self.userStorageFacade = userStorageFacade + self.substrateStorageFacade = substrateStorageFacade self.operationQueue = operationQueue + self.customFeeEstimatingFactory = customFeeEstimatingFactory } func createModuleResolutionWrapper( @@ -63,41 +69,26 @@ final class XcmTransferService { throw ChainRegistryError.runtimeMetadaUnavailable } - let senderResolvingFactory = senderResolutionFacade.createResolutionFactory( - for: chainAccount, - chainModel: chain - ) - - let feeEstimatingWrapperFactory = ExtrinsicFeeEstimatingWrapperFactory( - account: chainAccount, - chain: chain, - runtimeService: runtimeProvider, - connection: connection, - operationQueue: operationQueue - ) - - let feeEstimationRegistry = ExtrinsicFeeEstimationRegistry( - chain: chain, - estimatingWrapperFactory: feeEstimatingWrapperFactory, - connection: connection, - runtimeProvider: runtimeProvider, - userStorageFacade: UserDataStorageFacade.shared, - substrateStorageFacade: SubstrateDataStorageFacade.shared, - operationQueue: operationQueue - ) - - let signedExtensionFactory = ExtrinsicSignedExtensionFacade().createFactory(for: chain.chainId) - - return ExtrinsicOperationFactory( - chain: chain, - runtimeRegistry: runtimeProvider, - customExtensions: signedExtensionFactory.createExtensions(), - engine: connection, - feeEstimationRegistry: feeEstimationRegistry, - metadataHashOperationFactory: metadataHashOperationFactory, - senderResolvingFactory: senderResolvingFactory, - blockHashOperationFactory: BlockHashOperationFactory(), - operationManager: OperationManager(operationQueue: operationQueue) - ) + if let customFeeEstimatingFactory { + return ExtrinsicServiceFactory( + runtimeRegistry: runtimeProvider, + engine: connection, + operationQueue: operationQueue, + userStorageFacade: userStorageFacade, + substrateStorageFacade: substrateStorageFacade + ).createOperationFactory( + account: chainAccount, + chain: chain, + customFeeEstimatingFactory: customFeeEstimatingFactory + ) + } else { + return ExtrinsicServiceFactory( + runtimeRegistry: runtimeProvider, + engine: connection, + operationQueue: operationQueue, + userStorageFacade: userStorageFacade, + substrateStorageFacade: substrateStorageFacade + ).createOperationFactory(account: chainAccount, chain: chain) + } } } diff --git a/novawallet/Common/Services/RemoteSubscription/Substrate/BalanceRemoteSubscriptionService.swift b/novawallet/Common/Services/RemoteSubscription/Substrate/BalanceRemoteSubscriptionService.swift index c8736f4786..35267207a2 100644 --- a/novawallet/Common/Services/RemoteSubscription/Substrate/BalanceRemoteSubscriptionService.swift +++ b/novawallet/Common/Services/RemoteSubscription/Substrate/BalanceRemoteSubscriptionService.swift @@ -73,7 +73,7 @@ final class BalanceRemoteSubscriptionService: RemoteSubscriptionService { let storageKeyFactory = LocalStorageKeyFactory() let chainId = chainAsset.chain.chainId - let accountStoragePath = StorageCodingPath.account + let accountStoragePath = SystemPallet.accountPath let accountLocalKey = try storageKeyFactory.createFromStoragePath( accountStoragePath, accountId: accountId, diff --git a/novawallet/Common/Services/RemoteSubscription/Substrate/CrowdloanRemoteSubscriptionService.swift b/novawallet/Common/Services/RemoteSubscription/Substrate/CrowdloanRemoteSubscriptionService.swift index aa9aee2f3d..d4e5f7c986 100644 --- a/novawallet/Common/Services/RemoteSubscription/Substrate/CrowdloanRemoteSubscriptionService.swift +++ b/novawallet/Common/Services/RemoteSubscription/Substrate/CrowdloanRemoteSubscriptionService.swift @@ -35,7 +35,7 @@ class CrowdloanRemoteSubscriptionService: RemoteSubscriptionService, CrowdloanRe completion closure: RemoteSubscriptionClosure? ) -> UUID? { do { - let storagePath = StorageCodingPath.blockNumber + let storagePath = SystemPallet.blockNumberPath let localKey = try LocalStorageKeyFactory().createFromStoragePath(storagePath, chainId: chainId) let request = UnkeyedSubscriptionRequest(storagePath: storagePath, localKey: localKey) @@ -61,7 +61,7 @@ class CrowdloanRemoteSubscriptionService: RemoteSubscriptionService, CrowdloanRe completion closure: RemoteSubscriptionClosure? ) { do { - let storagePath = StorageCodingPath.blockNumber + let storagePath = SystemPallet.blockNumberPath let localKey = try LocalStorageKeyFactory().createFromStoragePath(storagePath, chainId: chainId) detachFromSubscription(localKey, subscriptionId: subscriptionId, queue: queue, closure: closure) diff --git a/novawallet/Common/Services/RemoteSubscription/Substrate/StakingAccountSubscription.swift b/novawallet/Common/Services/RemoteSubscription/Substrate/StakingAccountSubscription.swift index d0d8516616..9b2c79c32e 100644 --- a/novawallet/Common/Services/RemoteSubscription/Substrate/StakingAccountSubscription.swift +++ b/novawallet/Common/Services/RemoteSubscription/Substrate/StakingAccountSubscription.swift @@ -111,13 +111,13 @@ final class StakingAccountSubscription: WebSocketSubscribing { let stashId = try stashItem.stash.toAccountId(using: chainFormat) if stashId != accountId { - requests.append(.init(storagePath: .account, accountId: stashId)) + requests.append(.init(storagePath: SystemPallet.accountPath, accountId: stashId)) } let controllerId = try stashItem.controller.toAccountId(using: chainFormat) if controllerId != accountId { - requests.append(.init(storagePath: .account, accountId: controllerId)) + requests.append(.init(storagePath: SystemPallet.accountPath, accountId: controllerId)) } requests.append(.init(storagePath: Staking.payee, accountId: stashId)) diff --git a/novawallet/Common/Services/RemoteSubscription/Substrate/WalletRemoteQueryWrapperFactory.swift b/novawallet/Common/Services/RemoteSubscription/Substrate/WalletRemoteQueryWrapperFactory.swift index 6a3c19d7a1..adf4dfd4cf 100644 --- a/novawallet/Common/Services/RemoteSubscription/Substrate/WalletRemoteQueryWrapperFactory.swift +++ b/novawallet/Common/Services/RemoteSubscription/Substrate/WalletRemoteQueryWrapperFactory.swift @@ -14,18 +14,15 @@ final class WalletRemoteQueryWrapperFactory { let requestFactory: StorageRequestFactoryProtocol let runtimeProvider: RuntimeProviderProtocol let connection: JSONRPCEngine - let assetInfoOperationFactory: AssetStorageInfoOperationFactoryProtocol let operationQueue: OperationQueue init( requestFactory: StorageRequestFactoryProtocol, - assetInfoOperationFactory: AssetStorageInfoOperationFactoryProtocol, runtimeProvider: RuntimeProviderProtocol, connection: JSONRPCEngine, operationQueue: OperationQueue ) { self.requestFactory = requestFactory - self.assetInfoOperationFactory = assetInfoOperationFactory self.runtimeProvider = runtimeProvider self.connection = connection self.operationQueue = operationQueue @@ -42,7 +39,7 @@ final class WalletRemoteQueryWrapperFactory { factory: { try codingFactoryOperation.extractNoCancellableResultData() }, - storagePath: StorageCodingPath.account + storagePath: SystemPallet.accountPath ) wrapper.addDependency(operations: [codingFactoryOperation]) diff --git a/novawallet/Common/Services/RemoteSubscription/Substrate/WalletRemoteSubscription.swift b/novawallet/Common/Services/RemoteSubscription/Substrate/WalletRemoteSubscription.swift new file mode 100644 index 0000000000..0fcc5e176f --- /dev/null +++ b/novawallet/Common/Services/RemoteSubscription/Substrate/WalletRemoteSubscription.swift @@ -0,0 +1,340 @@ +import Foundation +import SubstrateSdk + +struct WalletRemoteSubscriptionUpdate { + let balance: AssetBalance? + let blockHash: Data? +} + +typealias WalletRemoteSubscriptionClosure = (Result) -> Void + +protocol WalletRemoteSubscriptionProtocol { + func subscribeBalance( + for accountId: AccountId, + chainAsset: ChainAsset, + callbackQueue: DispatchQueue, + callbackClosure: @escaping WalletRemoteSubscriptionClosure + ) + + func unsubscribe() +} + +final class WalletRemoteSubscription { + let runtimeProvider: RuntimeProviderProtocol + let connection: JSONRPCEngine + let operationQueue: OperationQueue + + private var unsubscribeClosure: (() -> Void)? + + init( + runtimeProvider: RuntimeProviderProtocol, + connection: JSONRPCEngine, + operationQueue: OperationQueue + ) { + self.runtimeProvider = runtimeProvider + self.connection = connection + self.operationQueue = operationQueue + } + + deinit { + doUnsubscribe() + } + + func doUnsubscribe() { + unsubscribeClosure?() + unsubscribeClosure = nil + } + + private func subscribeNativeBalance( + for accountId: AccountId, + chainAsset: ChainAsset, + callbackQueue: DispatchQueue, + callbackClosure: @escaping WalletRemoteSubscriptionClosure + ) { + let request = MapSubscriptionRequest( + storagePath: SystemPallet.accountPath, + localKey: "", + keyParamClosure: { + BytesCodable(wrappedValue: accountId) + } + ) + + let subscription = CallbackStorageSubscription( + request: request, + connection: connection, + runtimeService: runtimeProvider, + repository: nil, + operationQueue: operationQueue, + callbackWithBlockQueue: callbackQueue + ) { result in + switch result { + case let .success(valueWithBlock): + let assetBalance = valueWithBlock.value.map { accountInfo in + AssetBalance( + accountInfo: accountInfo, + chainAssetId: chainAsset.chainAssetId, + accountId: accountId + ) + } + + let callbackValue = WalletRemoteSubscriptionUpdate( + balance: assetBalance, + blockHash: valueWithBlock.blockHash + ) + + callbackClosure(.success(callbackValue)) + case let .failure(error): + callbackClosure(.failure(error)) + } + } + + unsubscribeClosure = { + subscription.unsubscribe() + } + } + + private func prepareAssetsBalanceRequests( + accountId: AccountId, + extras: StatemineAssetExtras + ) -> [BatchStorageSubscriptionRequest] { + let accountRequest = DoubleMapSubscriptionRequest( + storagePath: StorageCodingPath.assetsAccount(from: extras.palletName), + localKey: "", + keyParamClosure: { + (extras.assetId, BytesCodable(wrappedValue: accountId)) + }, + param1Encoder: StatemineAssetSerializer.subscriptionKeyEncoder(for: extras.assetId), + param2Encoder: nil + ) + + let detailsRequest = MapSubscriptionRequest( + storagePath: StorageCodingPath.assetsDetails(from: extras.palletName), + localKey: "", + keyParamClosure: { + extras.assetId + }, + paramEncoder: StatemineAssetSerializer.subscriptionKeyEncoder(for: extras.assetId) + ) + + return [ + BatchStorageSubscriptionRequest( + innerRequest: accountRequest, + mappingKey: AssetsPalletBalanceStateChange.Key.account.rawValue + ), + BatchStorageSubscriptionRequest( + innerRequest: detailsRequest, + mappingKey: AssetsPalletBalanceStateChange.Key.details.rawValue + ) + ] + } + + private func subscribeAssetsAccountBalance( + for accountId: AccountId, + chainAsset: ChainAsset, + extras: StatemineAssetExtras, + callbackQueue: DispatchQueue, + callbackClosure: @escaping WalletRemoteSubscriptionClosure + ) { + let requests = prepareAssetsBalanceRequests(accountId: accountId, extras: extras) + var state = AssetsPalletBalanceState(account: nil, details: nil) + + let subscription = CallbackBatchStorageSubscription( + requests: requests, + connection: connection, + runtimeService: runtimeProvider, + repository: nil, + operationQueue: operationQueue, + callbackQueue: callbackQueue + ) { result in + switch result { + case let .success(change): + state = state.applying(change: change) + + let assetBalance = AssetBalance( + assetsAccount: state.account, + assetsDetails: state.details, + chainAssetId: chainAsset.chainAssetId, + accountId: accountId + ) + + callbackClosure(.success(.init(balance: assetBalance, blockHash: change.blockHash))) + case let .failure(error): + callbackClosure(.failure(error)) + } + } + + unsubscribeClosure = { + subscription.unsubscribe() + } + + subscription.subscribe() + } + + private func subscribeOrmlAccountBalance( + for accountId: AccountId, + chainAsset: ChainAsset, + currencyId: Data, + callbackQueue: DispatchQueue, + callbackClosure: @escaping WalletRemoteSubscriptionClosure + ) { + let request = DoubleMapSubscriptionRequest( + storagePath: StorageCodingPath.ormlTokenAccount, + localKey: "", + keyParamClosure: { + (BytesCodable(wrappedValue: accountId), BytesCodable(wrappedValue: currencyId)) + }, + param1Encoder: nil, + param2Encoder: { $0.wrappedValue } + ) + + let subscription = CallbackStorageSubscription( + request: request, + connection: connection, + runtimeService: runtimeProvider, + repository: nil, + operationQueue: operationQueue, + callbackWithBlockQueue: callbackQueue + ) { result in + switch result { + case let .success(valueWithBlock): + let assetBalance = valueWithBlock.value.map { account in + AssetBalance( + ormlAccount: account, + chainAssetId: chainAsset.chainAssetId, + accountId: accountId + ) + } + + let callbackValue = WalletRemoteSubscriptionUpdate( + balance: assetBalance, + blockHash: valueWithBlock.blockHash + ) + + callbackClosure(.success(callbackValue)) + case let .failure(error): + callbackClosure(.failure(error)) + } + } + + unsubscribeClosure = { + subscription.unsubscribe() + } + } +} + +extension WalletRemoteSubscription: WalletRemoteSubscriptionProtocol { + func subscribeBalance( + for accountId: AccountId, + chainAsset: ChainAsset, + callbackQueue: DispatchQueue, + callbackClosure: @escaping WalletRemoteSubscriptionClosure + ) { + do { + return try CustomAssetMapper( + type: chainAsset.asset.type, + typeExtras: chainAsset.asset.typeExtras + ).mapAssetWithExtras( + nativeHandler: { + self.subscribeNativeBalance( + for: accountId, + chainAsset: chainAsset, + callbackQueue: callbackQueue, + callbackClosure: callbackClosure + ) + }, + statemineHandler: { extras in + self.subscribeAssetsAccountBalance( + for: accountId, + chainAsset: chainAsset, + extras: extras, + callbackQueue: callbackQueue, + callbackClosure: callbackClosure + ) + }, + ormlHandler: { extras in + do { + let currencyId = try Data(hexString: extras.currencyIdScale) + return self.subscribeOrmlAccountBalance( + for: accountId, + chainAsset: chainAsset, + currencyId: currencyId, + callbackQueue: callbackQueue, + callbackClosure: callbackClosure + ) + } catch { + callbackQueue.async { callbackClosure(.failure(error)) } + } + }, + evmHandler: { _ in + callbackQueue.async { + callbackClosure(.failure(WalletRemoteQueryWrapperFactoryError.unsupported)) + } + }, + evmNativeHandler: { + callbackQueue.async { + callbackClosure(.failure(WalletRemoteQueryWrapperFactoryError.unsupported)) + } + }, + equilibriumHandler: { _ in + callbackQueue.async { + callbackClosure(.failure(WalletRemoteQueryWrapperFactoryError.unsupported)) + } + } + ) + } catch { + callbackQueue.async { callbackClosure(.failure(error)) } + } + } + + func unsubscribe() { + doUnsubscribe() + } +} + +private struct AssetsPalletBalanceStateChange: BatchStorageSubscriptionResult { + enum Key: String { + case account + case details + } + + let account: UncertainStorage + let details: UncertainStorage + let blockHash: Data? + + init( + values: [BatchStorageSubscriptionResultValue], + blockHashJson: JSON, + context: [CodingUserInfoKey: Any]? + ) throws { + account = try UncertainStorage( + values: values, + mappingKey: Key.account.rawValue, + context: context + ) + + details = try UncertainStorage( + values: values, + mappingKey: Key.details.rawValue, + context: context + ) + + blockHash = try blockHashJson.map(to: Data?.self, with: context) + } +} + +private struct AssetsPalletBalanceState { + let account: PalletAssets.Account? + let details: PalletAssets.Details? + + init(account: PalletAssets.Account?, details: PalletAssets.Details?) { + self.account = account + self.details = details + } + + func applying(change: AssetsPalletBalanceStateChange) -> Self { + .init( + account: change.account.valueWhenDefined(else: account), + details: change.details.valueWhenDefined(else: details) + ) + } +} diff --git a/novawallet/Common/Services/RemoteSubscription/Substrate/WalletRemoteSubscriptionWrapper.swift b/novawallet/Common/Services/RemoteSubscription/Substrate/WalletRemoteSubscriptionWrapper.swift index b31c53d0ea..148d6a248f 100644 --- a/novawallet/Common/Services/RemoteSubscription/Substrate/WalletRemoteSubscriptionWrapper.swift +++ b/novawallet/Common/Services/RemoteSubscription/Substrate/WalletRemoteSubscriptionWrapper.swift @@ -19,27 +19,12 @@ protocol WalletRemoteSubscriptionWrapperProtocol { } final class WalletRemoteSubscriptionWrapper { - let chainRegistry: ChainRegistryProtocol let remoteSubscriptionService: BalanceRemoteSubscriptionServiceProtocol - let repositoryFactory: SubstrateRepositoryFactoryProtocol - let eventCenter: EventCenterProtocol - let operationQueue: OperationQueue - let logger: LoggerProtocol init( - remoteSubscriptionService: BalanceRemoteSubscriptionServiceProtocol, - chainRegistry: ChainRegistryProtocol, - repositoryFactory: SubstrateRepositoryFactoryProtocol, - eventCenter: EventCenterProtocol, - operationQueue: OperationQueue, - logger: LoggerProtocol + remoteSubscriptionService: BalanceRemoteSubscriptionServiceProtocol ) { self.remoteSubscriptionService = remoteSubscriptionService - self.chainRegistry = chainRegistry - self.repositoryFactory = repositoryFactory - self.eventCenter = eventCenter - self.operationQueue = operationQueue - self.logger = logger } } diff --git a/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+AssetHubSwapMatching.swift b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+AssetHubSwapMatching.swift index 87830c947f..90661612af 100644 --- a/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+AssetHubSwapMatching.swift +++ b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+AssetHubSwapMatching.swift @@ -154,7 +154,7 @@ extension ExtrinsicProcessor { return try findSuccessAssetHubSwapResult( from: call, callSender: mappingResult.callSender, - eventRecords: eventRecords, + eventRecords: eventRecords.filter { $0.extrinsicIndex == extrinsicIndex }, customFee: customFee, codingFactory: codingFactory ) @@ -192,9 +192,9 @@ extension ExtrinsicProcessor { } guard - let swap = findAssetHubSwap(swapEvents, customFee: customFee), - let remoteAssetIn = swap.path.first, - let remoteAssetOut = swap.path.last + let swap = swapEvents.last, + let remoteAssetIn = swap.path.first?.asset, + let remoteAssetOut = swap.path.last?.asset else { return nil } @@ -293,26 +293,4 @@ extension ExtrinsicProcessor { isSuccess: false ) } - - private func findAssetHubSwap( - _ swapEvents: [AssetConversionPallet.SwapExecutedEvent], - customFee: Fee? - ) -> AssetConversionPallet.SwapExecutedEvent? { - guard customFee != nil else { - return swapEvents.first - } - - let optFeeSwap = swapEvents.first - let swapsAfterFee = swapEvents.dropFirst() - - guard - let feeSwap = optFeeSwap, - let targetSwap = swapsAfterFee.first, - let feeAssetOut = feeSwap.path.last, - case .native = AssetHubTokensConverter.convertFromMultilocation(feeAssetOut, chain: chain) else { - return nil - } - - return targetSwap - } } diff --git a/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+CustomFee.swift b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+CustomFee.swift index 6907d1ed28..fc59628b45 100644 --- a/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+CustomFee.swift +++ b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+CustomFee.swift @@ -72,7 +72,7 @@ extension ExtrinsicProcessor { return nil } - let assetId = try HydraDxTokenConverter.converToLocal( + let assetId = try HydraDxTokenConverter.convertToLocal( for: depositedModel.currencyId.value, chain: chain, codingFactory: codingFactory diff --git a/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+Events.swift b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+Events.swift index 1f1ce045f9..4c485362ad 100644 --- a/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+Events.swift +++ b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+Events.swift @@ -9,9 +9,9 @@ extension ExtrinsicProcessor { context: RuntimeJsonContext ) throws -> BigUInt? { try eventRecords.first { record in - metadata.createEventCodingPath(from: record.event) == .balancesTransfer + metadata.createEventCodingPath(from: record.event) == BalancesPallet.balancesTransfer }.map { eventRecord in - try eventRecord.event.params.map(to: BalancesTransferEvent.self, with: context.toRawContext()) + try eventRecord.event.params.map(to: BalancesPallet.TransferEvent.self, with: context.toRawContext()) }?.amount } diff --git a/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+Fee.swift b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+Fee.swift index 72cef7e588..363f9a94cc 100644 --- a/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+Fee.swift +++ b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+Fee.swift @@ -112,20 +112,19 @@ extension ExtrinsicProcessor { metadata: RuntimeMetadataProtocol, runtimeJsonContext: RuntimeJsonContext ) -> Fee? { - let withdraw = EventCodingPath.balancesWithdraw + let withdrawPath = BalancesPallet.balancesWithdraw let closure: (EventRecord) -> Bool = { record in guard record.extrinsicIndex == index, let eventPath = metadata.createEventCodingPath(from: record.event) else { return false } - guard eventPath.moduleName == withdraw.moduleName, - eventPath.eventName == withdraw.eventName else { + guard eventPath == withdrawPath else { return false } guard let event = try? record.event.params.map( - to: BalancesWithdrawEvent.self, + to: BalancesPallet.WithdrawEvent.self, with: runtimeJsonContext.toRawContext() ) else { return false @@ -137,7 +136,7 @@ extension ExtrinsicProcessor { guard let record = eventRecords.first(where: closure), let event = try? record.event.params.map( - to: BalancesWithdrawEvent.self, + to: BalancesPallet.WithdrawEvent.self, with: runtimeJsonContext.toRawContext() ) else { return nil @@ -155,7 +154,7 @@ extension ExtrinsicProcessor { metadata: RuntimeMetadataProtocol, runtimeJsonContext: RuntimeJsonContext ) -> Fee? { - let balances = EventCodingPath.balancesDeposit + let balancesPath = BalancesPallet.balancesDeposit let maybeBalancesDeposit: BigUInt? = eventRecords.last { record in guard record.extrinsicIndex == index, @@ -163,11 +162,10 @@ extension ExtrinsicProcessor { return false } - return eventPath.moduleName == balances.moduleName && - eventPath.eventName == balances.eventName + return eventPath == balancesPath }.map { record in let event = try? record.event.params.map( - to: BalanceDepositEvent.self, + to: BalancesPallet.DepositEvent.self, with: runtimeJsonContext.toRawContext() ) return event?.amount ?? 0 diff --git a/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+HydraSwapMatching.swift b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+HydraSwapMatching.swift index 660779885a..8cd96691a4 100644 --- a/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+HydraSwapMatching.swift +++ b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+HydraSwapMatching.swift @@ -241,7 +241,7 @@ extension ExtrinsicProcessor { from: params, callSender: mappingResult.callSender, call: call, - eventRecords: eventRecords, + eventRecords: eventRecords.filter { $0.extrinsicIndex == extrinsicIndex }, codingFactory: codingFactory ) @@ -277,13 +277,13 @@ extension ExtrinsicProcessor { return nil } - let assetIn = try HydraDxTokenConverter.converToLocal( + let assetIn = try HydraDxTokenConverter.convertToLocal( for: swapArgs.assetIn, chain: chain, codingFactory: codingFactory ) - let assetOut = try HydraDxTokenConverter.converToLocal( + let assetOut = try HydraDxTokenConverter.convertToLocal( for: swapArgs.assetOut, chain: chain, codingFactory: codingFactory @@ -313,13 +313,13 @@ extension ExtrinsicProcessor { return nil } - let assetIn = try HydraDxTokenConverter.converToLocal( + let assetIn = try HydraDxTokenConverter.convertToLocal( for: swapArgs.assetIn, chain: chain, codingFactory: codingFactory ) - let assetOut = try HydraDxTokenConverter.converToLocal( + let assetOut = try HydraDxTokenConverter.convertToLocal( for: swapArgs.assetOut, chain: chain, codingFactory: codingFactory diff --git a/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+Matching.swift b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+Matching.swift index e0325aee08..4463d69c1d 100644 --- a/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+Matching.swift +++ b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+Matching.swift @@ -40,8 +40,11 @@ extension ExtrinsicProcessor { return false } - return [.extrisicSuccess, .extrinsicFailed].contains(eventPath) - }.first.map { metadata.createEventCodingPath(from: $0.event) == .extrisicSuccess } + return [ + SystemPallet.extrinsicSuccessEventPath, + SystemPallet.extrinsicFailedEventPath + ].contains(eventPath) + }.first.map { metadata.createEventCodingPath(from: $0.event) == SystemPallet.extrinsicSuccessEventPath } } func matchOrmlTransfer( diff --git a/novawallet/Common/Services/TransactionSubscription/TransactionSubscription.swift b/novawallet/Common/Services/TransactionSubscription/TransactionSubscription.swift index 7bd44e30d8..4712875e95 100644 --- a/novawallet/Common/Services/TransactionSubscription/TransactionSubscription.swift +++ b/novawallet/Common/Services/TransactionSubscription/TransactionSubscription.swift @@ -128,12 +128,12 @@ final class TransactionSubscription { connection: JSONRPCEngine, blockHash: Data ) throws -> CompoundOperationWrapper<[StorageResponse<[EventRecord]>]> { - let eventsKey = try StorageKeyFactory().key(from: .events) + let eventsKey = try StorageKeyFactory().key(from: SystemPallet.eventsPath) return storageRequestFactory.queryItems( engine: connection, keys: { [eventsKey] }, factory: { try coderFactoryOperation.extractNoCancellableResultData() }, - storagePath: .events, + storagePath: SystemPallet.eventsPath, at: blockHash ) } diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/AccountInfoSubscription.swift b/novawallet/Common/Services/WebSocketService/StorageSubscription/AccountInfoSubscription.swift index ecd9cc8199..48f74254c5 100644 --- a/novawallet/Common/Services/WebSocketService/StorageSubscription/AccountInfoSubscription.swift +++ b/novawallet/Common/Services/WebSocketService/StorageSubscription/AccountInfoSubscription.swift @@ -46,7 +46,7 @@ final class AccountInfoSubscription { let codingFactoryOperation = runtimeProvider.fetchCoderFactoryOperation() let decodingOperation = StorageFallbackDecodingOperation( - path: .account, + path: SystemPallet.accountPath, data: item ) diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/CallbackStorageSubscription.swift b/novawallet/Common/Services/WebSocketService/StorageSubscription/CallbackStorageSubscription.swift index 3cb8538671..0347a28147 100644 --- a/novawallet/Common/Services/WebSocketService/StorageSubscription/CallbackStorageSubscription.swift +++ b/novawallet/Common/Services/WebSocketService/StorageSubscription/CallbackStorageSubscription.swift @@ -142,7 +142,7 @@ final class CallbackStorageSubscription { } } - private func unsubscribe() { + func unsubscribe() { mutex.lock() defer { diff --git a/novawallet/Common/Storage/EntityToModel/ChainModelMapper.swift b/novawallet/Common/Storage/EntityToModel/ChainModelMapper.swift index c1c826ddd7..b3b54f40ed 100644 --- a/novawallet/Common/Storage/EntityToModel/ChainModelMapper.swift +++ b/novawallet/Common/Storage/EntityToModel/ChainModelMapper.swift @@ -445,8 +445,8 @@ extension ChainModelMapper: CoreDataMapperProtocol { entity.noSubstrateRuntime = model.noSubstrateRuntime entity.hasSwapHub = model.hasSwapHub entity.hasSwapHydra = model.hasSwapHydra - entity.hasAssetHubTransferFees = model.hasAssetHubTransferFees - entity.hasHydrationTransferFees = model.hasHydrationTransferFees + entity.hasAssetHubTransferFees = model.hasAssetHubFees + entity.hasHydrationTransferFees = model.hasHydrationFees entity.hasProxy = model.hasProxy entity.hasPushNotifications = model.hasPushNotifications entity.order = model.order diff --git a/novawallet/Common/Substrate/Calls/AssetConversionPallet/AssetConversionPallet+Event.swift b/novawallet/Common/Substrate/Calls/AssetConversionPallet/AssetConversionPallet+Event.swift index c915c35e94..4b8fd1908e 100644 --- a/novawallet/Common/Substrate/Calls/AssetConversionPallet/AssetConversionPallet+Event.swift +++ b/novawallet/Common/Substrate/Calls/AssetConversionPallet/AssetConversionPallet+Event.swift @@ -7,10 +7,24 @@ extension AssetConversionPallet { .init(moduleName: AssetConversionPallet.name, eventName: "SwapExecuted") } + struct BalancePathItem: Codable { + let asset: AssetId + let amount: BigUInt + + init(from decoder: Decoder) throws { + var unkeyedContainer = try decoder.unkeyedContainer() + + asset = try unkeyedContainer.decode(AssetId.self) + amount = try unkeyedContainer.decode(StringScaleMapper.self).value + } + } + + typealias BalancePath = [BalancePathItem] + struct SwapExecutedEvent: Codable { let who: AccountId let sendTo: AccountId - let path: [AssetId] + let path: BalancePath let amountIn: BigUInt let amountOut: BigUInt @@ -19,9 +33,9 @@ extension AssetConversionPallet { who = try unkeyedContainer.decode(BytesCodable.self).wrappedValue sendTo = try unkeyedContainer.decode(BytesCodable.self).wrappedValue - path = try unkeyedContainer.decode([AssetId].self) amountIn = try unkeyedContainer.decode(StringScaleMapper.self).value amountOut = try unkeyedContainer.decode(StringScaleMapper.self).value + path = try unkeyedContainer.decode(BalancePath.self) } } } diff --git a/novawallet/Common/Substrate/Types/Assets/PalletAssets+EventCodingPath.swift b/novawallet/Common/Substrate/Types/Assets/PalletAssets+EventCodingPath.swift new file mode 100644 index 0000000000..98484e7f73 --- /dev/null +++ b/novawallet/Common/Substrate/Types/Assets/PalletAssets+EventCodingPath.swift @@ -0,0 +1,7 @@ +import Foundation + +extension PalletAssets { + static func issuedPath(for moduleName: String?) -> EventCodingPath { + EventCodingPath(moduleName: moduleName ?? PalletAssets.name, eventName: "Issued") + } +} diff --git a/novawallet/Common/Substrate/Types/Assets/PalletAssets+Events.swift b/novawallet/Common/Substrate/Types/Assets/PalletAssets+Events.swift new file mode 100644 index 0000000000..ffd32c65b7 --- /dev/null +++ b/novawallet/Common/Substrate/Types/Assets/PalletAssets+Events.swift @@ -0,0 +1,18 @@ +import Foundation +import SubstrateSdk + +extension PalletAssets { + struct IssuedEvent: Decodable { + let assetId: JSON + let accountId: AccountId + let amount: Balance + + init(from decoder: Decoder) throws { + var unkeyedContainer = try decoder.unkeyedContainer() + + assetId = try unkeyedContainer.decode(JSON.self) + accountId = try unkeyedContainer.decode(BytesCodable.self).wrappedValue + amount = try unkeyedContainer.decode(StringScaleMapper.self).value + } + } +} diff --git a/novawallet/Common/Substrate/Types/BalanceDepositEvent.swift b/novawallet/Common/Substrate/Types/BalanceDepositEvent.swift deleted file mode 100644 index 967172e755..0000000000 --- a/novawallet/Common/Substrate/Types/BalanceDepositEvent.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation -import SubstrateSdk -import BigInt - -struct BalanceDepositEvent: Decodable { - let accountId: AccountId - let amount: BigUInt - - init(from decoder: Decoder) throws { - var unkeyedContainer = try decoder.unkeyedContainer() - - if let rawAccountId = try? unkeyedContainer.decode(AccountId.self) { - accountId = rawAccountId - } else { - accountId = try unkeyedContainer.decode(BytesCodable.self).wrappedValue - } - - amount = try unkeyedContainer.decode(StringScaleMapper.self).value - } -} diff --git a/novawallet/Common/Substrate/Types/BalancesPallet/BalancesPallet+EventCodingPath.swift b/novawallet/Common/Substrate/Types/BalancesPallet/BalancesPallet+EventCodingPath.swift new file mode 100644 index 0000000000..729c1d574a --- /dev/null +++ b/novawallet/Common/Substrate/Types/BalancesPallet/BalancesPallet+EventCodingPath.swift @@ -0,0 +1,19 @@ +import Foundation + +extension BalancesPallet { + static var balancesDeposit: EventCodingPath { + EventCodingPath(moduleName: Self.name, eventName: "Deposit") + } + + static var balancesWithdraw: EventCodingPath { + EventCodingPath(moduleName: Self.name, eventName: "Withdraw") + } + + static var balancesTransfer: EventCodingPath { + EventCodingPath(moduleName: Self.name, eventName: "Transfer") + } + + static var balancesMinted: EventCodingPath { + EventCodingPath(moduleName: Self.name, eventName: "Minted") + } +} diff --git a/novawallet/Common/Substrate/Types/BalancesPallet/BalancesPallet+Events.swift b/novawallet/Common/Substrate/Types/BalancesPallet/BalancesPallet+Events.swift new file mode 100644 index 0000000000..e37ff50cc6 --- /dev/null +++ b/novawallet/Common/Substrate/Types/BalancesPallet/BalancesPallet+Events.swift @@ -0,0 +1,57 @@ +import Foundation +import SubstrateSdk +import BigInt + +extension BalancesPallet { + struct DepositEvent: Decodable { + let accountId: AccountId + let amount: BigUInt + + init(from decoder: Decoder) throws { + var unkeyedContainer = try decoder.unkeyedContainer() + + accountId = try unkeyedContainer.decode(BytesCodable.self).wrappedValue + amount = try unkeyedContainer.decode(StringScaleMapper.self).value + } + } + + struct WithdrawEvent: Decodable { + let accountId: AccountId + let amount: BigUInt + + init(from decoder: Decoder) throws { + var unkeyedContainer = try decoder.unkeyedContainer() + + accountId = try unkeyedContainer.decode(BytesCodable.self).wrappedValue + amount = try unkeyedContainer.decode(StringScaleMapper.self).value + } + } + + struct MintedEvent: Decodable { + let accountId: AccountId + let amount: BigUInt + + init(from decoder: Decoder) throws { + var unkeyedContainer = try decoder.unkeyedContainer() + + accountId = try unkeyedContainer.decode(BytesCodable.self).wrappedValue + amount = try unkeyedContainer.decode(StringScaleMapper.self).value + } + } + + struct TransferEvent: Decodable { + let sender: AccountId + let receiver: AccountId + let amount: BigUInt + + init(from decoder: Decoder) throws { + var unkeyedContainer = try decoder.unkeyedContainer() + + sender = try unkeyedContainer.decode(AccountIdCodingWrapper.self).wrappedValue + + receiver = try unkeyedContainer.decode(AccountIdCodingWrapper.self).wrappedValue + + amount = try unkeyedContainer.decode(StringScaleMapper.self).value + } + } +} diff --git a/novawallet/Common/Substrate/Types/BalancesTransferEvent.swift b/novawallet/Common/Substrate/Types/BalancesTransferEvent.swift deleted file mode 100644 index f84e6b3ef7..0000000000 --- a/novawallet/Common/Substrate/Types/BalancesTransferEvent.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation -import SubstrateSdk -import BigInt - -struct BalancesTransferEvent: Decodable { - let sender: AccountId - let receiver: AccountId - let amount: BigUInt - - init(from decoder: Decoder) throws { - var unkeyedContainer = try decoder.unkeyedContainer() - - sender = try unkeyedContainer.decode(AccountIdCodingWrapper.self).wrappedValue - - receiver = try unkeyedContainer.decode(AccountIdCodingWrapper.self).wrappedValue - - amount = try unkeyedContainer.decode(StringScaleMapper.self).value - } -} diff --git a/novawallet/Common/Substrate/Types/BalancesWithdrawEvent.swift b/novawallet/Common/Substrate/Types/BalancesWithdrawEvent.swift deleted file mode 100644 index 4e8a98acd1..0000000000 --- a/novawallet/Common/Substrate/Types/BalancesWithdrawEvent.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation -import SubstrateSdk -import BigInt - -struct BalancesWithdrawEvent: Decodable { - let accountId: AccountId - let amount: BigUInt - - init(from decoder: Decoder) throws { - var unkeyedContainer = try decoder.unkeyedContainer() - - if let rawAccountId = try? unkeyedContainer.decode(AccountId.self) { - accountId = rawAccountId - } else { - accountId = try unkeyedContainer.decode(BytesCodable.self).wrappedValue - } - - amount = try unkeyedContainer.decode(StringScaleMapper.self).value - } -} diff --git a/novawallet/Common/Substrate/Types/EventCodingPath.swift b/novawallet/Common/Substrate/Types/EventCodingPath.swift index 7c21003b44..30ab7251ba 100644 --- a/novawallet/Common/Substrate/Types/EventCodingPath.swift +++ b/novawallet/Common/Substrate/Types/EventCodingPath.swift @@ -12,30 +12,10 @@ struct EventCodingPath: Equatable, Hashable { } extension EventCodingPath { - static var extrisicSuccess: EventCodingPath { - EventCodingPath(moduleName: "System", eventName: "ExtrinsicSuccess") - } - - static var extrinsicFailed: EventCodingPath { - EventCodingPath(moduleName: "System", eventName: "ExtrinsicFailed") - } - - static var balancesDeposit: EventCodingPath { - EventCodingPath(moduleName: "Balances", eventName: "Deposit") - } - static var treasuryDeposit: EventCodingPath { EventCodingPath(moduleName: "Treasury", eventName: "Deposit") } - static var balancesWithdraw: EventCodingPath { - EventCodingPath(moduleName: "Balances", eventName: "Withdraw") - } - - static var balancesTransfer: EventCodingPath { - EventCodingPath(moduleName: "Balances", eventName: "Transfer") - } - static var tokensTransfer: EventCodingPath { EventCodingPath(moduleName: "Tokens", eventName: "Transfer") } diff --git a/novawallet/Common/Substrate/Types/EventRecord.swift b/novawallet/Common/Substrate/Types/EventRecord.swift index c0658336cd..1217664e62 100644 --- a/novawallet/Common/Substrate/Types/EventRecord.swift +++ b/novawallet/Common/Substrate/Types/EventRecord.swift @@ -41,6 +41,33 @@ enum Phase: Decodable { case finalization case initialization + var isInitialization: Bool { + switch self { + case .initialization: + return true + case .applyExtrinsic, .finalization: + return false + } + } + + var isFinalization: Bool { + switch self { + case .finalization: + return true + case .applyExtrinsic, .initialization: + return false + } + } + + var isExtrinsicApplication: Bool { + switch self { + case .applyExtrinsic: + return true + case .finalization, .initialization: + return false + } + } + init(from decoder: Decoder) throws { var container = try decoder.unkeyedContainer() let type = try container.decode(String.self) @@ -75,3 +102,37 @@ struct Event: Decodable { params = try unkeyedContainer.decode(JSON.self) } } + +struct ExtrinsicFailedEventParams: Decodable { + let dispatchError: DispatchExtrinsicError + + init(from decoder: Decoder) throws { + var unkeyedContainer = try decoder.unkeyedContainer() + + dispatchError = try unkeyedContainer.decode(DispatchExtrinsicError.self) + } +} + +enum DispatchExtrinsicError: Decodable, Error { + struct ModuleError: Decodable { + @StringCodable var index: UInt8 + @BytesCodable var error: Data + } + + case module(ModuleError) + case other(String) + + init(from decoder: Decoder) throws { + var unkeyedContainer = try decoder.unkeyedContainer() + + let module = try unkeyedContainer.decode(String.self) + + switch module { + case "Module": + let moduleError = try unkeyedContainer.decode(ModuleError.self) + self = .module(moduleError) + default: + self = .other(module) + } + } +} diff --git a/novawallet/Common/Substrate/Types/StorageCodingPath.swift b/novawallet/Common/Substrate/Types/StorageCodingPath.swift index 43943b9477..9e9fb08adc 100644 --- a/novawallet/Common/Substrate/Types/StorageCodingPath.swift +++ b/novawallet/Common/Substrate/Types/StorageCodingPath.swift @@ -6,14 +6,6 @@ struct StorageCodingPath: Equatable { } extension StorageCodingPath { - static var account: StorageCodingPath { - StorageCodingPath(moduleName: "System", itemName: "Account") - } - - static var events: StorageCodingPath { - StorageCodingPath(moduleName: "System", itemName: "Events") - } - static var totalIssuance: StorageCodingPath { StorageCodingPath(moduleName: "Balances", itemName: "TotalIssuance") } @@ -50,10 +42,6 @@ extension StorageCodingPath { StorageCodingPath(moduleName: "Crowdloan", itemName: "Funds") } - static var blockNumber: StorageCodingPath { - StorageCodingPath(moduleName: "System", itemName: "Number") - } - static var timestampNow: StorageCodingPath { StorageCodingPath(moduleName: "Timestamp", itemName: "Now") } diff --git a/novawallet/Common/Substrate/Types/System/SystemPallet+Events.swift b/novawallet/Common/Substrate/Types/System/SystemPallet+Events.swift new file mode 100644 index 0000000000..a07e275ac3 --- /dev/null +++ b/novawallet/Common/Substrate/Types/System/SystemPallet+Events.swift @@ -0,0 +1,11 @@ +import Foundation + +extension SystemPallet { + static var extrinsicSuccessEventPath: EventCodingPath { + .init(moduleName: name, eventName: "ExtrinsicSuccess") + } + + static var extrinsicFailedEventPath: EventCodingPath { + .init(moduleName: name, eventName: "ExtrinsicFailed") + } +} diff --git a/novawallet/Common/Substrate/Types/System/SystemPallet+StoragePath.swift b/novawallet/Common/Substrate/Types/System/SystemPallet+StoragePath.swift new file mode 100644 index 0000000000..f3f3756089 --- /dev/null +++ b/novawallet/Common/Substrate/Types/System/SystemPallet+StoragePath.swift @@ -0,0 +1,15 @@ +import Foundation + +extension SystemPallet { + static var accountPath: StorageCodingPath { + .init(moduleName: name, itemName: "Account") + } + + static var blockNumberPath: StorageCodingPath { + StorageCodingPath(moduleName: name, itemName: "Number") + } + + static var eventsPath: StorageCodingPath { + StorageCodingPath(moduleName: name, itemName: "Events") + } +} diff --git a/novawallet/Common/Substrate/Types/System/SystemPallet.swift b/novawallet/Common/Substrate/Types/System/SystemPallet.swift new file mode 100644 index 0000000000..56fbdd85b0 --- /dev/null +++ b/novawallet/Common/Substrate/Types/System/SystemPallet.swift @@ -0,0 +1,5 @@ +import Foundation + +enum SystemPallet { + static let name = "System" +} diff --git a/novawallet/Common/View/AssetAmountView.swift b/novawallet/Common/View/AssetAmountView.swift new file mode 100644 index 0000000000..4de6e558dd --- /dev/null +++ b/novawallet/Common/View/AssetAmountView.swift @@ -0,0 +1,16 @@ +import UIKit + +class AssetAmountView: GenericPairValueView { + var assetIconView: AssetIconView { fView } + var amountLabel: UILabel { sView } + + override init(frame: CGRect) { + super.init(frame: frame) + + configure() + } + + private func configure() { + setHorizontalAndSpacing(1) + } +} diff --git a/novawallet/Common/View/CountdownLoadingView.swift b/novawallet/Common/View/CountdownLoadingView.swift new file mode 100644 index 0000000000..1a049688e1 --- /dev/null +++ b/novawallet/Common/View/CountdownLoadingView.swift @@ -0,0 +1,104 @@ +import UIKit +import SoraUI +import SoraFoundation + +final class CountdownLoadingView: UIView { + let timerView: MultiValueView = .create { view in + view.valueTop.apply(style: .boldTitle3Primary) + view.valueBottom.apply(style: .caption1Secondary) + view.valueTop.textAlignment = .center + view.valueBottom.textAlignment = .center + view.spacing = 0 + } + + var timerLabel: UILabel { timerView.valueTop } + + private var loadingView: LoadingView = .create { view in + view.contentBackgroundColor = .clear + view.indicatorImage = R.image.countdownTimerImage() + } + + let timeUpdateAnimator = TransitionAnimator( + type: .moveIn, + duration: 0.25, + subtype: .fromTop, + curve: .easeInEaseOut + ) + + var preferredSize: CGSize { + get { + loadingView.contentSize + } + + set { + loadingView.contentSize = newValue + } + } + + convenience init() { + self.init(frame: .zero) + } + + override init(frame: CGRect) { + super.init(frame: frame) + + setupStyle() + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func bind(viewModel: CountdownLoadingView.ViewModel, animated: Bool) { + loadingView.startAnimating() + timerView.valueBottom.text = viewModel.units + + updateTimeLabel(with: viewModel.duration, animated: animated) + } + + func update(remainedTime: UInt) { + updateTimeLabel(with: remainedTime, animated: true) + } + + func updateAnimationOnAppear() { + if loadingView.isAnimating { + loadingView.stopAnimating() + loadingView.startAnimating() + } + } + + private func setupStyle() { + timerView.clipsToBounds = true + } + + private func setupLayout() { + addSubview(loadingView) + loadingView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + addSubview(timerView) + + timerView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.leading.trailing.equalToSuperview().inset(8) + } + } + + private func updateTimeLabel(with remainedTime: UInt, animated: Bool) { + timerLabel.text = String(remainedTime) + + if animated { + timeUpdateAnimator.animate(view: timerLabel, completionBlock: nil) + } + } +} + +extension CountdownLoadingView { + struct ViewModel { + let duration: UInt + let units: String + } +} diff --git a/novawallet/Common/View/LinePatternView.swift b/novawallet/Common/View/LinePatternView.swift new file mode 100644 index 0000000000..c28e86cf6d --- /dev/null +++ b/novawallet/Common/View/LinePatternView.swift @@ -0,0 +1,51 @@ +import Foundation +import SoraUI + +final class LinePatternView: UIView { + var style: Style = .defaultStyle { + didSet { + setNeedsDisplay() + } + } + + override func draw(_ rect: CGRect) { + guard let context = UIGraphicsGetCurrentContext() else { + return + } + + context.setLineWidth(style.lineWidth) + context.setStrokeColor(style.color.cgColor) + + if let pattern = style.pattern { + context.setLineDash(phase: pattern.phase, lengths: pattern.segments) + } + + context.move(to: CGPoint(x: rect.midX, y: 0)) + context.addLine(to: CGPoint(x: rect.midX, y: rect.maxY)) + context.drawPath(using: .stroke) + } +} + +extension LinePatternView { + struct Pattern { + let segments: [CGFloat] + let phase: CGFloat + } + + struct Style { + let color: UIColor + let lineWidth: CGFloat + let pattern: Pattern? + + static var defaultStyle: Style { + Style( + color: R.color.colorIconSecondary()!, + lineWidth: 1, + pattern: LinePatternView.Pattern( + segments: [2, 3], + phase: 0 + ) + ) + } + } +} diff --git a/novawallet/Common/View/OperationExecutionProgressView.swift b/novawallet/Common/View/OperationExecutionProgressView.swift new file mode 100644 index 0000000000..8e27876680 --- /dev/null +++ b/novawallet/Common/View/OperationExecutionProgressView.swift @@ -0,0 +1,132 @@ +import UIKit +import SoraUI + +final class OperationExecutionProgressView: UIView { + let backgroundView: RoundedView = .create { view in + view.apply(style: .container) + } + + let preferredSize: CGFloat = 64 + + private var loadingView: CountdownLoadingView? + private var finalStatusView: UIImageView? + + private var currentViewModel: OperationExecutionProgressView.ViewModel? + + override var intrinsicContentSize: CGSize { + .init(width: preferredSize, height: preferredSize) + } + + convenience init() { + self.init(frame: .zero) + } + + override init(frame: CGRect) { + super.init(frame: frame) + + setupStyle() + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func bind(viewModel: OperationExecutionProgressView.ViewModel) { + if + let currentViewModel, + currentViewModel.isInProgress, + case let .inProgress(inProgress) = viewModel { + self.currentViewModel = viewModel + + loadingView?.bind(viewModel: inProgress, animated: true) + + return + } + + currentViewModel = viewModel + + clearCurrentStatusView() + + switch viewModel { + case let .inProgress(viewModel): + setupLoadingView() + loadingView?.bind(viewModel: viewModel, animated: false) + case .completed: + setupFinalStatusView(isComplete: true) + case .failed: + setupFinalStatusView(isComplete: false) + } + } + + func updateProgress(remainedTime: UInt) { + guard let currentViewModel, currentViewModel.isInProgress else { return } + + loadingView?.update(remainedTime: remainedTime) + } + + func updateAnimationOnAppear() { + loadingView?.updateAnimationOnAppear() + } + + private func setupStyle() { + backgroundView.cornerRadius = preferredSize / 2 + } + + private func setupLayout() { + addSubview(backgroundView) + + backgroundView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + + private func clearCurrentStatusView() { + loadingView?.removeFromSuperview() + loadingView = nil + + finalStatusView?.removeFromSuperview() + finalStatusView = nil + } + + private func setupLoadingView() { + let view = CountdownLoadingView() + view.preferredSize = CGSize(width: preferredSize, height: preferredSize) + configureContent(view: view) + + loadingView = view + } + + private func setupFinalStatusView(isComplete: Bool) { + let image = isComplete ? R.image.iconSwapExecutionComplete() : R.image.iconSwapExecutionFailed() + let view = UIImageView(image: image) + + configureContent(view: view) + + finalStatusView = view + } + + private func configureContent(view: UIView) { + addSubview(view) + + view.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } +} + +extension OperationExecutionProgressView { + enum ViewModel { + case inProgress(CountdownLoadingView.ViewModel) + case completed + case failed + + var isInProgress: Bool { + switch self { + case .inProgress: return true + case .completed, .failed: return false + } + } + } +} diff --git a/novawallet/Common/View/StackTable/StackTableView.swift b/novawallet/Common/View/StackTable/StackTableView.swift index 9818665eb2..5ec5119b16 100644 --- a/novawallet/Common/View/StackTable/StackTableView.swift +++ b/novawallet/Common/View/StackTable/StackTableView.swift @@ -93,6 +93,12 @@ final class StackTableView: RoundedView { updateLayout() } + func resetSeparators() { + showsSeparatorStore = [:] + + updateLayout() + } + func updateLayout() { let views = stackView.arrangedSubviews diff --git a/novawallet/Common/ViewModel/Amount/BalanceViewModelFactory.swift b/novawallet/Common/ViewModel/Amount/BalanceViewModelFactory.swift index 17895d74a6..174dfef4c8 100644 --- a/novawallet/Common/ViewModel/Amount/BalanceViewModelFactory.swift +++ b/novawallet/Common/ViewModel/Amount/BalanceViewModelFactory.swift @@ -49,7 +49,7 @@ final class BalanceViewModelFactory: PrimitiveBalanceViewModelFactory, BalanceVi priceData: PriceData? ) -> LocalizableResource { let localizableBalanceFormatter = formatterFactory.createTokenFormatter(for: targetAssetInfo) - let optLocalizablePriceFormatter = priceFormatter(for: priceData) + let localizablePriceFormatter = priceFormatter(for: priceData?.currencyId) let symbol = targetAssetInfo.symbol @@ -63,7 +63,6 @@ final class BalanceViewModelFactory: PrimitiveBalanceViewModelFactory, BalanceVi if let priceData = priceData, - let localizablePriceFormatter = optLocalizablePriceFormatter, let rate = Decimal(string: priceData.price) { let targetAmount = rate * amount diff --git a/novawallet/Common/ViewModel/Amount/PrimitiveBalanceViewModelFactory.swift b/novawallet/Common/ViewModel/Amount/PrimitiveBalanceViewModelFactory.swift index 9b80fc82c6..4563814e0c 100644 --- a/novawallet/Common/ViewModel/Amount/PrimitiveBalanceViewModelFactory.swift +++ b/novawallet/Common/ViewModel/Amount/PrimitiveBalanceViewModelFactory.swift @@ -19,11 +19,9 @@ class PrimitiveBalanceViewModelFactory: PrimitiveBalanceViewModelFactoryProtocol func priceFromFiatAmount( _ decimalValue: Decimal, - priceData: PriceData + currencyId: Int? ) -> LocalizableResource { - guard let localizableFormatter = priceFormatter(for: priceData) else { - return LocalizableResource { _ in "" } - } + let localizableFormatter = priceFormatter(for: currencyId) return LocalizableResource { locale in let formatter = localizableFormatter.value(for: locale) @@ -32,11 +30,12 @@ class PrimitiveBalanceViewModelFactory: PrimitiveBalanceViewModelFactoryProtocol } func priceFromAmount(_ amount: Decimal, priceData: PriceData) -> LocalizableResource { - guard let rate = Decimal(string: priceData.price), - let localizableFormatter = priceFormatter(for: priceData) else { + guard let rate = Decimal(string: priceData.price) else { return LocalizableResource { _ in "" } } + let localizableFormatter = priceFormatter(for: priceData.currencyId) + let targetAmount = rate * amount return LocalizableResource { locale in @@ -45,12 +44,8 @@ class PrimitiveBalanceViewModelFactory: PrimitiveBalanceViewModelFactoryProtocol } } - func priceFormatter(for priceData: PriceData?) -> LocalizableResource? { - guard let priceData = priceData else { - return nil - } - - let priceAssetInfo = priceAssetInfoFactory.createAssetBalanceDisplayInfo(from: priceData.currencyId) + func priceFormatter(for currencyId: Int?) -> LocalizableResource { + let priceAssetInfo = priceAssetInfoFactory.createAssetBalanceDisplayInfo(from: currencyId) return formatterFactory.createAssetPriceFormatter(for: priceAssetInfo) } @@ -89,7 +84,7 @@ class PrimitiveBalanceViewModelFactory: PrimitiveBalanceViewModelFactoryProtocol priceData: PriceData? ) -> LocalizableResource { let localizableAmountFormatter = formatterFactory.createInputTokenFormatter(for: targetAssetInfo) - let optLocalizablePriceFormatter = priceFormatter(for: priceData) + let localizablePriceFormatter = priceFormatter(for: priceData?.currencyId) return LocalizableResource { locale in let amountFormatter = localizableAmountFormatter.value(for: locale) @@ -98,7 +93,6 @@ class PrimitiveBalanceViewModelFactory: PrimitiveBalanceViewModelFactoryProtocol guard let priceData = priceData, - let localizablePriceFormatter = optLocalizablePriceFormatter, let rate = Decimal(string: priceData.price) else { return BalanceViewModel(amount: amountString, price: nil) } @@ -122,7 +116,7 @@ class PrimitiveBalanceViewModelFactory: PrimitiveBalanceViewModelFactoryProtocol roundingMode: roundingMode ) - let optLocalizablePriceFormatter = priceFormatter(for: priceData) + let localizablePriceFormatter = priceFormatter(for: priceData?.currencyId) return LocalizableResource { locale in let amountFormatter = localizableAmountFormatter.value(for: locale) @@ -131,7 +125,6 @@ class PrimitiveBalanceViewModelFactory: PrimitiveBalanceViewModelFactoryProtocol guard let priceData = priceData, - let localizablePriceFormatter = optLocalizablePriceFormatter, let rate = Decimal(string: priceData.price) else { return BalanceViewModel(amount: amountString, price: nil) } @@ -148,10 +141,9 @@ class PrimitiveBalanceViewModelFactory: PrimitiveBalanceViewModelFactoryProtocol func spendingAmountFromPrice( _ amount: Decimal, priceData: PriceData? - ) - -> LocalizableResource { + ) -> LocalizableResource { let localizableAmountFormatter = formatterFactory.createInputTokenFormatter(for: targetAssetInfo) - let optLocalizablePriceFormatter = priceFormatter(for: priceData) + let localizablePriceFormatter = priceFormatter(for: priceData?.currencyId) return LocalizableResource { locale in let amountFormatter = localizableAmountFormatter.value(for: locale) @@ -161,7 +153,6 @@ class PrimitiveBalanceViewModelFactory: PrimitiveBalanceViewModelFactoryProtocol guard let priceData = priceData, - let localizablePriceFormatter = optLocalizablePriceFormatter, let rate = Decimal(string: priceData.price) else { return BalanceViewModel(amount: amountString, price: nil) } diff --git a/novawallet/Common/ViewModel/Amount/PrimitiveBalanceViewModelFactoryProtocol.swift b/novawallet/Common/ViewModel/Amount/PrimitiveBalanceViewModelFactoryProtocol.swift index c88acc73a5..9504b04a2a 100644 --- a/novawallet/Common/ViewModel/Amount/PrimitiveBalanceViewModelFactoryProtocol.swift +++ b/novawallet/Common/ViewModel/Amount/PrimitiveBalanceViewModelFactoryProtocol.swift @@ -7,7 +7,7 @@ protocol PrimitiveBalanceViewModelFactoryProtocol { func priceFromFiatAmount( _ decimalValue: Decimal, - priceData: PriceData + currencyId: Int? ) -> LocalizableResource func amountFromValue( diff --git a/novawallet/Common/ViewModel/BalanceViewModelFactoryFacade.swift b/novawallet/Common/ViewModel/BalanceViewModelFactoryFacade.swift index 7e7fb54684..5903450197 100644 --- a/novawallet/Common/ViewModel/BalanceViewModelFactoryFacade.swift +++ b/novawallet/Common/ViewModel/BalanceViewModelFactoryFacade.swift @@ -7,39 +7,46 @@ protocol BalanceViewModelFactoryFacadeProtocol { amount: Decimal, priceData: PriceData ) -> LocalizableResource + + func priceFromFiatAmount( + _ decimalValue: Decimal, + currencyId: Int? + ) -> LocalizableResource + func amountFromValue( targetAssetInfo: AssetBalanceDisplayInfo, value: Decimal ) -> LocalizableResource + func balanceFromPrice( targetAssetInfo: AssetBalanceDisplayInfo, amount: Decimal, priceData: PriceData? - ) - -> LocalizableResource + ) -> LocalizableResource + func spendingAmountFromPrice( targetAssetInfo: AssetBalanceDisplayInfo, amount: Decimal, priceData: PriceData? - ) - -> LocalizableResource + ) -> LocalizableResource + func lockingAmountFromPrice( targetAssetInfo: AssetBalanceDisplayInfo, amount: Decimal, priceData: PriceData? - ) - -> LocalizableResource + ) -> LocalizableResource + func createBalanceInputViewModel( targetAssetInfo: AssetBalanceDisplayInfo, amount: Decimal? ) -> LocalizableResource + func createAssetBalanceViewModel( targetAssetInfo: AssetBalanceDisplayInfo, amount: Decimal, balance: Decimal?, priceData: PriceData? - ) - -> LocalizableResource + ) -> LocalizableResource } final class BalanceViewModelFactoryFacade { @@ -76,6 +83,18 @@ extension BalanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol { ) } + func priceFromFiatAmount( + _ decimalValue: Decimal, + currencyId: Int? + ) -> LocalizableResource { + let assetInfo = priceAssetInfoFactory.createAssetBalanceDisplayInfo(from: currencyId) + + return getOrCreateBalanceViewModelFactory(targetAssetInfo: assetInfo).priceFromFiatAmount( + decimalValue, + currencyId: currencyId + ) + } + func amountFromValue( targetAssetInfo: AssetBalanceDisplayInfo, value: Decimal diff --git a/novawallet/Modules/AppearanceSettings/AssetIconViewModelFactory.swift b/novawallet/Modules/AppearanceSettings/AssetIconViewModelFactory.swift index de8022b448..be9b7232bf 100644 --- a/novawallet/Modules/AppearanceSettings/AssetIconViewModelFactory.swift +++ b/novawallet/Modules/AppearanceSettings/AssetIconViewModelFactory.swift @@ -21,6 +21,13 @@ extension AssetIconViewModelFactoryProtocol { defaultURL: defaultURL ) } + + func createAssetIconViewModel(from assetDisplayInfo: AssetBalanceDisplayInfo) -> ImageViewModelProtocol { + createAssetIconViewModel( + for: assetDisplayInfo.icon?.getPath(), + defaultURL: assetDisplayInfo.icon?.getURL() + ) + } } class AssetIconViewModelFactory { diff --git a/novawallet/Modules/AssetConversion/Service/AssetConversionAggregationFactory.swift b/novawallet/Modules/AssetConversion/Service/AssetConversionAggregationFactory.swift deleted file mode 100644 index 250f285742..0000000000 --- a/novawallet/Modules/AssetConversion/Service/AssetConversionAggregationFactory.swift +++ /dev/null @@ -1,87 +0,0 @@ -import Foundation -import Operation_iOS - -protocol AssetConversionAggregationFactoryProtocol: AssetCanPayFeeWrapperFactoryProtocol { - func createAvailableDirectionsWrapper( - for chainAsset: ChainAsset - ) -> CompoundOperationWrapper> - - func createAvailableDirectionsWrapper( - for chain: ChainModel - ) -> CompoundOperationWrapper<[ChainAssetId: Set]> - - func createQuoteWrapper( - for state: AssetConversionFlowState, - args: AssetConversion.QuoteArgs - ) -> CompoundOperationWrapper -} - -enum AssetConversionAggregationFactoryError: Error { - case unavailableProvider(ChainModel) -} - -final class AssetConversionAggregationFactory: AssetConversionAggregationFactoryProtocol { - let operationQueue: OperationQueue - let chainRegistry: ChainRegistryProtocol - - init( - chainRegistry: ChainRegistryProtocol, - operationQueue: OperationQueue - ) { - self.chainRegistry = chainRegistry - self.operationQueue = operationQueue - } - - func createAvailableDirectionsWrapper( - for chainAsset: ChainAsset - ) -> CompoundOperationWrapper> { - if chainAsset.chain.hasSwapHub { - return createAssetHubDirections(for: chainAsset) - } else if chainAsset.chain.hasSwapHydra { - return createHydraDirections(for: chainAsset) - } else { - return CompoundOperationWrapper.createWithError( - AssetConversionAggregationFactoryError.unavailableProvider(chainAsset.chain) - ) - } - } - - func createAvailableDirectionsWrapper( - for chain: ChainModel - ) -> CompoundOperationWrapper<[ChainAssetId: Set]> { - if chain.hasSwapHub { - return createAssetHubAllDirections(for: chain) - } else if chain.hasSwapHydra { - return createHydraAllDirections(for: chain) - } else { - return CompoundOperationWrapper.createWithError( - AssetConversionAggregationFactoryError.unavailableProvider(chain) - ) - } - } - - func createQuoteWrapper( - for state: AssetConversionFlowState, - args: AssetConversion.QuoteArgs - ) -> CompoundOperationWrapper { - switch state { - case let .assetHub(assetHub): - _ = assetHub.setupReQuoteService() - return createAssetHubQuote(for: assetHub, args: args) - case let .hydra(hydra): - return createHydraQuote(for: hydra, args: args) - } - } - - func createCanPayFeeWrapper(in chainAsset: ChainAsset) -> CompoundOperationWrapper { - if chainAsset.chain.hasSwapHub { - return createAssetHubCanPayFee(for: chainAsset) - } else if chainAsset.chain.hasSwapHydra { - return createHydraCanPayFee(for: chainAsset) - } else { - return CompoundOperationWrapper.createWithError( - AssetConversionAggregationFactoryError.unavailableProvider(chainAsset.chain) - ) - } - } -} diff --git a/novawallet/Modules/AssetConversion/Service/AssetConversionExtrinsicService.swift b/novawallet/Modules/AssetConversion/Service/AssetConversionExtrinsicService.swift deleted file mode 100644 index c0fae4339c..0000000000 --- a/novawallet/Modules/AssetConversion/Service/AssetConversionExtrinsicService.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation -import Operation_iOS - -protocol AssetConversionExtrinsicServiceProtocol { - func submit( - callArgs: AssetConversion.CallArgs, - feeAsset: ChainAsset, - signer: SigningWrapperProtocol, - runCompletionIn queue: DispatchQueue, - completion closure: @escaping ExtrinsicSubmitClosure - ) -} - -enum AssetConversionExtrinsicServiceError: Error { - case remoteAssetNotFound(ChainAssetId) -} - -protocol AssetConversionCallPathFactoryProtocol { - func createHistoryCallPath(for args: AssetConversion.CallArgs) -> CallCodingPath -} diff --git a/novawallet/Modules/AssetConversion/Service/AssetConversionFeeService.swift b/novawallet/Modules/AssetConversion/Service/AssetConversionFeeService.swift deleted file mode 100644 index c22d9fc07a..0000000000 --- a/novawallet/Modules/AssetConversion/Service/AssetConversionFeeService.swift +++ /dev/null @@ -1,56 +0,0 @@ -import Foundation -import BigInt - -extension AssetConversion { - struct AmountWithNative: Equatable { - let targetAmount: BigUInt - let nativeAmount: BigUInt - } - - struct FeeModel: Equatable { - let totalFee: AmountWithNative - let networkFee: AmountWithNative - let networkFeePayer: ExtrinsicFeePayer? - - var networkNativeFeeAddition: AmountWithNative? { - let targetAmount = totalFee.targetAmount > networkFee.targetAmount ? - totalFee.targetAmount - networkFee.targetAmount : 0 - - guard targetAmount > 0 else { - return nil - } - - let nativeAmount = totalFee.nativeAmount > networkFee.nativeAmount ? - totalFee.nativeAmount - networkFee.nativeAmount : 0 - - return .init(targetAmount: targetAmount, nativeAmount: nativeAmount) - } - - var extrinsicFee: ExtrinsicFeeProtocol { - ExtrinsicFee(amount: networkFee.targetAmount, payer: networkFeePayer, weight: 0) - } - } - - typealias FeeResult = Result -} - -typealias AssetConversionFeeServiceClosure = (AssetConversion.FeeResult) -> Void - -protocol AssetConversionFeeServiceProtocol { - func calculate( - in asset: ChainAsset, - callArgs: AssetConversion.CallArgs, - runCompletionIn queue: DispatchQueue, - completion closure: @escaping AssetConversionFeeServiceClosure - ) -} - -enum AssetConversionFeeServiceError: Error { - case accountMissing - case chainRuntimeMissing - case chainConnectionMissing - case utilityAssetMissing - case feeAssetConversionFailed - case setupFailed(String) - case calculationFailed(String) -} diff --git a/novawallet/Modules/AssetConversion/Service/AssetConversionFlowState.swift b/novawallet/Modules/AssetConversion/Service/AssetConversionFlowState.swift deleted file mode 100644 index 9e85513a43..0000000000 --- a/novawallet/Modules/AssetConversion/Service/AssetConversionFlowState.swift +++ /dev/null @@ -1,170 +0,0 @@ -import Foundation -import Operation_iOS - -enum AssetConversionFlowState { - case assetHub(AssetHubFlowState) - case hydra(HydraFlowState) -} - -protocol AssetConversionFlowFacadeProtocol { - var generalSubscriptonFactory: GeneralStorageSubscriptionFactoryProtocol { get } - - func setup(for chain: ChainModel) throws -> AssetConversionFlowState - func getReQuoteService( - for assetIn: ChainAssetId, - assetOut: ChainAssetId - ) -> ObservableSyncServiceProtocol? - - func createFeeService(for chain: ChainModel) throws -> AssetConversionFeeServiceProtocol - func createExtrinsicService(for chain: ChainModel) throws -> AssetConversionExtrinsicServiceProtocol -} - -enum AssetConversionFlowFacadeError: Error { - case unsupportedChain(ChainModel.Id) -} - -final class AssetConversionFlowFacade { - let wallet: MetaAccountModel - let chainRegistry: ChainRegistryProtocol - let operationQueue: OperationQueue - let userStorageFacade: StorageFacadeProtocol - let substrateStorageFacade: StorageFacadeProtocol - let generalSubscriptonFactory: GeneralStorageSubscriptionFactoryProtocol - - var state: AssetConversionFlowState? - - init( - wallet: MetaAccountModel, - chainRegistry: ChainRegistryProtocol, - userStorageFacade: StorageFacadeProtocol, - substrateStorageFacade: StorageFacadeProtocol, - generalSubscriptonFactory: GeneralStorageSubscriptionFactoryProtocol, - operationQueue: OperationQueue - ) { - self.wallet = wallet - self.chainRegistry = chainRegistry - self.userStorageFacade = userStorageFacade - self.substrateStorageFacade = substrateStorageFacade - self.generalSubscriptonFactory = generalSubscriptonFactory - self.operationQueue = operationQueue - } - - func setupAssetHub(for chain: ChainModel) throws -> AssetConversionFlowState { - if - let currentState = state, - case let .assetHub(assetHub) = currentState, - assetHub.chain.chainId == chain.chainId { - return currentState - } - - guard let connection = chainRegistry.getConnection(for: chain.chainId) else { - throw ChainRegistryError.connectionUnavailable - } - - guard let runtimeProvider = chainRegistry.getRuntimeProvider(for: chain.chainId) else { - throw ChainRegistryError.runtimeMetadaUnavailable - } - - let assetHub = AssetHubFlowState( - wallet: wallet, - chain: chain, - connection: connection, - runtimeProvider: runtimeProvider, - userStorageFacade: userStorageFacade, - substrateStorageFacade: substrateStorageFacade, - operationQueue: operationQueue - ) - - let newState = AssetConversionFlowState.assetHub(assetHub) - state = newState - - return newState - } - - func setupHydra(for chain: ChainModel) throws -> AssetConversionFlowState { - if - let currentState = state, - case let .hydra(hydra) = currentState, - hydra.chain.chainId == chain.chainId { - return currentState - } - - guard let connection = chainRegistry.getConnection(for: chain.chainId) else { - throw ChainRegistryError.connectionUnavailable - } - - guard let runtimeProvider = chainRegistry.getRuntimeProvider(for: chain.chainId) else { - throw ChainRegistryError.runtimeMetadaUnavailable - } - - guard let account = wallet.fetch(for: chain.accountRequest()) else { - throw ChainAccountFetchingError.accountNotExists - } - - let flowStateStore = HydraFlowStateStore.getShared( - for: connection, - runtimeProvider: runtimeProvider, - userStorageFacade: userStorageFacade, - substrateStorageFacade: substrateStorageFacade - ) - - let hydraFlowState = try flowStateStore.setupFlowState( - account: account, - chain: chain, - queue: operationQueue - ) - - let newState = AssetConversionFlowState.hydra(hydraFlowState) - state = newState - - return newState - } -} - -extension AssetConversionFlowFacade: AssetConversionFlowFacadeProtocol { - func setup(for chain: ChainModel) throws -> AssetConversionFlowState { - if chain.hasSwapHub { - return try setupAssetHub(for: chain) - } else if chain.hasSwapHydra { - return try setupHydra(for: chain) - } else { - throw AssetConversionFlowFacadeError.unsupportedChain(chain.chainId) - } - } - - func getReQuoteService( - for assetIn: ChainAssetId, - assetOut: ChainAssetId - ) -> ObservableSyncServiceProtocol? { - switch state { - case let .assetHub(assetHub): - return assetHub.getReQuoteService() - case let .hydra(hydra): - return hydra.getReQuoteService(for: assetIn, assetOut: assetOut) - case .none: - return nil - } - } - - func createFeeService(for chain: ChainModel) throws -> AssetConversionFeeServiceProtocol { - let state = try setup(for: chain) - - switch state { - case let .assetHub(assetHub): - return try assetHub.createFeeService(using: chainRegistry) - case let .hydra(hydra): - return try hydra.createFeeService() - } - } - - func createExtrinsicService(for chain: ChainModel) throws -> AssetConversionExtrinsicServiceProtocol { - let state = try setup(for: chain) - - switch state { - case let .assetHub(assetHub): - return try assetHub.createExtrinsicService() - case let .hydra(hydra): - return try hydra.createExtrinsicService() - } - } -} diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetConversionAggregationFactory+AssetHub.swift b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetConversionAggregationFactory+AssetHub.swift deleted file mode 100644 index 35fe044ea7..0000000000 --- a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetConversionAggregationFactory+AssetHub.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Foundation -import Operation_iOS - -extension AssetConversionAggregationFactory { - func createAssetHubAllDirections( - for chain: ChainModel - ) -> CompoundOperationWrapper<[ChainAssetId: Set]> { - guard let connection = chainRegistry.getConnection(for: chain.chainId) else { - return .createWithError(ChainRegistryError.connectionUnavailable) - } - - guard let runtimeService = chainRegistry.getRuntimeProvider(for: chain.chainId) else { - return .createWithError(ChainRegistryError.runtimeMetadaUnavailable) - } - - return AssetHubSwapOperationFactory( - chain: chain, - runtimeService: runtimeService, - connection: connection, - operationQueue: operationQueue - ).availableDirections() - } - - func createAssetHubDirections(for chainAsset: ChainAsset) -> CompoundOperationWrapper> { - guard let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId) else { - return .createWithError(ChainRegistryError.connectionUnavailable) - } - - guard let runtimeService = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId) else { - return .createWithError(ChainRegistryError.runtimeMetadaUnavailable) - } - - return AssetHubSwapOperationFactory( - chain: chainAsset.chain, - runtimeService: runtimeService, - connection: connection, - operationQueue: operationQueue - ).availableDirectionsForAsset(chainAsset.chainAssetId) - } - - func createAssetHubQuote( - for state: AssetHubFlowState, - args: AssetConversion.QuoteArgs - ) -> CompoundOperationWrapper { - AssetHubSwapOperationFactory( - chain: state.chain, - runtimeService: state.runtimeProvider, - connection: state.connection, - operationQueue: operationQueue - ).quote(for: args) - } -} diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubCallPathFactory.swift b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubCallPathFactory.swift deleted file mode 100644 index 786d766d3d..0000000000 --- a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubCallPathFactory.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -struct AssetHubCallPathFactory: AssetConversionCallPathFactoryProtocol { - func createHistoryCallPath(for args: AssetConversion.CallArgs) -> CallCodingPath { - AssetConversionPallet.callPath(for: args.direction) - } -} diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubExtrinsicService.swift b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubExtrinsicService.swift deleted file mode 100644 index 79908ecd6a..0000000000 --- a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubExtrinsicService.swift +++ /dev/null @@ -1,127 +0,0 @@ -import Foundation -import SubstrateSdk -import Operation_iOS - -final class AssetHubExtrinsicService { - let account: ChainAccountResponse - let chain: ChainModel - let extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol - let runtimeProvider: RuntimeCodingServiceProtocol - let operationQueue: OperationQueue - let workQueue: DispatchQueue - - init( - account: ChainAccountResponse, - chain: ChainModel, - extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol, - runtimeProvider: RuntimeCodingServiceProtocol, - operationQueue: OperationQueue, - workQueue: DispatchQueue = .global() - ) { - self.account = account - self.chain = chain - self.extrinsicServiceFactory = extrinsicServiceFactory - self.runtimeProvider = runtimeProvider - self.operationQueue = operationQueue - self.workQueue = workQueue - } - - private func performSubmition( - remoteFeeAsset: AssetConversionPallet.AssetId?, - builderClosure: @escaping ExtrinsicBuilderClosure, - signer: SigningWrapperProtocol, - runCompletionIn queue: DispatchQueue, - completion closure: @escaping ExtrinsicSubmitClosure - ) { - let extrinsicFactory: ExtrinsicOperationFactoryProtocol - - if let remoteFeeAsset = remoteFeeAsset { - extrinsicFactory = extrinsicServiceFactory.createOperationFactory( - account: account, - chain: chain, - feeAssetConversionId: remoteFeeAsset - ) - } else { - extrinsicFactory = extrinsicServiceFactory.createOperationFactory( - account: account, - chain: chain - ) - } - - let wrapper = extrinsicFactory.submit(builderClosure, signer: signer) - - execute( - wrapper: wrapper, - inOperationQueue: operationQueue, - runningCallbackIn: queue, - callbackClosure: closure - ) - } -} - -extension AssetHubExtrinsicService: AssetConversionExtrinsicServiceProtocol { - func submit( - callArgs: AssetConversion.CallArgs, - feeAsset: ChainAsset, - signer: SigningWrapperProtocol, - runCompletionIn queue: DispatchQueue, - completion closure: @escaping ExtrinsicSubmitClosure - ) { - let coderFactoryOperation = runtimeProvider.fetchCoderFactoryOperation() - - let mappingOperation = ClosureOperation<(ExtrinsicBuilderClosure, AssetConversionPallet.AssetId?)> { - let codingFactory = try coderFactoryOperation.extractNoCancellableResultData() - - let builderClosure: ExtrinsicBuilderClosure = { builder in - try AssetHubExtrinsicConverter.addingOperation( - to: builder, - chain: feeAsset.chain, - args: callArgs, - codingFactory: codingFactory - ) - } - - guard !feeAsset.isUtilityAsset else { - return (builderClosure, nil) - } - - guard - let assetId = AssetHubTokensConverter.convertToMultilocation( - chainAsset: feeAsset, - codingFactory: codingFactory - ) else { - throw AssetConversionExtrinsicServiceError.remoteAssetNotFound(feeAsset.chainAssetId) - } - - return (builderClosure, assetId) - } - - mappingOperation.addDependency(coderFactoryOperation) - - let wrapper = CompoundOperationWrapper( - targetOperation: mappingOperation, - dependencies: [coderFactoryOperation] - ) - - execute( - wrapper: wrapper, - inOperationQueue: operationQueue, - runningCallbackIn: workQueue - ) { [weak self] result in - switch result { - case let .success((builder, remoteFeeAsset)): - self?.performSubmition( - remoteFeeAsset: remoteFeeAsset, - builderClosure: builder, - signer: signer, - runCompletionIn: queue, - completion: closure - ) - case let .failure(error): - dispatchInQueueWhenPossible(queue) { - closure(.failure(error)) - } - } - } - } -} diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubFeeService.swift b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubFeeService.swift deleted file mode 100644 index 46e8f82943..0000000000 --- a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubFeeService.swift +++ /dev/null @@ -1,322 +0,0 @@ -import Foundation -import Operation_iOS -import BigInt - -final class AssetHubFeeService: AnyCancellableCleaning { - let wallet: MetaAccountModel - let extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol - let conversionOperationFactory: AssetHubSwapOperationFactoryProtocol - let chainRegistry: ChainRegistryProtocol - let operationQueue: OperationQueue - - private var cancellableCall: CancellableCall? - private let lock = NSLock() - - init( - wallet: MetaAccountModel, - extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol, - conversionOperationFactory: AssetHubSwapOperationFactoryProtocol, - chainRegistry: ChainRegistryProtocol, - operationQueue: OperationQueue - ) { - self.wallet = wallet - self.extrinsicServiceFactory = extrinsicServiceFactory - self.conversionOperationFactory = conversionOperationFactory - self.chainRegistry = chainRegistry - self.operationQueue = operationQueue - } - - private func performCalculation( - in asset: ChainAsset, - callArgs: AssetConversion.CallArgs, - runCompletionIn queue: DispatchQueue, - completion closure: @escaping AssetConversionFeeServiceClosure - ) throws { - guard let runtimeProvider = chainRegistry.getRuntimeProvider(for: asset.chain.chainId) else { - throw ChainRegistryError.runtimeMetadaUnavailable - } - - guard let utilityAsset = asset.chain.utilityAsset() else { - throw AssetConversionFeeServiceError.utilityAssetMissing - } - - let utilityChainAsset = ChainAsset(chain: asset.chain, asset: utilityAsset) - - let nativeFeeWrapper = createNativeFeeWrapper( - for: callArgs, - runtimeProvider: runtimeProvider, - extrinsicServiceFactory: extrinsicServiceFactory, - wallet: wallet, - asset: asset - ) - - let universalFeeWrapper: CompoundOperationWrapper - - if asset.isUtilityAsset { - universalFeeWrapper = createNativeTokenFeeCalculationWrapper(using: nativeFeeWrapper) - } else { - universalFeeWrapper = createCustomTokenFeeCalculationWrapper( - in: asset, - utilityAsset: utilityChainAsset, - nativeFeeWrapper: nativeFeeWrapper, - runtimeProvider: runtimeProvider, - conversionOperationFactory: conversionOperationFactory - ) - } - - universalFeeWrapper.targetOperation.completionBlock = { [weak self] in - dispatchInQueueWhenPossible(queue) { - guard let self = self, self.completeOrIgnore(wrapper: universalFeeWrapper) else { - return - } - - do { - let model = try universalFeeWrapper.targetOperation.extractNoCancellableResultData() - closure(.success(model)) - } catch let error as AssetConversionFeeServiceError { - closure(.failure(error)) - } catch { - closure(.failure(.calculationFailed("Fee calculation failed \(asset.chain.name): \(error)"))) - } - } - } - - cancellableCall = universalFeeWrapper - - operationQueue.addOperations(universalFeeWrapper.allOperations, waitUntilFinished: false) - } - - private func createNativeFeeWrapper( - for callArgs: AssetConversion.CallArgs, - runtimeProvider: RuntimeProviderProtocol, - extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol, - wallet: MetaAccountModel, - asset: ChainAsset - ) -> CompoundOperationWrapper { - let coderFactoryOperation = runtimeProvider.fetchCoderFactoryOperation() - - let mainFeeOperation = OperationCombiningService( - operationManager: OperationManager(operationQueue: operationQueue) - ) { - guard let account = wallet.fetch(for: asset.chain.accountRequest()) else { - throw AssetConversionFeeServiceError.accountMissing - } - - let coderFactory = try coderFactoryOperation.extractNoCancellableResultData() - - let extrinsicOperationFactory: ExtrinsicOperationFactoryProtocol - - if asset.isUtilityAsset { - extrinsicOperationFactory = extrinsicServiceFactory.createOperationFactory( - account: account, - chain: asset.chain - ) - } else { - guard let assetId = AssetHubTokensConverter.convertToMultilocation( - chainAsset: asset, - codingFactory: coderFactory - ) else { - throw AssetConversionFeeServiceError.feeAssetConversionFailed - } - - extrinsicOperationFactory = extrinsicServiceFactory.createOperationFactory( - account: account, - chain: asset.chain, - feeAssetConversionId: assetId - ) - } - - let feeWrapper = extrinsicOperationFactory.estimateFeeOperation { builder in - let codingFactory = try coderFactoryOperation.extractNoCancellableResultData() - - return try AssetHubExtrinsicConverter.addingOperation( - to: builder, - chain: asset.chain, - args: callArgs, - codingFactory: codingFactory - ) - } - - return [feeWrapper] - }.longrunOperation() - - let mappingOperation = ClosureOperation { - guard let feeModel = try mainFeeOperation.extractNoCancellableResultData().first else { - throw CommonError.dataCorruption - } - - return feeModel - } - - mainFeeOperation.addDependency(coderFactoryOperation) - mappingOperation.addDependency(mainFeeOperation) - - return .init( - targetOperation: mappingOperation, - dependencies: [coderFactoryOperation, mainFeeOperation] - ) - } - - private func createNativeTokenFeeCalculationWrapper( - using nativeFeeWrapper: CompoundOperationWrapper - ) -> CompoundOperationWrapper { - let resultOperation = ClosureOperation { - let fee = try nativeFeeWrapper.targetOperation.extractNoCancellableResultData() - - let model = AssetConversion.AmountWithNative(targetAmount: fee.amount, nativeAmount: fee.amount) - - return .init(totalFee: model, networkFee: model, networkFeePayer: fee.payer) - } - - resultOperation.addDependency(nativeFeeWrapper.targetOperation) - - return nativeFeeWrapper.insertingTail(operation: resultOperation) - } - - private func createCustomTokenFeeCalculationWrapper( - in feeAsset: ChainAsset, - utilityAsset: ChainAsset, - nativeFeeWrapper: CompoundOperationWrapper, - runtimeProvider: RuntimeProviderProtocol, - conversionOperationFactory: AssetHubSwapOperationFactoryProtocol - ) -> CompoundOperationWrapper { - let edWrapper = AssetStorageInfoOperationFactory( - chainRegistry: chainRegistry, - operationQueue: operationQueue - ).createAssetBalanceExistenceOperation( - chainId: utilityAsset.chain.chainId, - asset: utilityAsset.asset, - runtimeProvider: runtimeProvider, - operationQueue: operationQueue - ) - - let feeWithEdOperation = ClosureOperation<(BigUInt, BigUInt)> { - let feeAmount = try nativeFeeWrapper.targetOperation.extractNoCancellableResultData().amount - let edAmount = try edWrapper.targetOperation.extractNoCancellableResultData().minBalance - - return (feeAmount, edAmount) - } - - feeWithEdOperation.addDependency(nativeFeeWrapper.targetOperation) - feeWithEdOperation.addDependency(edWrapper.targetOperation) - - let quoteOperation = createQuoteForCustomTokenWrapper( - for: feeAsset, - utilityAsset: utilityAsset, - conversionOperationFactory: conversionOperationFactory, - feeWithEdOperation: feeWithEdOperation - ) - - quoteOperation.addDependency(feeWithEdOperation) - - let mergeOperation = ClosureOperation { - let (feeAmount, edAmount) = try feeWithEdOperation.extractNoCancellableResultData() - let networkFeePayer = try nativeFeeWrapper.targetOperation.extractNoCancellableResultData().payer - - let quotes = try quoteOperation.extractNoCancellableResultData() - - return .init( - totalFee: .init( - targetAmount: quotes[0].amountIn, - nativeAmount: feeAmount + edAmount - ), - networkFee: .init( - targetAmount: quotes[1].amountIn, - nativeAmount: feeAmount - ), - networkFeePayer: networkFeePayer - ) - } - - mergeOperation.addDependency(feeWithEdOperation) - mergeOperation.addDependency(quoteOperation) - - let dependencies = nativeFeeWrapper.allOperations + edWrapper.allOperations + - [feeWithEdOperation, quoteOperation] - - return .init(targetOperation: mergeOperation, dependencies: dependencies) - } - - private func createQuoteForCustomTokenWrapper( - for feeAsset: ChainAsset, - utilityAsset: ChainAsset, - conversionOperationFactory: AssetHubSwapOperationFactoryProtocol, - feeWithEdOperation: BaseOperation<(BigUInt, BigUInt)> - ) -> BaseOperation<[AssetConversion.Quote]> { - OperationCombiningService( - operationManager: OperationManager(operationQueue: operationQueue) - ) { - let (fee, edAmount) = try feeWithEdOperation.extractNoCancellableResultData() - - let feeWithAdditionsQuoteWrapper = conversionOperationFactory.quote( - for: .init( - assetIn: feeAsset.chainAssetId, - assetOut: utilityAsset.chainAssetId, - amount: fee + edAmount, - direction: .buy - ) - ) - - let feeQuoteWrapper = conversionOperationFactory.quote( - for: .init( - assetIn: feeAsset.chainAssetId, - assetOut: utilityAsset.chainAssetId, - amount: fee, - direction: .buy - ) - ) - - return [feeWithAdditionsQuoteWrapper, feeQuoteWrapper] - }.longrunOperation() - } - - private func completeOrIgnore(wrapper: CompoundOperationWrapper) -> Bool { - lock.lock() - - defer { - lock.unlock() - } - - guard cancellableCall === wrapper else { - return false - } - - cancellableCall = nil - - return true - } -} - -extension AssetHubFeeService: AssetConversionFeeServiceProtocol { - func calculate( - in asset: ChainAsset, - callArgs: AssetConversion.CallArgs, - runCompletionIn queue: DispatchQueue, - completion closure: @escaping AssetConversionFeeServiceClosure - ) { - do { - lock.lock() - - defer { - lock.unlock() - } - - clear(cancellable: &cancellableCall) - - try performCalculation( - in: asset, - callArgs: callArgs, - runCompletionIn: queue, - completion: closure - ) - } catch let error as AssetConversionFeeServiceError { - dispatchInQueueWhenPossible(queue) { - closure(.failure(error)) - } - } catch { - dispatchInQueueWhenPossible(queue) { - closure(.failure(.setupFailed("Fee service setup failed for \(asset.chain.name): \(error)"))) - } - } - } -} diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubFlowState.swift b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubFlowState.swift deleted file mode 100644 index 76c8f4a198..0000000000 --- a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubFlowState.swift +++ /dev/null @@ -1,126 +0,0 @@ -import Foundation -import SubstrateSdk -import Operation_iOS - -protocol AssetHubFlowStateProtocol { - func setupReQuoteService() -> AssetHubReQuoteService - - func getReQuoteService() -> ObservableSyncServiceProtocol? - - func createFeeService(using chainRegistry: ChainRegistryProtocol) throws -> AssetConversionFeeServiceProtocol - func createExtrinsicService() throws -> AssetConversionExtrinsicServiceProtocol -} - -final class AssetHubFlowState { - let wallet: MetaAccountModel - let chain: ChainModel - let connection: JSONRPCEngine - let runtimeProvider: RuntimeProviderProtocol - let userStorageFacade: StorageFacadeProtocol - let substrateStorageFacade: StorageFacadeProtocol - let operationQueue: OperationQueue - - let mutex = NSLock() - - private var reQuoteService: AssetHubReQuoteService? - - init( - wallet: MetaAccountModel, - chain: ChainModel, - connection: JSONRPCEngine, - runtimeProvider: RuntimeProviderProtocol, - userStorageFacade: StorageFacadeProtocol, - substrateStorageFacade: StorageFacadeProtocol, - operationQueue: OperationQueue - ) { - self.wallet = wallet - self.chain = chain - self.connection = connection - self.runtimeProvider = runtimeProvider - self.userStorageFacade = userStorageFacade - self.substrateStorageFacade = substrateStorageFacade - self.operationQueue = operationQueue - } -} - -extension AssetHubFlowState: AssetHubFlowStateProtocol { - func setupReQuoteService() -> AssetHubReQuoteService { - mutex.lock() - - defer { - mutex.unlock() - } - - if let reQuoteService = reQuoteService { - return reQuoteService - } - - let service = AssetHubReQuoteService( - connection: connection, - runtimeProvider: runtimeProvider, - operationQueue: operationQueue - ) - - reQuoteService = service - service.setup() - - return service - } - - func getReQuoteService() -> ObservableSyncServiceProtocol? { - mutex.lock() - - defer { - mutex.unlock() - } - - return reQuoteService - } - - func createFeeService(using chainRegistry: ChainRegistryProtocol) throws -> AssetConversionFeeServiceProtocol { - let extrinsicServiceFactory = ExtrinsicServiceFactory( - runtimeRegistry: runtimeProvider, - engine: connection, - operationQueue: operationQueue, - userStorageFacade: userStorageFacade, - substrateStorageFacade: substrateStorageFacade - ) - - let conversionOperationFactory = AssetHubSwapOperationFactory( - chain: chain, - runtimeService: runtimeProvider, - connection: connection, - operationQueue: operationQueue - ) - - return AssetHubFeeService( - wallet: wallet, - extrinsicServiceFactory: extrinsicServiceFactory, - conversionOperationFactory: conversionOperationFactory, - chainRegistry: chainRegistry, - operationQueue: operationQueue - ) - } - - func createExtrinsicService() throws -> AssetConversionExtrinsicServiceProtocol { - guard let account = wallet.fetch(for: chain.accountRequest()) else { - throw ChainAccountFetchingError.accountNotExists - } - - let extrinsicServiceFactory = ExtrinsicServiceFactory( - runtimeRegistry: runtimeProvider, - engine: connection, - operationQueue: operationQueue, - userStorageFacade: userStorageFacade, - substrateStorageFacade: substrateStorageFacade - ) - - return AssetHubExtrinsicService( - account: account, - chain: chain, - extrinsicServiceFactory: extrinsicServiceFactory, - runtimeProvider: runtimeProvider, - operationQueue: operationQueue - ) - } -} diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/AssetConversionAggregationFactory+HydraDx.swift b/novawallet/Modules/AssetConversion/Service/HydraDx/AssetConversionAggregationFactory+HydraDx.swift deleted file mode 100644 index 6084de700a..0000000000 --- a/novawallet/Modules/AssetConversion/Service/HydraDx/AssetConversionAggregationFactory+HydraDx.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Foundation -import Operation_iOS - -extension AssetConversionAggregationFactory { - func createHydraAllDirections( - for chain: ChainModel - ) -> CompoundOperationWrapper<[ChainAssetId: Set]> { - guard let connection = chainRegistry.getConnection(for: chain.chainId) else { - return .createWithError(ChainRegistryError.connectionUnavailable) - } - - guard let runtimeService = chainRegistry.getRuntimeProvider(for: chain.chainId) else { - return .createWithError(ChainRegistryError.runtimeMetadaUnavailable) - } - - return HydraTokensFactory.createWithDefaultPools( - chain: chain, - runtimeService: runtimeService, - connection: connection, - operationQueue: operationQueue - ).availableDirections() - } - - func createHydraDirections(for chainAsset: ChainAsset) -> CompoundOperationWrapper> { - guard let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId) else { - return .createWithError(ChainRegistryError.connectionUnavailable) - } - - guard let runtimeService = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId) else { - return .createWithError(ChainRegistryError.runtimeMetadaUnavailable) - } - - return HydraTokensFactory.createWithDefaultPools( - chain: chainAsset.chain, - runtimeService: runtimeService, - connection: connection, - operationQueue: operationQueue - ).availableDirectionsForAsset(chainAsset.chainAssetId) - } - - func createHydraQuote( - for state: HydraFlowState, - args: AssetConversion.QuoteArgs - ) -> CompoundOperationWrapper { - HydraQuoteFactory(flowState: state).quote(for: args) - } -} diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraCallPathFactory.swift b/novawallet/Modules/AssetConversion/Service/HydraDx/HydraCallPathFactory.swift deleted file mode 100644 index 48ff19e6e7..0000000000 --- a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraCallPathFactory.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation - -struct HydraCallPathFactory: AssetConversionCallPathFactoryProtocol { - func createHistoryCallPath(for args: AssetConversion.CallArgs) -> CallCodingPath { - // TODO: Check the calls when implement realtime history - switch args.direction { - case .sell: - return HydraOmnipool.SellCall.callPath - case .buy: - return HydraOmnipool.BuyCall.callPath - } - } -} diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraFeeService.swift b/novawallet/Modules/AssetConversion/Service/HydraDx/HydraFeeService.swift deleted file mode 100644 index 267e33b65b..0000000000 --- a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraFeeService.swift +++ /dev/null @@ -1,223 +0,0 @@ -import Foundation -import SubstrateSdk -import Operation_iOS - -final class HydraFeeService { - let extrinsicFactory: ExtrinsicOperationFactoryProtocol - let conversionOperationFactory: HydraQuoteFactory - let conversionExtrinsicFactory: HydraExtrinsicOperationFactoryProtocol - let workQueue: DispatchQueue - let operationQueue: OperationQueue - - private var feeCall = CancellableCallStore() - private let mutex = NSLock() - - init( - extrinsicFactory: ExtrinsicOperationFactoryProtocol, - conversionOperationFactory: HydraQuoteFactory, - conversionExtrinsicFactory: HydraExtrinsicOperationFactoryProtocol, - operationQueue: OperationQueue, - workQueue: DispatchQueue = .global() - ) { - self.extrinsicFactory = extrinsicFactory - self.conversionOperationFactory = conversionOperationFactory - self.conversionExtrinsicFactory = conversionExtrinsicFactory - self.operationQueue = operationQueue - self.workQueue = workQueue - } - - deinit { - feeCall.cancel() - } - - private func createNativeFeeWrapper( - paramsOperation: BaseOperation - ) -> CompoundOperationWrapper { - OperationCombiningService.compoundNonOptionalWrapper( - operationManager: OperationManager(operationQueue: operationQueue) - ) { [weak self] in - guard let self else { - throw BaseOperationError.parentOperationCancelled - } - - let swap = try paramsOperation.extractNoCancellableResultData() - - let builderClosure: ExtrinsicBuilderClosure = { - try HydraExtrinsicConverter.addingOperation( - from: swap, - builder: $0 - ) - } - - return extrinsicFactory.estimateFeeOperation( - builderClosure, - payingFeeIn: nil - ) - } - } - - private func createNonNativeFeeWrapper( - for nativeFee: ExtrinsicFeeProtocol, - feeAsset: ChainAsset - ) -> CompoundOperationWrapper { - guard let utilityAssetId = feeAsset.chain.utilityChainAssetId() else { - return CompoundOperationWrapper.createWithError( - AssetConversionFeeServiceError.feeAssetConversionFailed - ) - } - - let quoteWrapper = conversionOperationFactory.quote( - for: .init( - assetIn: feeAsset.chainAssetId, - assetOut: utilityAssetId, - amount: nativeFee.amount, - direction: .buy - ) - ) - - /** - * We are adding ed to final fee amount as currently runtime has a bug that leads to extrinsic fail - * when fee paid in non native asset and balance goes below ed. - */ - let balanceExistenceWrapper = AssetStorageInfoOperationFactory().createAssetBalanceExistenceOperation( - chainId: feeAsset.chain.chainId, - asset: feeAsset.asset, - runtimeProvider: conversionOperationFactory.flowState.runtimeProvider, - operationQueue: conversionOperationFactory.flowState.operationQueue - ) - - let mappingOperation = ClosureOperation { - let quote = try quoteWrapper.targetOperation.extractNoCancellableResultData() - let balanceExistence = try balanceExistenceWrapper.targetOperation.extractNoCancellableResultData() - - let model = AssetConversion.AmountWithNative( - targetAmount: quote.amountIn + balanceExistence.minBalance, - nativeAmount: quote.amountOut - ) - - return .init(totalFee: model, networkFee: model, networkFeePayer: nativeFee.payer) - } - - mappingOperation.addDependency(balanceExistenceWrapper.targetOperation) - mappingOperation.addDependency(quoteWrapper.targetOperation) - - return quoteWrapper - .insertingHead(operations: balanceExistenceWrapper.allOperations) - .insertingTail(operation: mappingOperation) - } - - private func createConversionWrapper( - nativeFeeOperation: BaseOperation, - feeAsset: ChainAsset - ) -> CompoundOperationWrapper { - OperationCombiningService.compoundNonOptionalWrapper( - operationManager: OperationManager(operationQueue: operationQueue) - ) { - let feeResult = try nativeFeeOperation.extractNoCancellableResultData() - - let optTotalFee: ExtrinsicFeeProtocol? = try feeResult.results.reduce(nil) { accum, feeResult in - let fee = try feeResult.result.get() - - if let currentFee = accum { - return ExtrinsicFee( - amount: currentFee.amount + fee.amount, - payer: fee.payer, - weight: currentFee.weight + fee.weight - ) - } else { - return ExtrinsicFee( - amount: fee.amount, - payer: fee.payer, - weight: fee.weight - ) - } - } - - guard let totalFee = optTotalFee else { - return CompoundOperationWrapper.createWithError( - AssetConversionFeeServiceError.calculationFailed("Missing fee") - ) - } - - guard !feeAsset.isUtilityAsset else { - let model = AssetConversion.AmountWithNative( - targetAmount: totalFee.amount, - nativeAmount: totalFee.amount - ) - - let convertedFee = AssetConversion.FeeModel( - totalFee: model, - networkFee: model, - networkFeePayer: totalFee.payer - ) - - return CompoundOperationWrapper.createWithResult(convertedFee) - } - - return self.createNonNativeFeeWrapper(for: totalFee, feeAsset: feeAsset) - } - } -} - -extension HydraFeeService: AssetConversionFeeServiceProtocol { - func calculate( - in asset: ChainAsset, - callArgs: AssetConversion.CallArgs, - runCompletionIn queue: DispatchQueue, - completion closure: @escaping AssetConversionFeeServiceClosure - ) { - mutex.lock() - - defer { - mutex.unlock() - } - - feeCall.cancel() - - let paramsWrapper = conversionExtrinsicFactory.createOperationWrapper( - for: asset, - callArgs: callArgs - ) - - let nativeFeeWrapper = createNativeFeeWrapper( - paramsOperation: paramsWrapper.targetOperation - ) - - nativeFeeWrapper.addDependency(wrapper: paramsWrapper) - - let conversionWrapper = createConversionWrapper( - nativeFeeOperation: nativeFeeWrapper.targetOperation, - feeAsset: asset - ) - - conversionWrapper.addDependency(wrapper: nativeFeeWrapper) - conversionWrapper.addDependency(wrapper: paramsWrapper) - - let dependencies = paramsWrapper.allOperations + nativeFeeWrapper.allOperations + - conversionWrapper.dependencies - - let finalWrapper = CompoundOperationWrapper( - targetOperation: conversionWrapper.targetOperation, - dependencies: dependencies - ) - - executeCancellable( - wrapper: finalWrapper, - inOperationQueue: operationQueue, - backingCallIn: feeCall, - runningCallbackIn: workQueue, - mutex: mutex - ) { result in - dispatchInQueueWhenPossible(queue) { - do { - let model = try result.get() - closure(.success(model)) - } catch let error as AssetConversionFeeServiceError { - closure(.failure(error)) - } catch { - closure(.failure(.calculationFailed("Fee calculation error: \(error)"))) - } - } - } - } -} diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraFlowState.swift b/novawallet/Modules/AssetConversion/Service/HydraDx/HydraFlowState.swift deleted file mode 100644 index 2e8d53ad7a..0000000000 --- a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraFlowState.swift +++ /dev/null @@ -1,285 +0,0 @@ -import Foundation -import SubstrateSdk -import Operation_iOS - -protocol HydraFlowStateProtocol { - func getReQuoteService( - for assetIn: ChainAssetId, - assetOut: ChainAssetId - ) -> ObservableSyncServiceProtocol? - - func createFeeService() throws -> AssetConversionFeeServiceProtocol - func createExtrinsicService() throws -> AssetConversionExtrinsicServiceProtocol -} - -final class HydraFlowState { - let account: ChainAccountResponse - let chain: ChainModel - let connection: JSONRPCEngine - let runtimeProvider: RuntimeProviderProtocol - let userStorageFacade: StorageFacadeProtocol - let substrateStorageFacade: StorageFacadeProtocol - let operationQueue: OperationQueue - - let mutex = NSLock() - - private var omnipoolFlowState: HydraOmnipoolFlowState? - private var stableswapFlowState: HydraStableswapFlowState? - private var xykswapFlowState: HydraXYKFlowState? - private var reQuoteService: HydraReQuoteService? - private var swapStateService: HydraSwapParamsService? - private var routesFactory: HydraRoutesOperationFactoryProtocol? - - private var currentSwapPair: HydraDx.LocalSwapPair? - - init( - account: ChainAccountResponse, - chain: ChainModel, - connection: JSONRPCEngine, - runtimeProvider: RuntimeProviderProtocol, - userStorageFacade: StorageFacadeProtocol, - substrateStorageFacade: StorageFacadeProtocol, - operationQueue: OperationQueue - ) { - self.account = account - self.chain = chain - self.connection = connection - self.runtimeProvider = runtimeProvider - self.userStorageFacade = userStorageFacade - self.substrateStorageFacade = substrateStorageFacade - self.operationQueue = operationQueue - } - - deinit { - reQuoteService?.throttle() - } -} - -extension HydraFlowState { - func resetServicesIfNotMatchingPair(_ swapPair: HydraDx.LocalSwapPair) { - mutex.lock() - - defer { - mutex.unlock() - } - - guard swapPair != currentSwapPair else { - return - } - - omnipoolFlowState?.resetServices() - stableswapFlowState?.resetServices() - xykswapFlowState?.resetServices() - - reQuoteService?.throttle() - reQuoteService = nil - - routesFactory = nil - - currentSwapPair = swapPair - } - - func getOmnipoolFlowState() -> HydraOmnipoolFlowState { - mutex.lock() - - defer { - mutex.unlock() - } - - if let state = omnipoolFlowState { - return state - } - - let newState = HydraOmnipoolFlowState( - account: account, - chain: chain, - connection: connection, - runtimeProvider: runtimeProvider, - operationQueue: operationQueue - ) - - omnipoolFlowState = newState - - return newState - } - - func getStableswapFlowState() -> HydraStableswapFlowState { - mutex.lock() - - defer { - mutex.unlock() - } - - if let state = stableswapFlowState { - return state - } - - let newState = HydraStableswapFlowState( - account: account, - chain: chain, - connection: connection, - runtimeProvider: runtimeProvider, - operationQueue: operationQueue - ) - - stableswapFlowState = newState - - return newState - } - - func getXYKSwapFlowState() -> HydraXYKFlowState { - mutex.lock() - - defer { - mutex.unlock() - } - - if let state = xykswapFlowState { - return state - } - - let newState = HydraXYKFlowState( - account: account, - chain: chain, - connection: connection, - runtimeProvider: runtimeProvider, - operationQueue: operationQueue - ) - - xykswapFlowState = newState - - return newState - } - - func getRoutesFactory() -> HydraRoutesOperationFactoryProtocol { - mutex.lock() - - defer { - mutex.unlock() - } - - if let factory = routesFactory { - return factory - } - - let factory = HydraRoutesOperationFactory( - chain: chain, - connection: connection, - runtimeProvider: runtimeProvider, - operationQueue: operationQueue - ) - - routesFactory = factory - - return factory - } - - func getReQuoteService( - for assetIn: ChainAssetId, - assetOut: ChainAssetId - ) -> ObservableSyncServiceProtocol? { - mutex.lock() - - defer { - mutex.unlock() - } - - let swapPair = HydraDx.LocalSwapPair(assetIn: assetIn, assetOut: assetOut) - - guard currentSwapPair == swapPair else { - return nil - } - - if let reQuoteService = reQuoteService { - return reQuoteService - } - - let services: [ObservableSyncServiceProtocol] = (omnipoolFlowState?.getAllStateServices() ?? []) + - (stableswapFlowState?.getAllStateServices() ?? []) + - (xykswapFlowState?.getAllStateServices() ?? []) - - let reQuoteService = HydraReQuoteService(childServices: services) - self.reQuoteService = reQuoteService - - reQuoteService.setup() - - return reQuoteService - } - - func setupSwapService() -> HydraSwapParamsService { - mutex.lock() - - defer { - mutex.unlock() - } - - if let swapStateService = swapStateService { - return swapStateService - } - - let service = HydraSwapParamsService( - accountId: account.accountId, - connection: connection, - runtimeProvider: runtimeProvider, - operationQueue: operationQueue - ) - - swapStateService = service - service.setup() - - return service - } - - func createFeeService() throws -> AssetConversionFeeServiceProtocol { - let extrinsicFactory = ExtrinsicServiceFactory( - runtimeRegistry: runtimeProvider, - engine: connection, - operationQueue: operationQueue, - userStorageFacade: userStorageFacade, - substrateStorageFacade: substrateStorageFacade - ).createOperationFactory( - account: account, - chain: chain - ) - - let conversionOperationFactory = HydraQuoteFactory(flowState: self) - - let swapOperationFactory = HydraExtrinsicOperationFactory( - chain: chain, - swapService: setupSwapService(), - runtimeProvider: runtimeProvider - ) - - return HydraFeeService( - extrinsicFactory: extrinsicFactory, - conversionOperationFactory: conversionOperationFactory, - conversionExtrinsicFactory: swapOperationFactory, - operationQueue: operationQueue - ) - } - - func createExtrinsicService() throws -> AssetConversionExtrinsicServiceProtocol { - let extrinsicService = ExtrinsicServiceFactory( - runtimeRegistry: runtimeProvider, - engine: connection, - operationQueue: operationQueue, - userStorageFacade: userStorageFacade, - substrateStorageFacade: substrateStorageFacade - ).createService( - account: account, - chain: chain - ) - - let operationFactory = HydraExtrinsicOperationFactory( - chain: chain, - swapService: setupSwapService(), - runtimeProvider: runtimeProvider - ) - - return HydraSwapExtrinsicService( - extrinsicService: extrinsicService, - conversionExtrinsicFactory: operationFactory, - operationQueue: operationQueue - ) - } -} diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraFlowStateStore.swift b/novawallet/Modules/AssetConversion/Service/HydraDx/HydraFlowStateStore.swift deleted file mode 100644 index 184f085055..0000000000 --- a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraFlowStateStore.swift +++ /dev/null @@ -1,137 +0,0 @@ -import Foundation -import SubstrateSdk - -private struct StateKey: Hashable { - let chainId: ChainModel.Id - let accountId: AccountId -} - -protocol HydraFlowStateStoreSubscriber: AnyObject { - func flowStateStoreDidUpdate(_ newStates: [HydraFlowState]) -} - -class HydraFlowStateStore { - private static var shared: HydraFlowStateStore? - - private var states: [StateKey: WeakWrapper] = [:] - private var statesUpdatesSubscriptions: [WeakWrapper] = [] - private let mutex = NSLock() - private let connection: JSONRPCEngine - private let runtimeProvider: RuntimeProviderProtocol - private let userStorageFacade: StorageFacadeProtocol - private let substrateStorageFacade: StorageFacadeProtocol - - init( - connection: JSONRPCEngine, - runtimeProvider: RuntimeProviderProtocol, - userStorageFacade: StorageFacadeProtocol, - substrateStorageFacade: StorageFacadeProtocol - ) { - self.connection = connection - self.runtimeProvider = runtimeProvider - self.userStorageFacade = userStorageFacade - self.substrateStorageFacade = substrateStorageFacade - } -} - -private extension HydraFlowStateStore { - func setupNewFlowState( - account: ChainAccountResponse, - chain: ChainModel, - stateKey: StateKey, - queue: OperationQueue - ) throws -> HydraFlowState { - let flowState = HydraFlowState( - account: account, - chain: chain, - connection: connection, - runtimeProvider: runtimeProvider, - userStorageFacade: userStorageFacade, - substrateStorageFacade: substrateStorageFacade, - operationQueue: queue - ) - - states[stateKey] = WeakWrapper(target: flowState) - - statesUpdatesSubscriptions.forEach { sub in - let mappedStates = states - .values - .compactMap { $0.target as? HydraFlowState } - - (sub.target as? HydraFlowStateStoreSubscriber)?.flowStateStoreDidUpdate(mappedStates) - } - - return flowState - } - - func clearEmptySubscribers() { - statesUpdatesSubscriptions.removeAll { $0.target == nil } - } -} - -extension HydraFlowStateStore { - func setupFlowState( - account: ChainAccountResponse, - chain: ChainModel, - queue: OperationQueue - ) throws -> HydraFlowState { - mutex.lock() - - defer { - mutex.unlock() - } - - clearEmptySubscribers() - - let stateKey = StateKey( - chainId: chain.chainId, - accountId: account.accountId - ) - - let existingState = states[stateKey]?.target as? HydraFlowState - - return if let existingState { - existingState - } else { - try setupNewFlowState( - account: account, - chain: chain, - stateKey: stateKey, - queue: queue - ) - } - } - - func subscribeForChangesUpdates(_ subscriber: HydraFlowStateStoreSubscriber) { - mutex.lock() - - clearEmptySubscribers() - - let weak = WeakWrapper(target: subscriber) - statesUpdatesSubscriptions.append(weak) - - mutex.unlock() - } - - static func getShared( - for connection: JSONRPCEngine, - runtimeProvider: RuntimeProviderProtocol, - userStorageFacade: StorageFacadeProtocol, - substrateStorageFacade: StorageFacadeProtocol - ) -> HydraFlowStateStore { - if let shared { - return shared - } - - let store = HydraFlowStateStore( - connection: connection, - runtimeProvider: runtimeProvider, - userStorageFacade: userStorageFacade, - substrateStorageFacade: substrateStorageFacade - ) - - shared = store - - return store - } -} diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraReQuoteService.swift b/novawallet/Modules/AssetConversion/Service/HydraDx/HydraReQuoteService.swift deleted file mode 100644 index 715224427d..0000000000 --- a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraReQuoteService.swift +++ /dev/null @@ -1,54 +0,0 @@ -import Foundation -import SubstrateSdk - -final class HydraReQuoteService: ObservableSyncService { - let childServices: [ObservableSyncServiceProtocol] - let workQueue: DispatchQueue - - init( - childServices: [ObservableSyncServiceProtocol], - workQueue: DispatchQueue = .global(), - retryStrategy: ReconnectionStrategyProtocol = ExponentialReconnection(), - logger: LoggerProtocol = Logger.shared - ) { - self.childServices = childServices - self.workQueue = workQueue - - super.init(retryStrategy: retryStrategy, logger: logger) - } - - private func updateSyncState() { - let isChildSyncing = childServices.contains { $0.getIsSyncing() } - - if isSyncing != isChildSyncing { - isSyncing = isChildSyncing - } - } - - override func stopSyncUp() { - childServices.forEach { $0.unsubscribeSyncState(self) } - } - - override func performSyncUp() { - childServices.forEach { child in - if child.hasSubscription(for: self) { - return - } - - child.subscribeSyncState( - self, - queue: workQueue - ) { [weak self] oldState, _ in - self?.mutex.lock() - - self?.isSyncing = oldState - - self?.updateSyncState() - - self?.mutex.unlock() - } - } - - completeImmediate(nil) - } -} diff --git a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraSwapExtrinsicService.swift b/novawallet/Modules/AssetConversion/Service/HydraDx/HydraSwapExtrinsicService.swift deleted file mode 100644 index f452385475..0000000000 --- a/novawallet/Modules/AssetConversion/Service/HydraDx/HydraSwapExtrinsicService.swift +++ /dev/null @@ -1,89 +0,0 @@ -import Foundation - -final class HydraSwapExtrinsicService { - let extrinsicService: ExtrinsicServiceProtocol - let conversionExtrinsicFactory: HydraExtrinsicOperationFactoryProtocol - let operationQueue: OperationQueue - let workQueue: DispatchQueue - let logger: LoggerProtocol - - init( - extrinsicService: ExtrinsicServiceProtocol, - conversionExtrinsicFactory: HydraExtrinsicOperationFactoryProtocol, - operationQueue: OperationQueue, - workQueue: DispatchQueue = .global(), - logger: LoggerProtocol = Logger.shared - ) { - self.extrinsicService = extrinsicService - self.conversionExtrinsicFactory = conversionExtrinsicFactory - self.operationQueue = operationQueue - self.workQueue = workQueue - self.logger = logger - } - - private func cancelSubscription(for subscriptionId: UInt16?) { - if let subscriptionId = subscriptionId { - extrinsicService.cancelExtrinsicWatch(for: subscriptionId) - } - } - - private func performSwapSubmission( - for swapParams: HydraSwapParams, - signer: SigningWrapperProtocol, - runCompletionIn queue: DispatchQueue, - completion closure: @escaping ExtrinsicSubmitClosure - ) { - let builderClosure: ExtrinsicBuilderClosure = { builder in - try HydraExtrinsicConverter.addingOperation( - from: swapParams, - builder: builder - ) - } - - extrinsicService.submit( - builderClosure, - payingIn: swapParams.params.newFeeCurrency, - signer: signer, - runningIn: queue, - completion: closure - ) - } -} - -extension HydraSwapExtrinsicService: AssetConversionExtrinsicServiceProtocol { - func submit( - callArgs: AssetConversion.CallArgs, - feeAsset: ChainAsset, - signer: SigningWrapperProtocol, - runCompletionIn queue: DispatchQueue, - completion closure: @escaping ExtrinsicSubmitClosure - ) { - let swapParamsWrapper = conversionExtrinsicFactory.createOperationWrapper( - for: feeAsset, - callArgs: callArgs - ) - - execute( - wrapper: swapParamsWrapper, - inOperationQueue: operationQueue, - runningCallbackIn: workQueue, - callbackClosure: { [weak self] result in - self?.logger.debug("Extrinsic params: \(result)") - - switch result { - case let .success(swapParams): - self?.performSwapSubmission( - for: swapParams, - signer: signer, - runCompletionIn: queue, - completion: closure - ) - case let .failure(error): - dispatchInQueueWhenPossible(queue) { - closure(.failure(error)) - } - } - } - ) - } -} diff --git a/novawallet/Modules/AssetDetails/AssetDetailsInteractor.swift b/novawallet/Modules/AssetDetails/AssetDetailsInteractor.swift index 05b2099b9a..bcb10ea782 100644 --- a/novawallet/Modules/AssetDetails/AssetDetailsInteractor.swift +++ b/novawallet/Modules/AssetDetails/AssetDetailsInteractor.swift @@ -8,7 +8,7 @@ final class AssetDetailsInteractor: AnyCancellableCleaning { let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol let externalBalancesSubscriptionFactory: ExternalBalanceLocalSubscriptionFactoryProtocol - let assetConvertionAggregator: AssetConversionAggregationFactoryProtocol + let swapState: SwapTokensFlowStateProtocol let purchaseProvider: PurchaseProviderProtocol let assetMapper: CustomAssetMapper let operationQueue: OperationQueue @@ -20,6 +20,8 @@ final class AssetDetailsInteractor: AnyCancellableCleaning { private var assetHoldsSubscription: StreamableProvider? private var swapsCall = CancellableCallStore() + private var assetExchangeService: AssetsExchangeServiceProtocol? + private var accountId: AccountId? { selectedMetaAccount.fetch(for: chainAsset.chain.accountRequest())?.accountId } @@ -31,7 +33,7 @@ final class AssetDetailsInteractor: AnyCancellableCleaning { walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, externalBalancesSubscriptionFactory: ExternalBalanceLocalSubscriptionFactoryProtocol, - assetConvertionAggregator: AssetConversionAggregationFactoryProtocol, + swapState: SwapTokensFlowStateProtocol, operationQueue: OperationQueue, currencyManager: CurrencyManagerProtocol ) { @@ -41,7 +43,7 @@ final class AssetDetailsInteractor: AnyCancellableCleaning { self.selectedMetaAccount = selectedMetaAccount self.chainAsset = chainAsset self.purchaseProvider = purchaseProvider - self.assetConvertionAggregator = assetConvertionAggregator + self.swapState = swapState self.operationQueue = operationQueue assetMapper = CustomAssetMapper( type: chainAsset.asset.type, @@ -62,17 +64,27 @@ final class AssetDetailsInteractor: AnyCancellableCleaning { } } - private func fetchSwapsAndProvideOperations() { + private func fetchSwapsAndProvideOperations(for chainAsset: ChainAsset) { swapsCall.cancel() - guard chainAsset.chain.hasSwaps else { + guard let assetExchangeService else { return } - let wrapper = assetConvertionAggregator.createAvailableDirectionsWrapper(for: chainAsset) + let wrapper = assetExchangeService.fetchReachibilityWrapper() + + let checkOperation = ClosureOperation { + let reachability = try wrapper.targetOperation.extractNoCancellableResultData() + + return !reachability.getAssetsOut(for: chainAsset.chainAssetId).isEmpty + } + + checkOperation.addDependency(wrapper.targetOperation) + + let totalWrapper = wrapper.insertingTail(operation: checkOperation) executeCancellable( - wrapper: wrapper, + wrapper: totalWrapper, inOperationQueue: operationQueue, backingCallIn: swapsCall, runningCallbackIn: .main @@ -82,8 +94,7 @@ final class AssetDetailsInteractor: AnyCancellableCleaning { } switch result { - case let .success(directions): - let hasSwaps = !directions.isEmpty + case let .success(hasSwaps): self.setAvailableOperations(hasSwaps: hasSwaps) case let .failure(error): self.presenter?.didReceive(error: .swaps(error)) @@ -91,6 +102,21 @@ final class AssetDetailsInteractor: AnyCancellableCleaning { } } + private func setupSwapService() { + assetExchangeService = swapState.setupAssetExchangeService() + + assetExchangeService?.subscribeUpdates( + for: self, + notifyingIn: .main + ) { [weak self] in + guard let self else { + return + } + + fetchSwapsAndProvideOperations(for: chainAsset) + } + } + private func setAvailableOperations(hasSwaps: Bool) { guard let accountId = accountId else { return @@ -158,9 +184,7 @@ extension AssetDetailsInteractor: AssetDetailsInteractorInputProtocol { setAvailableOperations(hasSwaps: false) - if chainAsset.chain.hasSwaps { - fetchSwapsAndProvideOperations() - } + setupSwapService() } } diff --git a/novawallet/Modules/AssetDetails/AssetDetailsViewFactory.swift b/novawallet/Modules/AssetDetails/AssetDetailsViewFactory.swift index d2996fdc73..f0e6314625 100644 --- a/novawallet/Modules/AssetDetails/AssetDetailsViewFactory.swift +++ b/novawallet/Modules/AssetDetails/AssetDetailsViewFactory.swift @@ -5,7 +5,8 @@ struct AssetDetailsViewFactory { static func createView( chain: ChainModel, asset: AssetModel, - operationState: AssetOperationState + operationState: AssetOperationState, + swapState: SwapTokensFlowStateProtocol ) -> AssetDetailsViewProtocol? { guard let currencyManager = CurrencyManager.shared else { return nil @@ -16,11 +17,6 @@ struct AssetDetailsViewFactory { let chainAsset = ChainAsset(chain: chain, asset: asset) - let assetConversionAggregator = AssetConversionAggregationFactory( - chainRegistry: ChainRegistryFacade.sharedRegistry, - operationQueue: OperationManagerFacade.sharedDefaultQueue - ) - let interactor = AssetDetailsInteractor( selectedMetaAccount: selectedAccount, chainAsset: chainAsset, @@ -28,12 +24,12 @@ struct AssetDetailsViewFactory { walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, externalBalancesSubscriptionFactory: ExternalBalanceLocalSubscriptionFactory.shared, - assetConvertionAggregator: assetConversionAggregator, + swapState: swapState, operationQueue: OperationManagerFacade.sharedDefaultQueue, currencyManager: currencyManager ) - let wireframe = AssetDetailsWireframe(operationState: operationState) + let wireframe = AssetDetailsWireframe(operationState: operationState, swapState: swapState) let priceAssetInfoFactory = PriceAssetInfoFactory(currencyManager: currencyManager) let viewModelFactory = AssetDetailsViewModelFactory( diff --git a/novawallet/Modules/AssetDetails/AssetDetailsWireframe.swift b/novawallet/Modules/AssetDetails/AssetDetailsWireframe.swift index 3f9fe138a4..799b8cc729 100644 --- a/novawallet/Modules/AssetDetails/AssetDetailsWireframe.swift +++ b/novawallet/Modules/AssetDetails/AssetDetailsWireframe.swift @@ -5,9 +5,14 @@ import SoraFoundation final class AssetDetailsWireframe: AssetDetailsWireframeProtocol { let operationState: AssetOperationState + let swapState: SwapTokensFlowStateProtocol - init(operationState: AssetOperationState) { + init( + operationState: AssetOperationState, + swapState: SwapTokensFlowStateProtocol + ) { self.operationState = operationState + self.swapState = swapState } func showPurchaseTokens( @@ -101,7 +106,7 @@ final class AssetDetailsWireframe: AssetDetailsWireframeProtocol { func showSwaps(from view: AssetDetailsViewProtocol?, chainAsset: ChainAsset) { guard let swapsView = SwapSetupViewFactory.createView( - assetListObservable: operationState.assetListObservable, + state: swapState, payChainAsset: chainAsset, swapCompletionClosure: operationState.swapCompletionClosure ) else { diff --git a/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerViewFactory.swift b/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerViewFactory.swift index e31993bc87..8abfc05b8b 100644 --- a/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerViewFactory.swift +++ b/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerViewFactory.swift @@ -6,15 +6,24 @@ final class AssetDetailsContainerViewFactory: AssetDetailsContainerViewFactoryPr asset: AssetModel, operationState: AssetOperationState ) -> AssetDetailsContainerViewProtocol? { + let swapState = SwapTokensFlowState( + assetListObservable: operationState.assetListObservable, + assetExchangeParams: AssetExchangeGraphProvidingParams( + wallet: SelectedWalletSettings.shared.value + ) + ) + guard let accountView = AssetDetailsViewFactory.createView( chain: chain, asset: asset, - operationState: operationState + operationState: operationState, + swapState: swapState ), let historyView = TransactionHistoryViewFactory.createView( chainAsset: .init(chain: chain, asset: asset), - operationState: operationState + operationState: operationState, + swapState: swapState ) else { return nil } diff --git a/novawallet/Modules/AssetList/AssetListWireframe.swift b/novawallet/Modules/AssetList/AssetListWireframe.swift index bd0eb93149..c9591521f5 100644 --- a/novawallet/Modules/AssetList/AssetListWireframe.swift +++ b/novawallet/Modules/AssetList/AssetListWireframe.swift @@ -117,15 +117,17 @@ final class AssetListWireframe: AssetListWireframeProtocol { let completionClosure: (ChainAsset) -> Void = { [weak self] chainAsset in self?.showAssetDetails(from: view, chain: chainAsset.chain, asset: chainAsset.asset) } - let selectClosure: (ChainAsset) -> Void = { [weak self] chainAsset in + let selectClosure: SwapAssetSelectionClosure = { [weak self] chainAsset, state in self?.showSwapTokens( from: view, + state: state, payAsset: chainAsset, swapCompletionClosure: completionClosure ) } guard let swapDirectionsView = SwapAssetsOperationViewFactory.createSelectPayTokenView( for: assetListModelObservable, + selectionModel: .payForAsset(nil), selectClosureStrategy: .callbackAfterDismissal, selectClosure: selectClosure ) else { @@ -182,11 +184,12 @@ final class AssetListWireframe: AssetListWireframeProtocol { private func showSwapTokens( from view: AssetListViewProtocol?, + state: SwapTokensFlowStateProtocol, payAsset: ChainAsset, swapCompletionClosure: SwapCompletionClosure? ) { guard let swapTokensView = SwapSetupViewFactory.createView( - assetListObservable: assetListModelObservable, + state: state, payChainAsset: payAsset, swapCompletionClosure: swapCompletionClosure ) else { diff --git a/novawallet/Modules/AssetList/Base/AssetListAssetsViewModelFactory.swift b/novawallet/Modules/AssetList/Base/AssetListAssetsViewModelFactory.swift index 0081d1bdc9..a4c7a5014e 100644 --- a/novawallet/Modules/AssetList/Base/AssetListAssetsViewModelFactory.swift +++ b/novawallet/Modules/AssetList/Base/AssetListAssetsViewModelFactory.swift @@ -181,7 +181,7 @@ private extension AssetListAssetViewModelFactory { if let priceData { let balanceValue = balanceViewModelFactory.priceFromFiatAmount( value, - priceData: priceData + currencyId: priceData.currencyId ).value(for: locale) return (balanceState, .loaded(value: balanceValue)) @@ -283,7 +283,7 @@ extension AssetListAssetViewModelFactory: AssetListAssetViewModelFactoryProtocol let priceString: String = if let asset = assets.first, let priceData = asset.priceData { balanceViewModelFactory(assetInfo: asset.assetInfo) - .priceFromFiatAmount(value, priceData: priceData) + .priceFromFiatAmount(value, currencyId: priceData.currencyId) .value(for: locale) } else { "" diff --git a/novawallet/Modules/AssetsSearch/AssetOperation/AssetOperationNetworkList/AssetOperationNetworkListViewFactory.swift b/novawallet/Modules/AssetsSearch/AssetOperation/AssetOperationNetworkList/AssetOperationNetworkListViewFactory.swift index 3b5a04dc4d..7af5e89b39 100644 --- a/novawallet/Modules/AssetsSearch/AssetOperation/AssetOperationNetworkList/AssetOperationNetworkListViewFactory.swift +++ b/novawallet/Modules/AssetsSearch/AssetOperation/AssetOperationNetworkList/AssetOperationNetworkListViewFactory.swift @@ -197,8 +197,8 @@ extension AssetOperationNetworkListViewFactory { extension AssetOperationNetworkListViewFactory { static func createSwapsView( with multichainToken: MultichainToken, - stateObservable: AssetListModelObservable, - selectClosure: @escaping (ChainAsset) -> Void, + state: SwapTokensFlowStateProtocol, + selectClosure: @escaping SwapAssetSelectionClosure, selectClosureStrategy: SubmoduleNavigationStrategy ) -> AssetOperationNetworkListViewProtocol? { guard let currencyManager = CurrencyManager.shared else { @@ -209,7 +209,7 @@ extension AssetOperationNetworkListViewFactory { let interactor = SpendAssetOperationNetworkListInteractor( multichainToken: multichainToken, - stateObservable: stateObservable, + stateObservable: state.assetListObservable, logger: logger ) @@ -217,7 +217,7 @@ extension AssetOperationNetworkListViewFactory { dependencies: SwapPresenterDependencies( interactor: interactor, multichainToken: multichainToken, - stateObservable: stateObservable, + state: state, currencyManager: currencyManager, selectClosure: selectClosure, selectClosureStrategy: selectClosureStrategy @@ -227,7 +227,6 @@ extension AssetOperationNetworkListViewFactory { let view = AssetOperationNetworkListViewController(presenter: presenter) presenter.view = view - interactor.presenter = presenter return view } @@ -237,17 +236,27 @@ extension AssetOperationNetworkListViewFactory { ) -> SwapOperationNetworkListPresenter { let viewModelFactory = createViewModelFactory(with: dependencies.currencyManager) - let wireframe = SwapAssetsOperationWireframe(stateObservable: dependencies.stateObservable) + let wireframe = SwapAssetsOperationWireframe( + state: dependencies.state, + selectClosure: dependencies.selectClosure, + selectClosureStrategy: dependencies.selectClosureStrategy + ) - return SwapOperationNetworkListPresenter( + let presenter = SwapOperationNetworkListPresenter( interactor: dependencies.interactor, wireframe: wireframe, multichainToken: dependencies.multichainToken, viewModelFactory: viewModelFactory, - selectClosure: dependencies.selectClosure, + selectClosure: { chainAsset in + dependencies.selectClosure(chainAsset, dependencies.state) + }, selectClosureStrategy: dependencies.selectClosureStrategy, localizationManager: LocalizationManager.shared ) + + dependencies.interactor.presenter = presenter + + return presenter } } @@ -280,9 +289,9 @@ private extension AssetOperationNetworkListViewFactory { struct SwapPresenterDependencies { let interactor: AssetOperationNetworkListInteractor let multichainToken: MultichainToken - let stateObservable: AssetListModelObservable + let state: SwapTokensFlowStateProtocol let currencyManager: CurrencyManager - let selectClosure: (ChainAsset) -> Void + let selectClosure: SwapAssetSelectionClosure let selectClosureStrategy: SubmoduleNavigationStrategy } } diff --git a/novawallet/Modules/AssetsSearch/AssetOperation/Swaps/SwapAssetOperationPresenter.swift b/novawallet/Modules/AssetsSearch/AssetOperation/Swaps/SwapAssetOperationPresenter.swift index 9d9a754376..ed7adb1c52 100644 --- a/novawallet/Modules/AssetsSearch/AssetOperation/Swaps/SwapAssetOperationPresenter.swift +++ b/novawallet/Modules/AssetsSearch/AssetOperation/Swaps/SwapAssetOperationPresenter.swift @@ -62,9 +62,7 @@ final class SwapAssetsOperationPresenter: AssetsSearchPresenter { onMultipleInstances: { multichainToken in swapAssetsWireframe?.showSelectNetwork( from: view, - multichainToken: multichainToken, - selectClosure: selectClosure, - selectClosureStrategy: selectClosureStrategy + multichainToken: multichainToken ) } ) diff --git a/novawallet/Modules/AssetsSearch/AssetOperation/Swaps/SwapAssetOperationWireframe.swift b/novawallet/Modules/AssetsSearch/AssetOperation/Swaps/SwapAssetOperationWireframe.swift index c3f4b33796..522089014b 100644 --- a/novawallet/Modules/AssetsSearch/AssetOperation/Swaps/SwapAssetOperationWireframe.swift +++ b/novawallet/Modules/AssetsSearch/AssetOperation/Swaps/SwapAssetOperationWireframe.swift @@ -1,16 +1,25 @@ import UIKit import SoraUI -final class SwapAssetsOperationWireframe: AssetOperationWireframe, SwapAssetsOperationWireframeProtocol { - func showSelectNetwork( - from view: ControllerBackedProtocol?, - multichainToken: MultichainToken, - selectClosure: @escaping (ChainAsset) -> Void, +final class SwapAssetsOperationWireframe: SwapAssetsOperationWireframeProtocol { + let state: SwapTokensFlowStateProtocol + let selectClosure: SwapAssetSelectionClosure + let selectClosureStrategy: SubmoduleNavigationStrategy + + init( + state: SwapTokensFlowStateProtocol, + selectClosure: @escaping SwapAssetSelectionClosure, selectClosureStrategy: SubmoduleNavigationStrategy ) { + self.state = state + self.selectClosure = selectClosure + self.selectClosureStrategy = selectClosureStrategy + } + + func showSelectNetwork(from view: ControllerBackedProtocol?, multichainToken: MultichainToken) { guard let selectNetworkView = AssetOperationNetworkListViewFactory.createSwapsView( with: multichainToken, - stateObservable: stateObservable, + state: state, selectClosure: selectClosure, selectClosureStrategy: selectClosureStrategy ) else { diff --git a/novawallet/Modules/AssetsSearch/AssetOperation/Swaps/SwapAssetSelectionModel.swift b/novawallet/Modules/AssetsSearch/AssetOperation/Swaps/SwapAssetSelectionModel.swift new file mode 100644 index 0000000000..da40ce37a0 --- /dev/null +++ b/novawallet/Modules/AssetsSearch/AssetOperation/Swaps/SwapAssetSelectionModel.swift @@ -0,0 +1,6 @@ +import Foundation + +enum SwapAssetSelectionModel { + case payForAsset(ChainAsset?) + case receivePayingWith(ChainAsset?) +} diff --git a/novawallet/Modules/AssetsSearch/AssetOperation/Swaps/SwapAssetsOperationInteractor.swift b/novawallet/Modules/AssetsSearch/AssetOperation/Swaps/SwapAssetsOperationInteractor.swift index 6dfb37fcb6..0340d2a802 100644 --- a/novawallet/Modules/AssetsSearch/AssetOperation/Swaps/SwapAssetsOperationInteractor.swift +++ b/novawallet/Modules/AssetsSearch/AssetOperation/Swaps/SwapAssetsOperationInteractor.swift @@ -5,159 +5,91 @@ import Operation_iOS final class SwapAssetsOperationInteractor: AnyCancellableCleaning { weak var presenter: SwapAssetsOperationPresenterProtocol? - let stateObservable: AssetListModelObservable + let state: SwapTokensFlowStateProtocol let logger: LoggerProtocol - let chainAsset: ChainAsset? - let assetConversionAggregation: AssetConversionAggregationFactoryProtocol + let selectionModel: SwapAssetSelectionModel let settingsManager: SettingsManagerProtocol private let operationQueue: OperationQueue + private var builder: SpendAssetSearchBuilder? - private var directionsCall = CancellableCallStore() - private var availableDirections: [ChainAssetId: Set] = [:] - private var availableChains: Set = [] + private var reachabilityCallStore = CancellableCallStore() + private var reachability: AssetsExchageGraphReachabilityProtocol? + + private var assetExchangeService: AssetsExchangeServiceProtocol? init( - stateObservable: AssetListModelObservable, - chainAsset: ChainAsset?, - assetConversionAggregation: AssetConversionAggregationFactoryProtocol, + state: SwapTokensFlowStateProtocol, + selectionModel: SwapAssetSelectionModel, settingsManager: SettingsManagerProtocol, operationQueue: OperationQueue, logger: LoggerProtocol ) { - self.stateObservable = stateObservable + self.state = state self.logger = logger - self.chainAsset = chainAsset - self.assetConversionAggregation = assetConversionAggregation + self.selectionModel = selectionModel self.settingsManager = settingsManager self.operationQueue = operationQueue } deinit { - directionsCall.cancel() + reachabilityCallStore.cancel() } private func reloadDirectionsIfNeeded() { - if let chainAsset = chainAsset { - guard !availableChains.contains(chainAsset.chain.chainId), chainAsset.chain.hasSwaps else { - presenter?.directionsLoaded() - return - } - - availableChains.insert(chainAsset.chain.chainId) - availableDirections = [:] - loadAssetDirections(for: chainAsset) - } else { - let allChains = stateObservable.state.value.allChains.values - - let chainsWithSwaps = allChains.filter(\.hasSwaps) - let chainsWithSwapsIds = Set(chainsWithSwaps.map(\.chainId)) - - if chainsWithSwapsIds != availableChains { - availableChains = chainsWithSwapsIds - availableDirections = [:] - - loadDirections(for: chainsWithSwaps) - } else { - presenter?.directionsLoaded() - } - } - } - - private func loadDirections(for chains: [ChainModel]) { - directionsCall.cancel() - - let wrappers = chains.map { assetConversionAggregation.createAvailableDirectionsWrapper(for: $0) } - - let dependencies = wrappers.flatMap(\.allOperations) - - let mergingOperation = ClosureOperation { - try wrappers.forEach { _ = try $0.targetOperation.extractNoCancellableResultData() } - } - - dependencies.forEach { - mergingOperation.addDependency($0) - } - - let commonWrapper = CompoundOperationWrapper(targetOperation: mergingOperation, dependencies: dependencies) - - wrappers.forEach { wrapper in - wrapper.targetOperation.completionBlock = { [weak self] in - DispatchQueue.main.async { - guard let self = self else { - return - } - - if case let .success(directions) = wrapper.targetOperation.result { - self.updateAvailableDirections(directions) - } - } - } - } - - executeCancellable( - wrapper: commonWrapper, - inOperationQueue: operationQueue, - backingCallIn: directionsCall, - runningCallbackIn: .main - ) { [weak self] result in - switch result { - case .success: - self?.presenter?.directionsLoaded() - case let .failure(error): - self?.handleDirectionsResponse(error: error) - } + guard let assetExchangeService else { + return } - } - private func loadAssetDirections(for chainAsset: ChainAsset) { - directionsCall.cancel() - - let wrapper = assetConversionAggregation.createAvailableDirectionsWrapper(for: chainAsset) + let wrapper = assetExchangeService.fetchReachibilityWrapper() executeCancellable( wrapper: wrapper, inOperationQueue: operationQueue, - backingCallIn: directionsCall, + backingCallIn: reachabilityCallStore, runningCallbackIn: .main ) { [weak self] result in switch result { - case let .success(directions): - self?.updateAvailableDirections([chainAsset.chainAssetId: directions]) + case let .success(reachibility): + self?.reachability = reachibility + self?.builder?.reload() self?.presenter?.directionsLoaded() case let .failure(error): - self?.handleDirectionsResponse(error: error) + self?.presenter?.didReceive(error: .directions(error)) } } } - private func handleDirectionsResponse(error: Error) { - if let encodingError = error as? StorageKeyEncodingOperationError, encodingError == .invalidStoragePath { - // ignore not retryable errors - presenter?.directionsLoaded() - } else { - presenter?.didReceive(error: .directions(error)) - } - } - - private func updateAvailableDirections(_ newDirections: [ChainAssetId: Set]) { - availableDirections = newDirections.reduce(into: availableDirections) { accum, keyValue in - accum[keyValue.key] = keyValue.value - } - - builder?.reload() - } - private func createBuilder() { let searchQueue = OperationQueue() searchQueue.maxConcurrentOperationCount = 1 let filter: ChainAssetsFilter = { [weak self] chainAsset in - guard let availableDirections = self?.availableDirections else { + guard let self, let reachability else { return false } - return availableDirections.contains(where: { $0.value.contains(chainAsset.chainAssetId) }) + + switch selectionModel { + case let .payForAsset(receiveAsset): + let assetsOut = reachability.getAssetsOut(for: chainAsset.chainAssetId) + + if let receiveAsset { + return assetsOut.contains(receiveAsset.chainAssetId) + } else { + return !assetsOut.isEmpty + } + + case let .receivePayingWith(payAsset): + let assetsIn = reachability.getAssetsIn(for: chainAsset.chainAssetId) + + if let payAsset { + return assetsIn.contains(payAsset.chainAssetId) + + } else { + return !assetsIn.isEmpty + } + } } builder = .init( @@ -174,14 +106,24 @@ final class SwapAssetsOperationInteractor: AnyCancellableCleaning { logger: logger ) - builder?.apply(model: stateObservable.state.value) + builder?.apply(model: state.assetListObservable.state.value) - stateObservable.addObserver(with: self) { [weak self] _, newState in + state.assetListObservable.addObserver(with: self) { [weak self] _, newState in guard let self = self else { return } self.builder?.apply(model: newState.value) - self.reloadDirectionsIfNeeded() + } + } + + private func setupSwapService() { + assetExchangeService = state.setupAssetExchangeService() + + assetExchangeService?.subscribeUpdates( + for: self, + notifyingIn: .main + ) { [weak self] in + self?.reloadDirectionsIfNeeded() } } @@ -196,7 +138,7 @@ extension SwapAssetsOperationInteractor: SwapAssetsOperationInteractorInputProto func setup() { provideAssetsGroupStyle() createBuilder() - reloadDirectionsIfNeeded() + setupSwapService() } func search(query: String) { diff --git a/novawallet/Modules/AssetsSearch/AssetOperation/Swaps/SwapAssetsOperationProtocols.swift b/novawallet/Modules/AssetsSearch/AssetOperation/Swaps/SwapAssetsOperationProtocols.swift index 74e00ac012..c77814f35f 100644 --- a/novawallet/Modules/AssetsSearch/AssetOperation/Swaps/SwapAssetsOperationProtocols.swift +++ b/novawallet/Modules/AssetsSearch/AssetOperation/Swaps/SwapAssetsOperationProtocols.swift @@ -2,9 +2,7 @@ protocol SwapAssetsOperationWireframeProtocol: AssetsSearchWireframeProtocol, Er AlertPresentable, CommonRetryable { func showSelectNetwork( from view: ControllerBackedProtocol?, - multichainToken: MultichainToken, - selectClosure: @escaping (ChainAsset) -> Void, - selectClosureStrategy: SubmoduleNavigationStrategy + multichainToken: MultichainToken ) } diff --git a/novawallet/Modules/AssetsSearch/AssetOperation/Swaps/SwapAssetsOperationViewFactory.swift b/novawallet/Modules/AssetsSearch/AssetOperation/Swaps/SwapAssetsOperationViewFactory.swift index bcc6021e02..39e078a1ca 100644 --- a/novawallet/Modules/AssetsSearch/AssetOperation/Swaps/SwapAssetsOperationViewFactory.swift +++ b/novawallet/Modules/AssetsSearch/AssetOperation/Swaps/SwapAssetsOperationViewFactory.swift @@ -5,9 +5,30 @@ import SoraFoundation enum SwapAssetsOperationViewFactory { static func createSelectPayTokenView( for stateObservable: AssetListModelObservable, - chainAsset: ChainAsset? = nil, + selectionModel: SwapAssetSelectionModel, selectClosureStrategy: SubmoduleNavigationStrategy = .callbackBeforeDismissal, - selectClosure: @escaping (ChainAsset) -> Void + selectClosure: @escaping SwapAssetSelectionClosure + ) -> AssetsSearchViewProtocol? { + let state = SwapTokensFlowState( + assetListObservable: stateObservable, + assetExchangeParams: AssetExchangeGraphProvidingParams( + wallet: SelectedWalletSettings.shared.value + ) + ) + + return createSelectPayTokenViewWithState( + state, + selectionModel: selectionModel, + selectClosureStrategy: selectClosureStrategy, + selectClosure: selectClosure + ) + } + + static func createSelectPayTokenViewWithState( + _ state: SwapTokensFlowStateProtocol, + selectionModel: SwapAssetSelectionModel, + selectClosureStrategy: SubmoduleNavigationStrategy = .callbackBeforeDismissal, + selectClosure: @escaping SwapAssetSelectionClosure ) -> AssetsSearchViewProtocol? { let title: LocalizableResource = .init { R.string.localizable.swapsPayTokenSelectionTitle( @@ -16,8 +37,8 @@ enum SwapAssetsOperationViewFactory { } return createView( - for: stateObservable, - chainAsset: chainAsset, + using: state, + selectionModel: selectionModel, title: title, selectClosureStrategy: selectClosureStrategy, selectClosure: selectClosure @@ -26,9 +47,30 @@ enum SwapAssetsOperationViewFactory { static func createSelectReceiveTokenView( for stateObservable: AssetListModelObservable, - chainAsset: ChainAsset? = nil, + selectionModel: SwapAssetSelectionModel, + selectClosureStrategy: SubmoduleNavigationStrategy = .callbackBeforeDismissal, + selectClosure: @escaping SwapAssetSelectionClosure + ) -> AssetsSearchViewProtocol? { + let state = SwapTokensFlowState( + assetListObservable: stateObservable, + assetExchangeParams: AssetExchangeGraphProvidingParams( + wallet: SelectedWalletSettings.shared.value + ) + ) + + return createSelectReceiveTokenViewWithState( + state, + selectionModel: selectionModel, + selectClosureStrategy: selectClosureStrategy, + selectClosure: selectClosure + ) + } + + static func createSelectReceiveTokenViewWithState( + _ state: SwapTokensFlowStateProtocol, + selectionModel: SwapAssetSelectionModel, selectClosureStrategy: SubmoduleNavigationStrategy = .callbackBeforeDismissal, - selectClosure: @escaping (ChainAsset) -> Void + selectClosure: @escaping SwapAssetSelectionClosure ) -> AssetsSearchViewProtocol? { let title: LocalizableResource = .init { R.string.localizable.swapsReceiveTokenSelectionTitle( @@ -37,8 +79,8 @@ enum SwapAssetsOperationViewFactory { } return createView( - for: stateObservable, - chainAsset: chainAsset, + using: state, + selectionModel: selectionModel, title: title, selectClosureStrategy: selectClosureStrategy, selectClosure: selectClosure @@ -46,11 +88,11 @@ enum SwapAssetsOperationViewFactory { } static func createView( - for stateObservable: AssetListModelObservable, - chainAsset: ChainAsset? = nil, + using state: SwapTokensFlowStateProtocol, + selectionModel: SwapAssetSelectionModel, title: LocalizableResource, selectClosureStrategy: SubmoduleNavigationStrategy, - selectClosure: @escaping (ChainAsset) -> Void + selectClosure: @escaping SwapAssetSelectionClosure ) -> AssetsSearchViewProtocol? { guard let currencyManager = CurrencyManager.shared else { return nil @@ -67,9 +109,9 @@ enum SwapAssetsOperationViewFactory { ) guard let presenter = createPresenter( - stateObservable: stateObservable, + state: state, viewModelFactory: viewModelFactory, - chainAsset: chainAsset, + selectionModel: selectionModel, selectClosureStrategy: selectClosureStrategy, selectClosure: selectClosure ) else { @@ -90,37 +132,35 @@ enum SwapAssetsOperationViewFactory { } private static func createPresenter( - stateObservable: AssetListModelObservable, + state: SwapTokensFlowStateProtocol, viewModelFactory: AssetListAssetViewModelFactoryProtocol, - chainAsset: ChainAsset?, + selectionModel: SwapAssetSelectionModel, selectClosureStrategy: SubmoduleNavigationStrategy, - selectClosure: @escaping (ChainAsset) -> Void + selectClosure: @escaping SwapAssetSelectionClosure ) -> SwapAssetsOperationPresenter? { - let chainRegistry = ChainRegistryFacade.sharedRegistry - let operationQueue = OperationManagerFacade.sharedDefaultQueue - let assetConversionAggregator = AssetConversionAggregationFactory( - chainRegistry: chainRegistry, - operationQueue: OperationManagerFacade.sharedDefaultQueue - ) - let interactor = SwapAssetsOperationInteractor( - stateObservable: stateObservable, - chainAsset: chainAsset, - assetConversionAggregation: assetConversionAggregator, + state: state, + selectionModel: selectionModel, settingsManager: SettingsManager.shared, operationQueue: operationQueue, logger: Logger.shared ) let presenter = SwapAssetsOperationPresenter( - selectClosure: selectClosure, + selectClosure: { chainAsset in + selectClosure(chainAsset, state) + }, selectClosureStrategy: selectClosureStrategy, interactor: interactor, viewModelFactory: viewModelFactory, localizationManager: LocalizationManager.shared, - wireframe: SwapAssetsOperationWireframe(stateObservable: stateObservable), + wireframe: SwapAssetsOperationWireframe( + state: state, + selectClosure: selectClosure, + selectClosureStrategy: selectClosureStrategy + ), logger: Logger.shared ) diff --git a/novawallet/Modules/AssetsSearch/Model/SpendAssetSearchBuilder.swift b/novawallet/Modules/AssetsSearch/Model/SpendAssetSearchBuilder.swift index c53c5a7c1e..0ffcf6ec21 100644 --- a/novawallet/Modules/AssetsSearch/Model/SpendAssetSearchBuilder.swift +++ b/novawallet/Modules/AssetsSearch/Model/SpendAssetSearchBuilder.swift @@ -2,12 +2,18 @@ import BigInt final class SpendAssetSearchBuilder: AssetSearchBuilder { override func assetListState(from model: AssetListModel) -> AssetListState { - let balanceResults = model.balances.reduce(into: [ChainAssetId: Result]()) { - switch $1.value { - case let .success(balance): - $0[$1.key] = .success(balance.transferable) + let chainAssets = model.allChains.flatMap { _, chain in + chain.assets.map { ChainAssetId(chainId: chain.chainId, assetId: $0.assetId) } + } + + let balanceResults = chainAssets.reduce(into: [ChainAssetId: Result]()) { + switch model.balances[$1] { + case let .success(amount): + $0[$1] = .success(amount.transferable) case let .failure(error): - $0[$1.key] = .failure(error) + $0[$1] = .failure(error) + case .none: + $0[$1] = .success(0) } } diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmViewFactory.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmViewFactory.swift index d11a20e698..23ae2cb308 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmViewFactory.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmViewFactory.swift @@ -153,22 +153,25 @@ struct DAppOperationConfirmViewFactory { let operationQueue = OperationManagerFacade.sharedDefaultQueue let substrateStorageFacade = SubstrateDataStorageFacade.shared - let feeEstimatingWrapperFactory = ExtrinsicFeeEstimatingWrapperFactory( + let extrinsicFeeHost = ExtrinsicFeeEstimatorHost( account: account, chain: chain, - runtimeService: runtimeProvider, connection: connection, + runtimeProvider: runtimeProvider, + userStorageFacade: UserDataStorageFacade.shared, + substrateStorageFacade: substrateStorageFacade, operationQueue: operationQueue ) let feeEstimationRegistry = ExtrinsicFeeEstimationRegistry( chain: chain, - estimatingWrapperFactory: feeEstimatingWrapperFactory, - connection: connection, - runtimeProvider: runtimeProvider, - userStorageFacade: UserDataStorageFacade.shared, - substrateStorageFacade: substrateStorageFacade, - operationQueue: operationQueue + estimatingWrapperFactory: ExtrinsicFeeEstimatingWrapperFactory( + host: extrinsicFeeHost, + customFeeEstimatorFactory: AssetConversionFeeEstimatingFactory(host: extrinsicFeeHost) + ), + feeInstallingWrapperFactory: ExtrinsicFeeInstallingWrapperFactory( + customFeeInstallerFactory: AssetConversionFeeInstallingFactory(host: extrinsicFeeHost) + ) ) let metadataHashFactory = MetadataHashOperationFactory( diff --git a/novawallet/Modules/InAppUpdates/InAppUpdatesStyles.swift b/novawallet/Modules/InAppUpdates/InAppUpdatesStyles.swift index 7d06767d42..8f14e8dc16 100644 --- a/novawallet/Modules/InAppUpdates/InAppUpdatesStyles.swift +++ b/novawallet/Modules/InAppUpdates/InAppUpdatesStyles.swift @@ -13,6 +13,19 @@ extension BorderedLabelView { } extension BorderedLabelView.Style { + static let stepNumber = BorderedLabelView.Style( + text: .init( + textColor: R.color.colorTextSecondary()!, + font: .semiBoldCaps1 + ), + background: .init( + shadowOpacity: 0, + strokeWidth: 0, + fillColor: R.color.colorRouteNumberBackground()!, + highlightedFillColor: R.color.colorRouteNumberBackground()! + ) + ) + static let chipsText = BorderedLabelView.Style( text: .init( textColor: R.color.colorChipText()!, diff --git a/novawallet/Modules/Ledger/AccountConfirmation/Base/LedgerBaseAccountConfirmationInteractor.swift b/novawallet/Modules/Ledger/AccountConfirmation/Base/LedgerBaseAccountConfirmationInteractor.swift index 44b1e22fb7..3f66087e27 100644 --- a/novawallet/Modules/Ledger/AccountConfirmation/Base/LedgerBaseAccountConfirmationInteractor.swift +++ b/novawallet/Modules/Ledger/AccountConfirmation/Base/LedgerBaseAccountConfirmationInteractor.swift @@ -77,7 +77,7 @@ class LedgerBaseAccountConfirmationInteractor { factory: { try codingFactoryOperation.extractNoCancellableResultData() }, - storagePath: .account + storagePath: SystemPallet.accountPath ) balanceWrapper.addDependency(wrapper: ledgerWrapper) diff --git a/novawallet/Modules/Ledger/GenericLedgerAccountSelection/GenericLedgerAccountSelectionInteractor.swift b/novawallet/Modules/Ledger/GenericLedgerAccountSelection/GenericLedgerAccountSelectionInteractor.swift index 072cc025b7..e82acc3967 100644 --- a/novawallet/Modules/Ledger/GenericLedgerAccountSelection/GenericLedgerAccountSelectionInteractor.swift +++ b/novawallet/Modules/Ledger/GenericLedgerAccountSelection/GenericLedgerAccountSelectionInteractor.swift @@ -49,10 +49,6 @@ final class GenericLedgerAccountSelectionInteractor { ) -> CompoundOperationWrapper { let queryFactory = WalletRemoteQueryWrapperFactory( requestFactory: requestFactory, - assetInfoOperationFactory: AssetStorageInfoOperationFactory( - chainRegistry: chainRegistry, - operationQueue: operationQueue - ), runtimeProvider: runtimeProvider, connection: connection, operationQueue: operationQueue diff --git a/novawallet/Modules/MainTabBar/MainTabBarWireframe.swift b/novawallet/Modules/MainTabBar/MainTabBarWireframe.swift index f92b5cd14a..d8d8c59458 100644 --- a/novawallet/Modules/MainTabBar/MainTabBarWireframe.swift +++ b/novawallet/Modules/MainTabBar/MainTabBarWireframe.swift @@ -187,10 +187,12 @@ final class MainTabBarWireframe: MainTabBarWireframeProtocol { let navigationController = viewController as? UINavigationController navigationController?.popToRootViewController(animated: true) + // TODO: Check navigation logic here let operationState = AssetOperationState( assetListObservable: .init(state: .init(value: .init())), swapCompletionClosure: nil ) + guard let detailsView = AssetDetailsContainerViewFactory.createView( chain: chainAsset.chain, asset: chainAsset.asset, diff --git a/novawallet/Modules/OperationDetails/OperationDetailsViewFactory.swift b/novawallet/Modules/OperationDetails/OperationDetailsViewFactory.swift index 709f86d35f..dac52915d5 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsViewFactory.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsViewFactory.swift @@ -7,7 +7,8 @@ struct OperationDetailsViewFactory { static func createView( for transaction: TransactionHistoryItem, chainAsset: ChainAsset, - operationState: AssetOperationState + operationState: AssetOperationState, + swapState: SwapTokensFlowStateProtocol ) -> OperationDetailsViewProtocol? { guard let currencyManager = CurrencyManager.shared, @@ -61,7 +62,8 @@ struct OperationDetailsViewFactory { } let wireframe = OperationDetailsWireframe( - operationState: operationState + operationState: operationState, + swapState: swapState ) let localizationManager = LocalizationManager.shared diff --git a/novawallet/Modules/OperationDetails/OperationDetailsWireframe.swift b/novawallet/Modules/OperationDetails/OperationDetailsWireframe.swift index 904c444e74..ef938c6836 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsWireframe.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsWireframe.swift @@ -2,11 +2,14 @@ import Foundation final class OperationDetailsWireframe: OperationDetailsWireframeProtocol { let operationState: AssetOperationState + let swapState: SwapTokensFlowStateProtocol init( - operationState: AssetOperationState + operationState: AssetOperationState, + swapState: SwapTokensFlowStateProtocol ) { self.operationState = operationState + self.swapState = swapState } func showSend( @@ -29,7 +32,7 @@ final class OperationDetailsWireframe: OperationDetailsWireframeProtocol { state: SwapSetupInitState ) { guard let swapView = SwapSetupViewFactory.createView( - assetListObservable: operationState.assetListObservable, + state: swapState, initState: state, swapCompletionClosure: operationState.swapCompletionClosure ) else { diff --git a/novawallet/Modules/Proxy/ProxySignValidation/ProxySignValidationViewFactory.swift b/novawallet/Modules/Proxy/ProxySignValidation/ProxySignValidationViewFactory.swift index 6bdd5931c9..d8625d40f9 100644 --- a/novawallet/Modules/Proxy/ProxySignValidation/ProxySignValidationViewFactory.swift +++ b/novawallet/Modules/Proxy/ProxySignValidation/ProxySignValidationViewFactory.swift @@ -78,7 +78,6 @@ struct ProxySignValidationViewFactory { remoteFactory: StorageKeyFactory(), operationManager: OperationManagerFacade.sharedManager ), - assetInfoOperationFactory: assetInfoOperationFactory, runtimeProvider: runtimeProvider, connection: connection, operationQueue: OperationManagerFacade.sharedDefaultQueue diff --git a/novawallet/Modules/Staking/ControllerAccount/ControllerAccountInteractor.swift b/novawallet/Modules/Staking/ControllerAccount/ControllerAccountInteractor.swift index d51fb4d7d2..7ffc12af14 100644 --- a/novawallet/Modules/Staking/ControllerAccount/ControllerAccountInteractor.swift +++ b/novawallet/Modules/Staking/ControllerAccount/ControllerAccountInteractor.swift @@ -211,7 +211,7 @@ extension ControllerAccountInteractor: ControllerAccountInteractorInputProtocol engine: connection, keyParams: { [accountId] }, factory: { try coderFactoryOperation.extractNoCancellableResultData() }, - storagePath: .account + storagePath: SystemPallet.accountPath ) let mapOperation = ClosureOperation { diff --git a/novawallet/Modules/Staking/Operations/BlockTimeOperationFactory.swift b/novawallet/Modules/Staking/Operations/BlockTimeOperationFactory.swift index cffd93927a..67bca48399 100644 --- a/novawallet/Modules/Staking/Operations/BlockTimeOperationFactory.swift +++ b/novawallet/Modules/Staking/Operations/BlockTimeOperationFactory.swift @@ -43,26 +43,19 @@ final class BlockTimeOperationFactory { dependingOn: codingFactoryOperation ) - let minBlockTimeOperation: BaseOperation = PrimitiveConstantOperation.operation( - for: .minimumPeriodBetweenBlocks, - dependingOn: codingFactoryOperation - ) - let mapOperation = ClosureOperation { let optBabeTime = try? babeTimeOperation.extractNoCancellableResultData() - let optMinBlockTime = try? minBlockTimeOperation.extractNoCancellableResultData() - let exptectedBlockTime = (optBabeTime ?? optMinBlockTime) ?? fallbackTime + let exptectedBlockTime = optBabeTime ?? fallbackTime return exptectedBlockTime >= fallbackThreshold ? exptectedBlockTime : fallbackTime } mapOperation.addDependency(babeTimeOperation) - mapOperation.addDependency(minBlockTimeOperation) return CompoundOperationWrapper( targetOperation: mapOperation, - dependencies: [babeTimeOperation, minBlockTimeOperation] + dependencies: [babeTimeOperation] ) } } diff --git a/novawallet/Modules/Staking/Operations/EraCountdownOperationFactory/AuraEraOperationFactory.swift b/novawallet/Modules/Staking/Operations/EraCountdownOperationFactory/AuraEraOperationFactory.swift index 054daa9e7d..93c2e02d81 100644 --- a/novawallet/Modules/Staking/Operations/EraCountdownOperationFactory/AuraEraOperationFactory.swift +++ b/novawallet/Modules/Staking/Operations/EraCountdownOperationFactory/AuraEraOperationFactory.swift @@ -53,9 +53,9 @@ final class AuraEraOperationFactory: EraCountdownOperationFactoryProtocol { let blockNumberWrapper: CompoundOperationWrapper<[StorageResponse>]> = storageRequestFactory.queryItems( engine: connection, - keys: { [try keyFactory.key(from: .blockNumber)] }, + keys: { [try keyFactory.key(from: SystemPallet.blockNumberPath)] }, factory: { try codingFactoryOperation.extractNoCancellableResultData() }, - storagePath: .blockNumber + storagePath: SystemPallet.blockNumberPath ) let activeEraWrapper: CompoundOperationWrapper<[StorageResponse]> = diff --git a/novawallet/Modules/Staking/Parachain/ParaStkRebond/ParaStkRebondInteractor.swift b/novawallet/Modules/Staking/Parachain/ParaStkRebond/ParaStkRebondInteractor.swift index d14ff03267..8e00506e59 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkRebond/ParaStkRebondInteractor.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkRebond/ParaStkRebondInteractor.swift @@ -114,8 +114,8 @@ extension ParaStkRebondInteractor: ParaStkRebondInteractorInputProtocol { let notificationClosure: ExtrinsicSubscriptionStatusClosure = { [weak self] result in switch result { - case let .success(status): - if case let .inBlock(extrinsicHash) = status { + case let .success(statusUpdate): + if case let .inBlock(extrinsicHash) = statusUpdate.extrinsicStatus { self?.cancelExtrinsicSubscriptionIfNeeded() self?.presenter?.didCompleteExtrinsicSubmission(for: .success(extrinsicHash)) } diff --git a/novawallet/Modules/Staking/Parachain/ParaStkRedeem/ParaStkRedeemInteractor.swift b/novawallet/Modules/Staking/Parachain/ParaStkRedeem/ParaStkRedeemInteractor.swift index 6d12bb0bce..429bf19cfe 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkRedeem/ParaStkRedeemInteractor.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkRedeem/ParaStkRedeemInteractor.swift @@ -139,8 +139,8 @@ extension ParaStkRedeemInteractor: ParaStkRedeemInteractorInputProtocol { let notificationClosure: ExtrinsicSubscriptionStatusClosure = { [weak self] result in switch result { - case let .success(status): - if case let .inBlock(extrinsicHash) = status { + case let .success(statusUpdate): + if case let .inBlock(extrinsicHash) = statusUpdate.extrinsicStatus { self?.cancelExtrinsicSubscriptionIfNeeded() self?.presenter?.didCompleteExtrinsicSubmission(for: .success(extrinsicHash)) } diff --git a/novawallet/Modules/Staking/Parachain/ParaStkStakeConfirm/ParaStkStakeConfirmInteractor.swift b/novawallet/Modules/Staking/Parachain/ParaStkStakeConfirm/ParaStkStakeConfirmInteractor.swift index 4394fecb0f..d042534127 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkStakeConfirm/ParaStkStakeConfirmInteractor.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkStakeConfirm/ParaStkStakeConfirmInteractor.swift @@ -227,8 +227,8 @@ extension ParaStkStakeConfirmInteractor: ParaStkStakeConfirmInteractorInputProto let notificationClosure: ExtrinsicSubscriptionStatusClosure = { [weak self] result in switch result { - case let .success(status): - if case let .inBlock(extrinsicHash) = status { + case let .success(statusUpdate): + if case let .inBlock(extrinsicHash) = statusUpdate.extrinsicStatus { self?.cancelExtrinsicSubscriptionIfNeeded() self?.presenter?.didCompleteExtrinsicSubmission(for: .success(extrinsicHash)) } diff --git a/novawallet/Modules/Staking/Parachain/ParaStkUnstakeConfirm/ParaStkUnstakeConfirmInteractor.swift b/novawallet/Modules/Staking/Parachain/ParaStkUnstakeConfirm/ParaStkUnstakeConfirmInteractor.swift index f69574f7c5..709341fe59 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkUnstakeConfirm/ParaStkUnstakeConfirmInteractor.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkUnstakeConfirm/ParaStkUnstakeConfirmInteractor.swift @@ -73,8 +73,8 @@ extension ParaStkUnstakeConfirmInteractor: ParaStkUnstakeConfirmInteractorInputP let notificationClosure: ExtrinsicSubscriptionStatusClosure = { [weak self] result in switch result { - case let .success(status): - if case let .inBlock(extrinsicHash) = status { + case let .success(statusUpdate): + if case let .inBlock(extrinsicHash) = statusUpdate.extrinsicStatus { self?.cancelExtrinsicSubscriptionIfNeeded() self?.presenter?.didCompleteExtrinsicSubmission(for: .success(extrinsicHash)) } diff --git a/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStart/ParaStkYieldBoostStartInteractor.swift b/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStart/ParaStkYieldBoostStartInteractor.swift index 7d98e8d27d..fc48065700 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStart/ParaStkYieldBoostStartInteractor.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStart/ParaStkYieldBoostStartInteractor.swift @@ -97,8 +97,8 @@ extension ParaStkYieldBoostStartInteractor: ParaStkYieldBoostStartInteractorInpu let notificationClosure: ExtrinsicSubscriptionStatusClosure = { [weak self] result in switch result { - case let .success(status): - if case let .inBlock(extrinsicHash) = status { + case let .success(statusUpdate): + if case let .inBlock(extrinsicHash) = statusUpdate.extrinsicStatus { self?.cancelExtrinsicSubscriptionIfNeeded() self?.confirmPresenter?.didScheduleYieldBoost(for: extrinsicHash) } diff --git a/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStop/ParaStkYieldBoostStopInteractor.swift b/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStop/ParaStkYieldBoostStopInteractor.swift index 1c63fd2ce5..af47fcb119 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStop/ParaStkYieldBoostStopInteractor.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStop/ParaStkYieldBoostStopInteractor.swift @@ -67,8 +67,8 @@ extension ParaStkYieldBoostStopInteractor: ParaStkYieldBoostStopInteractorInputP let notificationClosure: ExtrinsicSubscriptionStatusClosure = { [weak self] result in switch result { - case let .success(status): - if case let .inBlock(extrinsicHash) = status { + case let .success(statusUpdate): + if case let .inBlock(extrinsicHash) = statusUpdate.extrinsicStatus { self?.cancelExtrinsicSubscriptionIfNeeded() self?.confirmPresenter?.didStopAutocompound(with: extrinsicHash) } diff --git a/novawallet/Modules/Staking/Services/BlockTimeEstimationService.swift b/novawallet/Modules/Staking/Services/BlockTimeEstimationService.swift index fe527423f3..c43530a8eb 100644 --- a/novawallet/Modules/Staking/Services/BlockTimeEstimationService.swift +++ b/novawallet/Modules/Staking/Services/BlockTimeEstimationService.swift @@ -187,7 +187,7 @@ final class BlockTimeEstimationService { return } - let storagePaths: [StorageCodingPath] = [.blockNumber, .timestampNow] + let storagePaths: [StorageCodingPath] = [SystemPallet.blockNumberPath, .timestampNow] let optRequests: [UnkeyedSubscriptionRequest]? = try? storagePaths.map { path in let localKey = try localKeyFactory.createFromStoragePath(path, chainId: chainId) return UnkeyedSubscriptionRequest(storagePath: path, localKey: localKey) diff --git a/novawallet/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationInteractor.swift b/novawallet/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationInteractor.swift index 88956f0074..46e7afcc11 100644 --- a/novawallet/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationInteractor.swift +++ b/novawallet/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationInteractor.swift @@ -4,7 +4,7 @@ import BigInt import SoraKeystore final class StakingBondMoreConfirmationInteractor: AccountFetching { - weak var presenter: StakingBondMoreConfirmationOutputProtocol! + weak var presenter: StakingBondMoreConfirmationOutputProtocol? let selectedAccount: ChainAccountResponse let chainAsset: ChainAsset @@ -76,7 +76,7 @@ extension StakingBondMoreConfirmationInteractor: StakingBondMoreConfirmationInte if let priceId = chainAsset.asset.priceId { priceProvider = subscribeToPrice(for: priceId, currency: selectedCurrency) } else { - presenter.didReceivePriceData(result: .success(nil)) + presenter?.didReceivePriceData(result: .success(nil)) } feeProxy.delegate = self @@ -87,7 +87,7 @@ extension StakingBondMoreConfirmationInteractor: StakingBondMoreConfirmationInte let amountValue = amount.toSubstrateAmount( precision: chainAsset.assetDisplayInfo.assetPrecision ) else { - presenter.didReceiveFee(result: .failure(CommonError.undefined)) + presenter?.didReceiveFee(result: .failure(CommonError.undefined)) return } @@ -107,7 +107,7 @@ extension StakingBondMoreConfirmationInteractor: StakingBondMoreConfirmationInte let amountValue = amount.toSubstrateAmount( precision: chainAsset.assetDisplayInfo.assetPrecision ) else { - presenter.didSubmitBonding(result: .failure(CommonError.undefined)) + presenter?.didSubmitBonding(result: .failure(CommonError.undefined)) return } @@ -120,7 +120,7 @@ extension StakingBondMoreConfirmationInteractor: StakingBondMoreConfirmationInte signer: signingWrapper, runningIn: .main, completion: { [weak self] result in - self?.presenter.didSubmitBonding(result: result) + self?.presenter?.didSubmitBonding(result: result) } ) } @@ -131,16 +131,16 @@ extension StakingBondMoreConfirmationInteractor: StakingLocalStorageSubscriber, func handleStashItem(result: Result, for _: AccountAddress) { do { guard let stashItem = try result.get() else { - presenter.didReceiveStashItem(result: .success(nil)) - presenter.didReceiveAccountBalance(result: .success(nil)) - presenter.didReceiveStakingLedger(result: .success(nil)) + presenter?.didReceiveStashItem(result: .success(nil)) + presenter?.didReceiveAccountBalance(result: .success(nil)) + presenter?.didReceiveStakingLedger(result: .success(nil)) return } clear(streamableProvider: &balanceProvider) clear(dataProvider: &ledgerProvider) - presenter.didReceiveStashItem(result: result) + presenter?.didReceiveStashItem(result: result) let stashAccountId = try stashItem.stash.toAccountId(using: chainAsset.chain.chainFormat) let controllerAccountId = try stashItem.controller.toAccountId(using: chainAsset.chain.chainFormat) @@ -168,16 +168,16 @@ extension StakingBondMoreConfirmationInteractor: StakingLocalStorageSubscriber, switch result { case let .success(response): - self?.presenter.didReceiveStash(result: .success(response)) + self?.presenter?.didReceiveStash(result: .success(response)) case let .failure(error): - self?.presenter.didReceiveStash(result: .failure(error)) + self?.presenter?.didReceiveStash(result: .failure(error)) } } } catch { - presenter.didReceiveStashItem(result: .failure(error)) - presenter.didReceiveAccountBalance(result: .failure(error)) - presenter.didReceiveStakingLedger(result: .failure(error)) + presenter?.didReceiveStashItem(result: .failure(error)) + presenter?.didReceiveAccountBalance(result: .failure(error)) + presenter?.didReceiveStakingLedger(result: .failure(error)) } } @@ -186,14 +186,14 @@ extension StakingBondMoreConfirmationInteractor: StakingLocalStorageSubscriber, accountId _: AccountId, chainId _: ChainModel.Id ) { - presenter.didReceiveStakingLedger(result: result) + presenter?.didReceiveStakingLedger(result: result) } } extension StakingBondMoreConfirmationInteractor: PriceLocalStorageSubscriber, PriceLocalSubscriptionHandler { func handlePrice(result: Result, priceId _: AssetModel.PriceId) { - presenter.didReceivePriceData(result: result) + presenter?.didReceivePriceData(result: result) } } @@ -204,13 +204,13 @@ extension StakingBondMoreConfirmationInteractor: WalletLocalStorageSubscriber, W chainId _: ChainModel.Id, assetId _: AssetModel.Id ) { - presenter.didReceiveAccountBalance(result: result) + presenter?.didReceiveAccountBalance(result: result) } } extension StakingBondMoreConfirmationInteractor: ExtrinsicFeeProxyDelegate { func didReceiveFee(result: Result, for _: TransactionFeeId) { - presenter.didReceiveFee(result: result) + presenter?.didReceiveFee(result: result) } } diff --git a/novawallet/Modules/Swaps/Base/Model/AssetHubFeeModelBuilder.swift b/novawallet/Modules/Swaps/Base/Model/AssetHubFeeModelBuilder.swift deleted file mode 100644 index b8dc595591..0000000000 --- a/novawallet/Modules/Swaps/Base/Model/AssetHubFeeModelBuilder.swift +++ /dev/null @@ -1,61 +0,0 @@ -import Foundation -typealias FeeChainAssetId = ChainAssetId - -final class AssetHubFeeModelBuilder { - typealias ResultClosure = (AssetConversion.FeeModel, AssetConversion.CallArgs, FeeChainAssetId?) -> Void - let utilityChainAssetId: ChainAssetId - let resultClosure: ResultClosure - - private(set) var feeAsset: ChainAsset? - - private var recepientUtilityBalance: AssetBalance? - private var feeModel: AssetConversion.FeeModel? - private var callArgs: AssetConversion.CallArgs? - - init( - utilityChainAssetId: ChainAssetId, - resultClosure: @escaping ResultClosure - ) { - self.utilityChainAssetId = utilityChainAssetId - self.resultClosure = resultClosure - } - - private func provideResult() { - guard - let balance = recepientUtilityBalance, - let feeModel = feeModel, - let callArgs = callArgs else { - return - } - - let resultModel: AssetConversion.FeeModel - - if balance.freeInPlank >= feeModel.totalFee.nativeAmount { - // we have enough tokens for ed - need to exchange only network fee - let networkFee = feeModel.networkFee - resultModel = .init(totalFee: networkFee, networkFee: networkFee, networkFeePayer: feeModel.networkFeePayer) - } else { - resultModel = feeModel - } - - resultClosure(resultModel, callArgs, feeAsset?.chainAssetId) - } -} - -extension AssetHubFeeModelBuilder { - func apply(recepientUtilityBalance: AssetBalance) { - self.recepientUtilityBalance = recepientUtilityBalance - provideResult() - } - - func apply(feeModel: AssetConversion.FeeModel, args: AssetConversion.CallArgs) { - self.feeModel = feeModel - callArgs = args - - provideResult() - } - - func apply(feeAsset: ChainAsset) { - self.feeAsset = feeAsset - } -} diff --git a/novawallet/Modules/Swaps/Base/Model/SwapBaseViewModelFactory.swift b/novawallet/Modules/Swaps/Base/Model/SwapBaseViewModelFactory.swift index b0307193e5..0841eb721e 100644 --- a/novawallet/Modules/Swaps/Base/Model/SwapBaseViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Base/Model/SwapBaseViewModelFactory.swift @@ -12,6 +12,10 @@ struct RateParams { protocol SwapBaseViewModelFactoryProtocol { func rateViewModel(from params: RateParams, locale: Locale) -> String + func routeViewModel(from metaOperations: [AssetExchangeMetaOperationProtocol]) -> [SwapRouteItemView.ViewModel] + + func executionTimeViewModel(from timeInterval: TimeInterval, locale: Locale) -> String + func priceDifferenceViewModel( rateParams: RateParams, priceIn: PriceData?, @@ -19,26 +23,28 @@ protocol SwapBaseViewModelFactoryProtocol { locale: Locale ) -> DifferenceViewModel? - func minimalBalanceSwapForFeeMessage( - for networkFeeAddition: AssetConversion.AmountWithNative, - feeChainAsset: ChainAsset, - utilityChainAsset: ChainAsset, - utilityPriceData: PriceData?, + func feeViewModel( + amountInFiat: Decimal, + isEditable: Bool, + currencyId: Int?, locale: Locale - ) -> String + ) -> NetworkFeeInfoViewModel } class SwapBaseViewModelFactory { let balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol let percentForamatter: LocalizableResource let priceDifferenceConfig: SwapPriceDifferenceConfig + let priceAssetInfoFactory: PriceAssetInfoFactoryProtocol init( balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol, + priceAssetInfoFactory: PriceAssetInfoFactoryProtocol, percentForamatter: LocalizableResource, priceDifferenceConfig: SwapPriceDifferenceConfig ) { self.balanceViewModelFactoryFacade = balanceViewModelFactoryFacade + self.priceAssetInfoFactory = priceAssetInfoFactory self.percentForamatter = percentForamatter self.priceDifferenceConfig = priceDifferenceConfig } @@ -72,47 +78,6 @@ extension SwapBaseViewModelFactory: SwapBaseViewModelFactoryProtocol { return amountIn.estimatedEqual(to: amountOut) } - func minimalBalanceSwapForFeeMessage( - for networkFeeAddition: AssetConversion.AmountWithNative, - feeChainAsset: ChainAsset, - utilityChainAsset: ChainAsset, - utilityPriceData: PriceData?, - locale: Locale - ) -> String { - let targetAmount = balanceViewModelFactoryFacade.amountFromValue( - targetAssetInfo: feeChainAsset.assetDisplayInfo, - value: networkFeeAddition.targetAmount.decimal(precision: feeChainAsset.asset.precision) - ).value(for: locale) - - let nativeAmountDecimal = networkFeeAddition.nativeAmount.decimal(precision: utilityChainAsset.asset.precision) - let nativeAmountWithoutPrice = balanceViewModelFactoryFacade.amountFromValue( - targetAssetInfo: utilityChainAsset.assetDisplayInfo, - value: nativeAmountDecimal - ).value(for: locale) - - let nativeAmount: String - - if let priceData = utilityPriceData { - let price = balanceViewModelFactoryFacade.priceFromAmount( - targetAssetInfo: utilityChainAsset.assetDisplayInfo, - amount: nativeAmountDecimal, - priceData: priceData - ).value(for: locale) - - nativeAmount = "\(nativeAmountWithoutPrice) \(price.inParenthesis())" - } else { - nativeAmount = nativeAmountWithoutPrice - } - - return R.string.localizable.swapsPayAssetFeeEdMessage( - feeChainAsset.asset.symbol, - targetAmount, - nativeAmount, - utilityChainAsset.asset.symbol, - preferredLanguages: locale.rLanguages - ) - } - func priceDifferenceViewModel( rateParams params: RateParams, priceIn: PriceData?, @@ -156,4 +121,47 @@ extension SwapBaseViewModelFactory: SwapBaseViewModelFactoryProtocol { return nil } } + + func routeViewModel(from metaOperations: [AssetExchangeMetaOperationProtocol]) -> [SwapRouteItemView.ViewModel] { + let chains = metaOperations.flatMap { operation in + [operation.assetIn.chain, operation.assetOut.chain] + } + + var interchangingChains: [ChainModel] = [] + + for chain in chains where interchangingChains.last?.chainId != chain.chainId { + interchangingChains.append(chain) + } + + return interchangingChains.map { chain in + SwapRouteItemView.ItemViewModel( + title: nil, + icon: ImageViewModelFactory.createChainIconOrDefault(from: chain.icon) + ) + } + } + + func executionTimeViewModel(from timeInterval: TimeInterval, locale: Locale) -> String { + R.string.localizable.commonSecondsFormat( + format: Int(timeInterval.rounded(.up)), + preferredLanguages: locale.rLanguages + ).approximately() + } + + func feeViewModel( + amountInFiat: Decimal, + isEditable: Bool, + currencyId: Int?, + locale: Locale + ) -> NetworkFeeInfoViewModel { + let amount = balanceViewModelFactoryFacade.priceFromFiatAmount( + amountInFiat, + currencyId: currencyId + ).value(for: locale) + + return .init( + isEditable: isEditable, + balanceViewModel: BalanceViewModel(amount: amount, price: nil) + ) + } } diff --git a/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift b/novawallet/Modules/Swaps/Base/Model/SwapDetailsViewModelFactory.swift similarity index 66% rename from novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift rename to novawallet/Modules/Swaps/Base/Model/SwapDetailsViewModelFactory.swift index 415d4e8946..d62a5086db 100644 --- a/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Base/Model/SwapDetailsViewModelFactory.swift @@ -2,7 +2,7 @@ import Foundation import SoraFoundation import BigInt -protocol SwapConfirmViewModelFactoryProtocol: SwapBaseViewModelFactoryProtocol { +protocol SwapDetailsViewModelFactoryProtocol: SwapBaseViewModelFactoryProtocol { func assetViewModel( chainAsset: ChainAsset, amount: BigUInt, @@ -12,23 +12,17 @@ protocol SwapConfirmViewModelFactoryProtocol: SwapBaseViewModelFactoryProtocol { func slippageViewModel(slippage: BigRational, locale: Locale) -> String - func feeViewModel( - fee: BigUInt, - chainAsset: ChainAsset, - priceData: PriceData?, - locale: Locale - ) -> NetworkFeeInfoViewModel - - func walletViewModel(walletAddress: WalletDisplayAddress) -> WalletAccountViewModel? + func walletViewModel(metaAccountResponse: MetaChainAccountResponse) -> WalletAccountViewModel? } -final class SwapConfirmViewModelFactory: SwapBaseViewModelFactory { +final class SwapDetailsViewModelFactory: SwapBaseViewModelFactory { let walletViewModelFactory = WalletAccountViewModelFactory() let networkViewModelFactory: NetworkViewModelFactoryProtocol let assetIconViewModelFactory: AssetIconViewModelFactoryProtocol init( balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol, + priceAssetInfoFactory: PriceAssetInfoFactoryProtocol, networkViewModelFactory: NetworkViewModelFactoryProtocol, assetIconViewModelFactory: AssetIconViewModelFactoryProtocol, percentForamatter: LocalizableResource, @@ -39,13 +33,14 @@ final class SwapConfirmViewModelFactory: SwapBaseViewModelFactory { super.init( balanceViewModelFactoryFacade: balanceViewModelFactoryFacade, + priceAssetInfoFactory: priceAssetInfoFactory, percentForamatter: percentForamatter, priceDifferenceConfig: priceDifferenceConfig ) } } -extension SwapConfirmViewModelFactory: SwapConfirmViewModelFactoryProtocol { +extension SwapDetailsViewModelFactory: SwapDetailsViewModelFactoryProtocol { func assetViewModel( chainAsset: ChainAsset, amount: BigUInt, @@ -77,26 +72,7 @@ extension SwapConfirmViewModelFactory: SwapConfirmViewModelFactoryProtocol { slippage.decimalValue.map { percentForamatter.value(for: locale).stringFromDecimal($0) ?? "" } ?? "" } - func feeViewModel( - fee: BigUInt, - chainAsset: ChainAsset, - priceData: PriceData?, - locale: Locale - ) -> NetworkFeeInfoViewModel { - let amountDecimal = Decimal.fromSubstrateAmount( - fee, - precision: chainAsset.assetDisplayInfo.assetPrecision - ) ?? 0 - let balanceViewModel = balanceViewModelFactoryFacade.balanceFromPrice( - targetAssetInfo: chainAsset.assetDisplayInfo, - amount: amountDecimal, - priceData: priceData - ).value(for: locale) - - return .init(isEditable: false, balanceViewModel: balanceViewModel) - } - - func walletViewModel(walletAddress: WalletDisplayAddress) -> WalletAccountViewModel? { - try? walletViewModelFactory.createViewModel(from: walletAddress) + func walletViewModel(metaAccountResponse: MetaChainAccountResponse) -> WalletAccountViewModel? { + try? walletViewModelFactory.createViewModel(from: metaAccountResponse) } } diff --git a/novawallet/Modules/Swaps/Base/Model/SwapInterEDNotMet.swift b/novawallet/Modules/Swaps/Base/Model/SwapInterEDNotMet.swift new file mode 100644 index 0000000000..f028a6adb9 --- /dev/null +++ b/novawallet/Modules/Swaps/Base/Model/SwapInterEDNotMet.swift @@ -0,0 +1,8 @@ +import Foundation + +typealias SwapInterEDCheckClosure = (SwapInterEDNotMet?) -> Void + +struct SwapInterEDNotMet { + let operationIndex: Int + let minBalanceResult: Result +} diff --git a/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift b/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift index 8817383c20..b3615485cf 100644 --- a/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift +++ b/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift @@ -3,8 +3,9 @@ import Foundation struct SwapMaxModel { let payChainAsset: ChainAsset? let feeChainAsset: ChainAsset? + let receiveChainAsset: ChainAsset? let balance: AssetBalance? - let feeModel: AssetConversion.FeeModel? + let feeModel: AssetExchangeFee? let payAssetExistense: AssetBalanceExistence? let receiveAssetExistense: AssetBalanceExistence? let accountInfo: AccountInfo? @@ -12,21 +13,42 @@ struct SwapMaxModel { func minBalanceCoveredByFrozen(in balance: AssetBalance) -> Bool { let minBalance = payAssetExistense?.minBalance ?? 0 - return balance.transferable + minBalance <= balance.freeInPlank + return balance.transferable + minBalance <= balance.balanceCountingEd } - var shouldKeepMinBalance: Bool { - guard payChainAsset?.isUtilityAsset == true else { + var needMinBalanceDueToReceiveInsufficiency: Bool { + guard + let payChainAsset, + payChainAsset.isUtilityAsset, + payChainAsset.chain.chainId == receiveChainAsset?.chain.chainId, + let receiveAssetExistense = receiveAssetExistense + else { return false } - guard let receiveAssetExistense = receiveAssetExistense else { + let hasConsumers = (accountInfo?.hasConsumers ?? false) + + return (!receiveAssetExistense.isSelfSufficient || hasConsumers) + } + + var needMinBalanceDueConsumers: Bool { + accountInfo?.hasConsumers ?? false + } + + var needMinBalanceDueToPostsubmissionFee: Bool { + guard + let payChainAsset, payChainAsset.isUtilityAsset, + let feeModel else { return false } - let hasConsumers = (accountInfo?.hasConsumers ?? false) + return feeModel.hasOriginPostSubmissionByAccount + } - return (!receiveAssetExistense.isSelfSufficient || hasConsumers) + var shouldKeepMinBalance: Bool { + needMinBalanceDueConsumers || + needMinBalanceDueToPostsubmissionFee || + needMinBalanceDueToReceiveInsufficiency } private func calculateForNativeAsset(_ payChainAsset: ChainAsset, balance: AssetBalance) -> Decimal { @@ -38,7 +60,7 @@ struct SwapMaxModel { } if let feeModel = feeModel { - let fee = feeModel.totalFee.targetAmount + let fee = feeModel.totalFeeInAssetIn(payChainAsset) maxAmount = maxAmount.subtractOrZero(fee) } @@ -46,11 +68,16 @@ struct SwapMaxModel { } private func calculateForCustomAsset(_ payChainAsset: ChainAsset, balance: AssetBalance) -> Decimal { - guard let feeModel = feeModel, payChainAsset.chainAssetId == feeChainAsset?.chainAssetId else { + guard let feeModel = feeModel else { return balance.transferable.decimal(precision: payChainAsset.asset.precision) } - let fee = feeModel.totalFee.targetAmount + let fee: Balance = if payChainAsset.chainAssetId == feeChainAsset?.chainAssetId { + feeModel.totalFeeInAssetIn(payChainAsset) + } else { + feeModel.postSubmissionFeeInAssetIn(payChainAsset) + } + let maxAmount = balance.transferable.subtractOrZero(fee) return maxAmount.decimal(precision: payChainAsset.asset.precision) diff --git a/novawallet/Modules/Swaps/Base/SwapAssetSelectionClosure.swift b/novawallet/Modules/Swaps/Base/SwapAssetSelectionClosure.swift new file mode 100644 index 0000000000..c672a8d42e --- /dev/null +++ b/novawallet/Modules/Swaps/Base/SwapAssetSelectionClosure.swift @@ -0,0 +1,3 @@ +import Foundation + +typealias SwapAssetSelectionClosure = (ChainAsset, SwapTokensFlowStateProtocol) -> Void diff --git a/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift index 41eae47b36..a73425cac9 100644 --- a/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift +++ b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift @@ -5,120 +5,117 @@ import BigInt class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapBaseInteractorInputProtocol { weak var basePresenter: SwapBaseInteractorOutputProtocol? - let flowState: AssetConversionFlowFacadeProtocol - let assetConversionAggregator: AssetConversionAggregationFactoryProtocol + let assetsExchangeService: AssetsExchangeServiceProtocol let chainRegistry: ChainRegistryProtocol let assetStorageFactory: AssetStorageInfoOperationFactoryProtocol - let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol + let generalLocalSubscriptionFactory: GeneralStorageSubscriptionFactoryProtocol let currencyManager: CurrencyManagerProtocol let selectedWallet: MetaAccountModel let operationQueue: OperationQueue + let logger: LoggerProtocol - var generalLocalSubscriptionFactory: GeneralStorageSubscriptionFactoryProtocol { - flowState.generalSubscriptonFactory - } - - private var quoteCall = CancellableCallStore() + private var quoteCallStore = CancellableCallStore() + private var feeCallStore = CancellableCallStore() - private var assetConversionFeeService: AssetConversionFeeServiceProtocol? - private var priceProviders: [ChainAssetId: StreamableProvider] = [:] private var assetBalanceProviders: [ChainAssetId: StreamableProvider] = [:] - private var feeModelBuilder: AssetHubFeeModelBuilder? private var accountInfoProvider: AnyDataProvider? - var currentChain: ChainModel? - init( - flowState: AssetConversionFlowFacadeProtocol, - assetConversionAggregator: AssetConversionAggregationFactoryProtocol, + state: SwapTokensFlowStateProtocol, chainRegistry: ChainRegistryProtocol, assetStorageFactory: AssetStorageInfoOperationFactoryProtocol, - priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, currencyManager: CurrencyManagerProtocol, selectedWallet: MetaAccountModel, - operationQueue: OperationQueue + operationQueue: OperationQueue, + logger: LoggerProtocol ) { - self.flowState = flowState - self.assetConversionAggregator = assetConversionAggregator + assetsExchangeService = state.setupAssetExchangeService() self.chainRegistry = chainRegistry self.assetStorageFactory = assetStorageFactory - self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory + generalLocalSubscriptionFactory = state.generalLocalSubscriptionFactory self.currencyManager = currencyManager self.selectedWallet = selectedWallet self.operationQueue = operationQueue + self.logger = logger } deinit { - quoteCall.cancel() + quoteCallStore.cancel() + feeCallStore.cancel() } - private func provideAssetBalanceExistense(for chainAsset: ChainAsset) { - guard let runtimeService = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId) else { - let error = ChainRegistryError.runtimeMetadaUnavailable - basePresenter?.didReceive(baseError: .assetBalanceExistense(error, chainAsset)) - return - } + private func fetchAssetBalanceExistence( + for chainAssetIds: Set, + completion: @escaping (Result<[ChainAssetId: AssetBalanceExistence], Error>) -> Void + ) { + do { + let chainAssetIdList = Array(chainAssetIds) + + let wrappers = try chainAssetIdList.map { chainAssetId in + let chain = try chainRegistry.getChainOrError(for: chainAssetId.chainId) + let runtimeService = try chainRegistry.getRuntimeProviderOrError(for: chainAssetId.chainId) + let chainAsset = try chain.chainAssetOrError(for: chainAssetId.assetId) + + return assetStorageFactory.createAssetBalanceExistenceOperation( + chainId: chainAsset.chain.chainId, + asset: chainAsset.asset, + runtimeProvider: runtimeService, + operationQueue: operationQueue + ) + } - let wrapper = assetStorageFactory.createAssetBalanceExistenceOperation( - chainId: chainAsset.chain.chainId, - asset: chainAsset.asset, - runtimeProvider: runtimeService, - operationQueue: operationQueue - ) + let mappingOperation = ClosureOperation<[ChainAssetId: AssetBalanceExistence]> { + let balanceExistences = try wrappers.map { try $0.targetOperation.extractNoCancellableResultData() } - execute( - wrapper: wrapper, - inOperationQueue: operationQueue, - runningCallbackIn: .main - ) { [weak self] result in + return zip(chainAssetIds, balanceExistences).reduce( + into: [ChainAssetId: AssetBalanceExistence]() + ) { + $0[$1.0] = $1.1 + } + } + + wrappers.forEach { mappingOperation.addDependency($0.targetOperation) } + + let dependencies = wrappers.flatMap(\.allOperations) + + let totalWrapper = CompoundOperationWrapper(targetOperation: mappingOperation, dependencies: dependencies) + + execute( + wrapper: totalWrapper, + inOperationQueue: operationQueue, + runningCallbackIn: .main + ) { result in + completion(result) + } + + } catch { + completion(.failure(error)) + } + } + + private func provideAssetBalanceExistenses(for chainAsset: ChainAsset) { + fetchAssetBalanceExistence(for: [chainAsset.chainAssetId]) { [weak self] result in switch result { - case let .success(existense): + case let .success(existenses): + guard let existense = existenses[chainAsset.chainAssetId] else { + return + } + self?.basePresenter?.didReceiveAssetBalance( existense: existense, chainAssetId: chainAsset.chainAssetId ) case let .failure(error): self?.basePresenter?.didReceive( - baseError: .assetBalanceExistense(error, chainAsset) + baseError: .assetBalanceExistence(error, chainAsset) ) } } } - func updateFeeModelBuilder(for chain: ChainModel) { - guard - let utilityAsset = chain.utilityChainAsset(), - feeModelBuilder?.utilityChainAssetId != utilityAsset.chainAssetId else { - return - } - - feeModelBuilder = AssetHubFeeModelBuilder( - utilityChainAssetId: utilityAsset.chainAssetId - ) { [weak self] feeModel, callArgs, feeChainAssetId in - self?.basePresenter?.didReceive( - fee: feeModel, - transactionFeeId: callArgs.identifier, - feeChainAssetId: feeChainAssetId - ) - } - - assetBalanceProviders[utilityAsset.chainAssetId] = assetBalanceSubscription(chainAsset: utilityAsset) - } - - func updateChain(with newChain: ChainModel) { - let oldChainId = currentChain?.chainId - currentChain = newChain - - if newChain.chainId != oldChainId { - assetConversionFeeService = try? flowState.createFeeService(for: newChain) - - updateAccountInfoProvider(for: newChain) - } - } - func updateAccountInfoProvider(for chain: ChainModel) { clear(dataProvider: &accountInfoProvider) @@ -129,17 +126,16 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB accountInfoProvider = subscribeAccountInfo(for: accountId, chainId: chain.chainId) } - func updateSubscriptions(activeChainAssets: Set) { - priceProviders = clear(providers: priceProviders, activeChainAssets: activeChainAssets) - assetBalanceProviders = clear(providers: assetBalanceProviders, activeChainAssets: activeChainAssets) + func clearSubscriptionsByAssets(_ activeChainAssets: Set) { + assetBalanceProviders = clear(providers: assetBalanceProviders, activeIds: activeChainAssets) } - func clear( - providers: [ChainAssetId: StreamableProvider], - activeChainAssets: Set - ) -> [ChainAssetId: StreamableProvider] { - providers.reduce(into: [ChainAssetId: StreamableProvider]()) { - if !activeChainAssets.contains($1.key) { + func clear( + providers: [K: StreamableProvider], + activeIds: Set + ) -> [K: StreamableProvider] { + providers.reduce(into: [K: StreamableProvider]()) { + if !activeIds.contains($1.key) { $1.value.removeObserver(self) } else { $0[$1.key] = $1.value @@ -147,17 +143,6 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB } } - func priceSubscription(chainAsset: ChainAsset) -> StreamableProvider? { - guard let priceId = chainAsset.asset.priceId else { - return nil - } - - return priceProviders[chainAsset.chainAssetId] ?? subscribeToPrice( - for: priceId, - currency: currencyManager.selectedCurrency - ) - } - func assetBalanceSubscription(chainAsset: ChainAsset) -> StreamableProvider? { guard let accountId = chainAccountResponse(for: chainAsset)?.accountId else { return nil @@ -171,18 +156,14 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB } func quote(args: AssetConversion.QuoteArgs) { - quoteCall.cancel() - - guard let chain = currentChain, let state = try? flowState.setup(for: chain) else { - return - } + quoteCallStore.cancel() - let wrapper = assetConversionAggregator.createQuoteWrapper(for: state, args: args) + let wrapper = assetsExchangeService.fetchQuoteWrapper(for: args) executeCancellable( wrapper: wrapper, inOperationQueue: operationQueue, - backingCallIn: quoteCall, + backingCallIn: quoteCallStore, runningCallbackIn: .main ) { [weak self] result in switch result { @@ -199,22 +180,36 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB // by default we always request quote manually } - func fee(args: AssetConversion.CallArgs) { - guard let feeAsset = feeModelBuilder?.feeAsset else { - return - } + func performUpdateOnGraphChange() { + logger.debug("Asset Exchange graph did change") + } + + func fee(route: AssetExchangeRoute, slippage: BigRational, feeAsset: ChainAsset) { + feeCallStore.cancel() + + let args = AssetExchangeFeeArgs( + route: route, + slippage: slippage, + feeAssetId: feeAsset.chainAssetId + ) - assetConversionFeeService?.calculate( - in: feeAsset, - callArgs: args, - runCompletionIn: .main + let wrapper = assetsExchangeService.estimateFee(for: args) + + executeCancellable( + wrapper: wrapper, + inOperationQueue: operationQueue, + backingCallIn: feeCallStore, + runningCallbackIn: .main ) { [weak self] result in switch result { - case let .success(feeModel): - self?.feeModelBuilder?.apply(feeModel: feeModel, args: args) + case let .success(fee): + self?.basePresenter?.didReceive( + fee: fee, + feeChainAssetId: fee.feeAssetId + ) case let .failure(error): self?.basePresenter?.didReceive( - baseError: .fetchFeeFailed(error, args.identifier, feeAsset.chainAssetId) + baseError: .fetchFeeFailed(error, feeAsset.chainAssetId) ) } } @@ -225,92 +220,59 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB return metaChainAccountResponse?.chainAccount } - func set(receiveChainAsset chainAsset: ChainAsset) { - updateChain(with: chainAsset.chain) + func setReceiveChainAssetSubscriptions(_ chainAsset: ChainAsset) { + provideAssetBalanceExistenses(for: chainAsset) - updateFeeModelBuilder(for: chainAsset.chain) - - provideAssetBalanceExistense(for: chainAsset) - - priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) assetBalanceProviders[chainAsset.chainAssetId] = assetBalanceSubscription(chainAsset: chainAsset) } - func set(payChainAsset chainAsset: ChainAsset) { - updateChain(with: chainAsset.chain) + func setPayChainAssetSubscriptions(_ chainAsset: ChainAsset) { + updateAccountInfoProvider(for: chainAsset.chain) - updateFeeModelBuilder(for: chainAsset.chain) - - if let utilityAsset = chainAsset.chain.utilityChainAsset() { - feeModelBuilder?.apply(feeAsset: utilityAsset) - } + provideAssetBalanceExistenses(for: chainAsset) - provideAssetBalanceExistense(for: chainAsset) - - priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) assetBalanceProviders[chainAsset.chainAssetId] = assetBalanceSubscription(chainAsset: chainAsset) } - func set(feeChainAsset chainAsset: ChainAsset) { - updateFeeModelBuilder(for: chainAsset.chain) - feeModelBuilder?.apply(feeAsset: chainAsset) + func setFeeChainAssetSubscriptions(_ chainAsset: ChainAsset) { + assetBalanceProviders[chainAsset.chainAssetId] = assetBalanceSubscription(chainAsset: chainAsset) - provideAssetBalanceExistense(for: chainAsset) + provideAssetBalanceExistenses(for: chainAsset) - if let utilityAsset = chainAsset.chain.utilityChainAsset(), !chainAsset.isUtilityAsset { - provideAssetBalanceExistense(for: utilityAsset) - } + // we still may need utility asset to pay fee on the origin chain + if + let utilityChainAsset = chainAsset.chain.utilityChainAsset(), + utilityChainAsset.chainAssetId != chainAsset.chainAssetId { + assetBalanceProviders[chainAsset.chainAssetId] = assetBalanceSubscription(chainAsset: utilityChainAsset) - priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) - assetBalanceProviders[chainAsset.chainAssetId] = assetBalanceSubscription(chainAsset: chainAsset) + provideAssetBalanceExistenses(for: chainAsset) + } } // MARK: - SwapBaseInteractorInputProtocol - func setup() {} + func setup() { + assetsExchangeService.subscribeUpdates( + for: self, + notifyingIn: .main + ) { [weak self] in + self?.performUpdateOnGraphChange() + } + } func calculateQuote(for args: AssetConversion.QuoteArgs) { quote(args: args) } - func calculateFee( - args: AssetConversion.CallArgs - ) { - fee(args: args) - } - - func retryAssetBalanceSubscription(for chainAsset: ChainAsset) { - clear(streamableProvider: &assetBalanceProviders[chainAsset.chainAssetId]) - assetBalanceProviders[chainAsset.chainAssetId] = assetBalanceSubscription(chainAsset: chainAsset) - } - - func remakePriceSubscription(for chainAsset: ChainAsset) { - clear(streamableProvider: &priceProviders[chainAsset.chainAssetId]) - priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) - } - - func retryAssetBalanceExistenseFetch(for chainAsset: ChainAsset) { - provideAssetBalanceExistense(for: chainAsset) - } - - func retryAccountInfoSubscription() { - guard let chain = currentChain else { - return - } - - updateAccountInfoProvider(for: chain) + func calculateFee(for route: AssetExchangeRoute, slippage: BigRational, feeAsset: ChainAsset) { + fee(route: route, slippage: slippage, feeAsset: feeAsset) } func requestValidatingQuote( for args: AssetConversion.QuoteArgs, - completion: @escaping (Result) -> Void + completion: @escaping (Result) -> Void ) { - guard let chain = currentChain, let state = try? flowState.setup(for: chain) else { - completion(.failure(ChainRegistryError.connectionUnavailable)) - return - } - - let wrapper = assetConversionAggregator.createQuoteWrapper(for: state, args: args) + let wrapper = assetsExchangeService.fetchQuoteWrapper(for: args) execute( wrapper: wrapper, @@ -319,16 +281,49 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB callbackClosure: completion ) } -} -extension SwapBaseInteractor: PriceLocalStorageSubscriber, PriceLocalSubscriptionHandler { - func handlePrice(result: Result, priceId: AssetModel.PriceId) { - switch result { - case let .success(priceData): - basePresenter?.didReceive(price: priceData, priceId: priceId) - case let .failure(error): - basePresenter?.didReceive(baseError: .price(error, priceId)) + func requestValidatingIntermediateED( + for operations: [AssetExchangeMetaOperationProtocol], + completion: @escaping SwapInterEDCheckClosure + ) { + guard !operations.isEmpty else { + completion(nil) + return } + + let assetOutIds = operations.map(\.assetOut.chainAssetId) + + fetchAssetBalanceExistence(for: Set(assetOutIds)) { result in + switch result { + case let .success(edMapping): + for (index, operation) in operations.enumerated() { + let minBalance = edMapping[operation.assetOut.chainAssetId]?.minBalance ?? 0 + + if operation.amountOut < minBalance { + let checkValue = SwapInterEDNotMet( + operationIndex: index, + minBalanceResult: .success(minBalance) + ) + + completion(checkValue) + return + } + } + + completion(nil) + case let .failure(error): + let checkValue = SwapInterEDNotMet( + operationIndex: 0, + minBalanceResult: .failure(error) + ) + + completion(checkValue) + } + } + } + + func retryAssetBalanceExistenseFetch(for chainAsset: ChainAsset) { + provideAssetBalanceExistenses(for: chainAsset) } } @@ -347,14 +342,10 @@ extension SwapBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscript accountId: accountId ) - if feeModelBuilder?.utilityChainAssetId == chainAssetId { - feeModelBuilder?.apply(recepientUtilityBalance: balance) - } - basePresenter?.didReceive(balance: balance, for: chainAssetId) case let .failure(error): - basePresenter?.didReceive(baseError: .assetBalance(error, chainAssetId, accountId)) + logger.error("Unexpected balance error: \(error)") } } } @@ -369,7 +360,7 @@ extension SwapBaseInteractor: GeneralLocalStorageSubscriber, GeneralLocalStorage case let .success(accountInfo): basePresenter?.didReceive(accountInfo: accountInfo, chainId: chainId) case let .failure(error): - basePresenter?.didReceive(baseError: .accountInfo(error)) + logger.error("Unexpected account info error: \(error)") } } } diff --git a/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift b/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift index ba8e9f19e8..cca2f57851 100644 --- a/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift +++ b/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift @@ -4,16 +4,7 @@ class SwapBasePresenter { let logger: LoggerProtocol let selectedWallet: MetaAccountModel let dataValidatingFactory: SwapDataValidatorFactoryProtocol - - init( - selectedWallet: MetaAccountModel, - dataValidatingFactory: SwapDataValidatorFactoryProtocol, - logger: LoggerProtocol - ) { - self.selectedWallet = selectedWallet - self.dataValidatingFactory = dataValidatingFactory - self.logger = logger - } + let priceStore: AssetExchangePriceStoring private(set) var balances: [ChainAssetId: AssetBalance] = [:] @@ -37,18 +28,16 @@ class SwapBasePresenter { return balances[utilityAssetId] } - private(set) var prices: [ChainAssetId: PriceData] = [:] - var payAssetPriceData: PriceData? { - getPayChainAsset().flatMap { prices[$0.chainAssetId] } + getPayChainAsset().flatMap { priceStore.fetchPrice(for: $0.chainAssetId) } } var receiveAssetPriceData: PriceData? { - getReceiveChainAsset().flatMap { prices[$0.chainAssetId] } + getReceiveChainAsset().flatMap { priceStore.fetchPrice(for: $0.chainAssetId) } } var feeAssetPriceData: PriceData? { - getFeeChainAsset().flatMap { prices[$0.chainAssetId] } + getFeeChainAsset().flatMap { priceStore.fetchPrice(for: $0.chainAssetId) } } var assetBalanceExistences: [ChainAssetId: AssetBalanceExistence] = [:] @@ -71,10 +60,10 @@ class SwapBasePresenter { } } - var fee: AssetConversion.FeeModel? - var quoteResult: Result? + var fee: AssetExchangeFee? + var quoteResult: Result? - var quote: AssetConversion.Quote? { + var quote: AssetExchangeQuote? { switch quoteResult { case let .success(quote): return quote @@ -85,6 +74,18 @@ class SwapBasePresenter { var accountInfo: AccountInfo? + init( + selectedWallet: MetaAccountModel, + dataValidatingFactory: SwapDataValidatorFactoryProtocol, + priceStore: AssetExchangePriceStoring, + logger: LoggerProtocol + ) { + self.selectedWallet = selectedWallet + self.dataValidatingFactory = dataValidatingFactory + self.priceStore = priceStore + self.logger = logger + } + func getSwapModel() -> SwapModel? { guard let payChainAsset = getPayChainAsset(), @@ -115,10 +116,11 @@ class SwapBasePresenter { ) } - func getMaxModel() -> SwapMaxModel? { + func getMaxModel() -> SwapMaxModel { .init( payChainAsset: getPayChainAsset(), feeChainAsset: getFeeChainAsset(), + receiveChainAsset: getReceiveChainAsset(), balance: payAssetBalance, feeModel: fee, payAssetExistense: payAssetBalanceExistense, @@ -151,11 +153,7 @@ class SwapBasePresenter { fatalError("Must be implemented by parent class") } - func shouldHandleQuote(for _: AssetConversion.QuoteArgs?) -> Bool { - fatalError("Must be implemented by parent class") - } - - func shouldHandleFee(for _: TransactionFeeId, feeChainAssetId _: ChainAssetId?) -> Bool { + func shouldHandleRoute(for _: AssetConversion.QuoteArgs?) -> Bool { fatalError("Must be implemented by parent class") } @@ -169,15 +167,14 @@ class SwapBasePresenter { func handleBaseError(_: SwapBaseError) {} - func handleNewQuote(_: AssetConversion.Quote, for _: AssetConversion.QuoteArgs) {} + func handleNewQuote(_: AssetExchangeQuote, for _: AssetConversion.QuoteArgs) {} func handleNewFee( - _: AssetConversion.FeeModel?, - transactionFeeId _: TransactionFeeId, + _: AssetExchangeFee?, feeChainAssetId _: ChainAssetId? ) {} - func handleNewPrice(_: PriceData?, chainAssetId _: ChainAssetId) {} + func handleNewPrice(_: PriceData?, priceId _: AssetModel.PriceId) {} func handleNewBalance(_: AssetBalance?, for _: ChainAssetId) {} @@ -196,47 +193,19 @@ class SwapBasePresenter { switch error { case let .quote(error, args): - guard shouldHandleQuote(for: args) else { + guard shouldHandleRoute(for: args) else { return } quoteResult = .failure(error) - case let .fetchFeeFailed(_, id, feeChainAssetId): - guard shouldHandleFee(for: id, feeChainAssetId: feeChainAssetId) else { - return - } - + case .fetchFeeFailed: wireframe.presentRequestStatus(on: view, locale: locale) { [weak self] in self?.estimateFee() } - case let .price(_, priceId): - wireframe.presentRequestStatus(on: view, locale: locale) { [weak self] in - guard let self = self else { - return - } - [self.getPayChainAsset(), self.getReceiveChainAsset(), self.getFeeChainAsset()] - .compactMap { $0 } - .filter { $0.asset.priceId == priceId } - .forEach(interactor.remakePriceSubscription) - } - case let .assetBalance(_, chainAssetId, _): - wireframe.presentRequestStatus(on: view, locale: locale) { [weak self] in - guard let self = self else { - return - } - [self.getPayChainAsset(), self.getReceiveChainAsset(), self.getFeeChainAsset()] - .compactMap { $0 } - .filter { $0.chainAssetId == chainAssetId } - .forEach(interactor.retryAssetBalanceSubscription) - } - case let .assetBalanceExistense(_, chainAsset): + case let .assetBalanceExistence(_, chainAsset): wireframe.presentRequestStatus(on: view, locale: locale) { interactor.retryAssetBalanceExistenseFetch(for: chainAsset) } - case .accountInfo: - wireframe.presentRequestStatus(on: view, locale: locale) { - interactor.retryAccountInfoSubscription() - } } } @@ -245,9 +214,9 @@ class SwapBasePresenter { interactor: SwapBaseInteractorInputProtocol, locale: Locale ) -> [DataValidating] { - [ + var baseValidations = [ dataValidatingFactory.has( - fee: swapModel.feeModel?.extrinsicFee, + fee: swapModel.feeModel?.originExtrinsicFee(), locale: locale ) { [weak self] in self?.estimateFee() @@ -260,7 +229,7 @@ class SwapBasePresenter { locale: locale ), dataValidatingFactory.notViolatingMinBalancePaying( - fee: swapModel.feeChainAsset.isUtilityAsset ? swapModel.feeModel?.extrinsicFee : nil, + fee: swapModel.feeChainAsset.isUtilityAsset ? swapModel.feeModel?.originExtrinsicFee() : nil, total: swapModel.utilityAssetBalance?.balanceCountingEd, minBalance: swapModel.feeChainAsset.isUtilityAsset ? swapModel.utilityAssetExistense?.minBalance : 0, asset: swapModel.utilityChainAsset?.assetDisplayInfo ?? swapModel.feeChainAsset.assetDisplayInfo, @@ -273,64 +242,72 @@ class SwapBasePresenter { self?.applySwapMax() }, locale: locale - ), - dataValidatingFactory.passesRealtimeQuoteValidation( + ) + ] + + // for last operation validation is covered by canReceive + if let operations = swapModel.quote?.metaOperations, operations.count > 1 { + let intermediateEdValidation = dataValidatingFactory.passesIntermediateEDValidation( params: swapModel, - remoteValidatingClosure: { args, completion in - interactor.requestValidatingQuote(for: args, completion: completion) - }, - onQuoteUpdate: { [weak self] quote in - self?.quoteResult = .success(quote) - self?.handleNewQuote(quote, for: swapModel.quoteArgs) + remoteValidatingClosure: { closureParams in + interactor.requestValidatingIntermediateED( + for: closureParams.operations.dropLast(), + completion: closureParams.completionClosure + ) }, locale: locale ) - ] + + baseValidations.append(intermediateEdValidation) + } + + let quoteValidation = dataValidatingFactory.passesRealtimeQuoteValidation( + params: swapModel, + remoteValidatingClosure: { args, completion in + interactor.requestValidatingQuote(for: args, completion: completion) + }, + onQuoteUpdate: { [weak self] quote in + self?.quoteResult = .success(quote) + self?.handleNewQuote(quote, for: swapModel.quoteArgs) + }, + locale: locale + ) + + baseValidations.append(quoteValidation) + + return baseValidations } } extension SwapBasePresenter: SwapBaseInteractorOutputProtocol { - func didReceive(quote: AssetConversion.Quote, for quoteArgs: AssetConversion.QuoteArgs) { - guard shouldHandleQuote(for: quoteArgs), self.quote != quote else { + func didReceive(quote: AssetExchangeQuote, for quoteArgs: AssetConversion.QuoteArgs) { + guard shouldHandleRoute(for: quoteArgs) else { return } + logger.debug("New quote: \(quote)") + quoteResult = .success(quote) handleNewQuote(quote, for: quoteArgs) } - func didReceive( - fee: AssetConversion.FeeModel?, - transactionFeeId: TransactionFeeId, - feeChainAssetId: ChainAssetId? - ) { - guard shouldHandleFee(for: transactionFeeId, feeChainAssetId: feeChainAssetId), self.fee != fee else { + func didReceive(fee: AssetExchangeFee, feeChainAssetId: ChainAssetId?) { + guard self.fee != fee else { return } + logger.debug("Did receive new fee: \(fee)") + self.fee = fee - handleNewFee(fee, transactionFeeId: transactionFeeId, feeChainAssetId: feeChainAssetId) + handleNewFee(fee, feeChainAssetId: feeChainAssetId) } func didReceive(baseError: SwapBaseError) { - handleBaseError(baseError) - } - - func didReceive(price: PriceData?, priceId: AssetModel.PriceId) { - let optChainAssetId = [getPayChainAsset(), getReceiveChainAsset(), getFeeChainAsset()] - .compactMap { $0 } - .filter { $0.asset.priceId == priceId } - .first?.chainAssetId + logger.error("Did receive error: \(baseError)") - guard let chainAssetId = optChainAssetId, prices[chainAssetId] != price else { - return - } - - prices[chainAssetId] = price - - handleNewPrice(price, chainAssetId: chainAssetId) + handleBaseError(baseError) } func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId) { @@ -338,6 +315,8 @@ extension SwapBasePresenter: SwapBaseInteractorOutputProtocol { return } + logger.debug("New balance: \(String(describing: balance))") + balances[chainAsset] = balance handleNewBalance(balance, for: chainAsset) @@ -348,6 +327,8 @@ extension SwapBasePresenter: SwapBaseInteractorOutputProtocol { return } + logger.debug("New balance existence: \(existense)") + assetBalanceExistences[chainAssetId] = existense handleNewBalanceExistense(existense, chainAssetId: chainAssetId) diff --git a/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift b/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift index 744127e863..ba1c6d2e97 100644 --- a/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift +++ b/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift @@ -3,22 +3,24 @@ import BigInt protocol SwapBaseInteractorInputProtocol: AnyObject { func setup() func calculateQuote(for args: AssetConversion.QuoteArgs) - func calculateFee(args: AssetConversion.CallArgs) - func remakePriceSubscription(for chainAsset: ChainAsset) - func retryAssetBalanceSubscription(for chainAsset: ChainAsset) + func calculateFee(for route: AssetExchangeRoute, slippage: BigRational, feeAsset: ChainAsset) func retryAssetBalanceExistenseFetch(for chainAsset: ChainAsset) - func retryAccountInfoSubscription() + func requestValidatingQuote( for args: AssetConversion.QuoteArgs, - completion: @escaping (Result) -> Void + completion: @escaping (Result) -> Void + ) + + func requestValidatingIntermediateED( + for operations: [AssetExchangeMetaOperationProtocol], + completion: @escaping SwapInterEDCheckClosure ) } protocol SwapBaseInteractorOutputProtocol: AnyObject { - func didReceive(quote: AssetConversion.Quote, for quoteArgs: AssetConversion.QuoteArgs) - func didReceive(fee: AssetConversion.FeeModel?, transactionFeeId: TransactionFeeId, feeChainAssetId: ChainAssetId?) + func didReceive(quote: AssetExchangeQuote, for quoteArgs: AssetConversion.QuoteArgs) + func didReceive(fee: AssetExchangeFee, feeChainAssetId: ChainAssetId?) func didReceive(baseError: SwapBaseError) - func didReceive(price: PriceData?, priceId: AssetModel.PriceId) func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId) func didReceiveAssetBalance(existense: AssetBalanceExistence, chainAssetId: ChainAssetId) func didReceive(accountInfo: AccountInfo?, chainId: ChainModel.Id) @@ -29,9 +31,6 @@ protocol SwapBaseWireframeProtocol: AnyObject, SwapErrorPresentable, AlertPresen enum SwapBaseError: Error { case quote(Error, AssetConversion.QuoteArgs) - case fetchFeeFailed(Error, TransactionFeeId, FeeChainAssetId?) - case price(Error, AssetModel.PriceId) - case assetBalance(Error, ChainAssetId, AccountId) - case assetBalanceExistense(Error, ChainAsset) - case accountInfo(Error) + case fetchFeeFailed(Error, ChainAssetId?) + case assetBalanceExistence(Error, ChainAsset) } diff --git a/novawallet/Modules/Swaps/Base/View/SwapInfoView.swift b/novawallet/Modules/Swaps/Base/View/SwapInfoView.swift index 2bf82caa1a..241d06c6b4 100644 --- a/novawallet/Modules/Swaps/Base/View/SwapInfoView.swift +++ b/novawallet/Modules/Swaps/Base/View/SwapInfoView.swift @@ -1,13 +1,18 @@ import UIKit import SoraUI -final class SwapInfoView: GenericTitleValueView, SkeletonableView { +class SwapGenericInfoView: GenericTitleValueView, SkeletonableView { var titleButton: RoundedButton { titleView } - var valueLabel: UILabel { valueView } var skeletonView: SkrullableView? private var isLoading: Bool = false + var selectable: Bool = true { + didSet { + applySelectable() + } + } + override func layoutSubviews() { super.layoutSubviews() @@ -30,35 +35,23 @@ final class SwapInfoView: GenericTitleValueView, Skeleto fatalError("init(coder:) has not been implemented") } - private func configure() { + func configure() { titleButton.applyIconStyle() - titleButton.imageWithTitleView?.iconImage = R.image.iconInfoFilled() titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() titleButton.imageWithTitleView?.titleFont = .regularFootnote titleButton.imageWithTitleView?.spacingBetweenLabelAndIcon = 4 titleButton.imageWithTitleView?.layoutType = .horizontalLabelFirst titleButton.contentInsets = .init(top: 8, left: 0, bottom: 8, right: 0) - valueLabel.textColor = R.color.colorTextPrimary() - valueLabel.font = .regularFootnote + applySelectable() } -} -extension SwapInfoView { - func bind(loadableViewModel: LoadableViewModelState) { - switch loadableViewModel { - case let .cached(value), let .loaded(value): - stopLoadingIfNeeded() - isLoading = false - valueView.text = value - case .loading: - startLoadingIfNeeded() - isLoading = true - } + func applySelectable() { + titleButton.imageWithTitleView?.iconImage = selectable ? R.image.iconInfoFilled() : nil } } -extension SwapInfoView { +extension SwapGenericInfoView { func createSkeletons(for spaceSize: CGSize) -> [Skeletonable] { let size = CGSize(width: 68, height: 8) let offset = CGPoint( @@ -93,3 +86,24 @@ extension SwapInfoView { isLoading = false } } + +final class SwapInfoView: SwapGenericInfoView { + var valueLabel: UILabel { valueView } + + override func configure() { + super.configure() + + valueLabel.textColor = R.color.colorTextPrimary() + valueLabel.font = .regularFootnote + } + + func bind(loadableViewModel: LoadableViewModelState) { + switch loadableViewModel { + case let .cached(value), let .loaded(value): + stopLoadingIfNeeded() + valueView.text = value + case .loading: + startLoadingIfNeeded() + } + } +} diff --git a/novawallet/Modules/Swaps/Base/View/SwapRouteViewCell.swift b/novawallet/Modules/Swaps/Base/View/SwapRouteViewCell.swift new file mode 100644 index 0000000000..b43f8d9f8f --- /dev/null +++ b/novawallet/Modules/Swaps/Base/View/SwapRouteViewCell.swift @@ -0,0 +1,25 @@ +import UIKit +import SoraUI + +final class SwapRouteViewCell: RowView>, StackTableViewCellProtocol { + var titleButton: RoundedButton { rowContentView.titleView } + var routeView: SwapRouteView { rowContentView.valueView } + + var itemStyle: SwapRouteItemView.Style = .init(iconSize: 16) + var separatorStyle: SwapRouteSeparatorView.Style = R.image.iconForward() + + func bind(loadableRouteViewModel: LoadableViewModelState<[SwapRouteItemView.ItemViewModel]>) { + switch loadableRouteViewModel { + case let .cached(value), let .loaded(value): + rowContentView.stopLoadingIfNeeded() + + routeView.bind( + items: value, + itemStyle: itemStyle, + separatorStyle: separatorStyle + ) + case .loading: + rowContentView.startLoadingIfNeeded() + } + } +} diff --git a/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmInitState.swift b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmInitState.swift index ec4d9d2251..a59a56c3e8 100644 --- a/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmInitState.swift +++ b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmInitState.swift @@ -3,6 +3,6 @@ struct SwapConfirmInitState { let chainAssetOut: ChainAsset let feeChainAsset: ChainAsset let slippage: BigRational - let quote: AssetConversion.Quote + let quote: AssetExchangeQuote let quoteArgs: AssetConversion.QuoteArgs } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift index c4e9e4205c..b4b636762c 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift @@ -4,119 +4,40 @@ import IrohaCrypto import BigInt final class SwapConfirmInteractor: SwapBaseInteractor { - var presenter: SwapConfirmInteractorOutputProtocol? { - basePresenter as? SwapConfirmInteractorOutputProtocol - } - - let persistExtrinsicService: PersistentExtrinsicServiceProtocol - let eventCenter: EventCenterProtocol let initState: SwapConfirmInitState - let assetConversionExtrinsicService: AssetConversionExtrinsicServiceProtocol - let signer: SigningWrapperProtocol - let callPathFactory: AssetConversionCallPathFactoryProtocol init( - flowState: AssetConversionFlowFacadeProtocol, + state: SwapTokensFlowStateProtocol, initState: SwapConfirmInitState, - assetConversionAggregator: AssetConversionAggregationFactoryProtocol, - assetConversionExtrinsicService: AssetConversionExtrinsicServiceProtocol, chainRegistry: ChainRegistryProtocol, assetStorageFactory: AssetStorageInfoOperationFactoryProtocol, - priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, - persistExtrinsicService: PersistentExtrinsicServiceProtocol, - eventCenter: EventCenterProtocol, currencyManager: CurrencyManagerProtocol, selectedWallet: MetaAccountModel, operationQueue: OperationQueue, - signer: SigningWrapperProtocol, - callPathFactory: AssetConversionCallPathFactoryProtocol + logger: LoggerProtocol ) { self.initState = initState - self.signer = signer - self.assetConversionExtrinsicService = assetConversionExtrinsicService - self.persistExtrinsicService = persistExtrinsicService - self.eventCenter = eventCenter - self.callPathFactory = callPathFactory super.init( - flowState: flowState, - assetConversionAggregator: assetConversionAggregator, + state: state, chainRegistry: chainRegistry, assetStorageFactory: assetStorageFactory, - priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, currencyManager: currencyManager, selectedWallet: selectedWallet, - operationQueue: operationQueue + operationQueue: operationQueue, + logger: logger ) } - private func persistSwapAndComplete(txHash: String, args: AssetConversion.CallArgs, lastFee: BigUInt?) { - do { - let chainIn = initState.chainAssetIn.chain - - guard let sender = selectedWallet.fetch(for: chainIn.accountRequest())?.toAddress() else { - throw ChainAccountFetchingError.accountNotExists - } - - let receiver = try args.receiver.toAddress(using: initState.chainAssetOut.chain.chainFormat) - - let details = PersistSwapDetails( - txHash: try Data(hexString: txHash), - sender: sender, - receiver: receiver, - assetIdIn: args.assetIn, - amountIn: args.amountIn, - assetIdOut: args.assetOut, - amountOut: args.amountOut, - fee: lastFee, - feeAssetId: initState.feeChainAsset.asset.assetId, - callPath: callPathFactory.createHistoryCallPath(for: args) - ) - - persistExtrinsicService.saveSwap( - source: .substrate, - chainAssetId: details.assetIdIn, - details: details, - runningIn: .main - ) { [weak self] _ in - self?.eventCenter.notify(with: WalletTransactionListUpdated()) - self?.presenter?.didReceiveConfirmation(hash: txHash) - } - } catch { - // complete successfully as we don't want a user to think tx is failed - presenter?.didReceiveConfirmation(hash: txHash) - } - } - override func setup() { super.setup() - set(payChainAsset: initState.chainAssetIn) - set(receiveChainAsset: initState.chainAssetOut) - set(feeChainAsset: initState.feeChainAsset) + setPayChainAssetSubscriptions(initState.chainAssetIn) + setReceiveChainAssetSubscriptions(initState.chainAssetOut) + setFeeChainAssetSubscriptions(initState.feeChainAsset) } } -extension SwapConfirmInteractor: SwapConfirmInteractorInputProtocol { - func submit(args: AssetConversion.CallArgs, lastFee: BigUInt?) { - assetConversionExtrinsicService.submit( - callArgs: args, - feeAsset: initState.feeChainAsset, - signer: signer, - runCompletionIn: .main - ) { [weak self] result in - switch result { - case let .success(hash): - self?.persistSwapAndComplete( - txHash: hash, - args: args, - lastFee: lastFee - ) - case let .failure(error): - self?.presenter?.didReceive(error: .submit(error)) - } - } - } -} +extension SwapConfirmInteractor: SwapConfirmInteractorInputProtocol {} diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index ed6a216721..2e8d0738bb 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -13,7 +13,7 @@ final class SwapConfirmPresenter: SwapBasePresenter { selectedWallet.fetch(for: initState.chainAssetOut.chain.accountRequest())?.accountId } - private var viewModelFactory: SwapConfirmViewModelFactoryProtocol + private var viewModelFactory: SwapDetailsViewModelFactoryProtocol private var quoteArgs: AssetConversion.QuoteArgs @@ -22,7 +22,8 @@ final class SwapConfirmPresenter: SwapBasePresenter { wireframe: SwapConfirmWireframeProtocol, initState: SwapConfirmInitState, selectedWallet: MetaAccountModel, - viewModelFactory: SwapConfirmViewModelFactoryProtocol, + viewModelFactory: SwapDetailsViewModelFactoryProtocol, + priceStore: AssetExchangePriceStoring, slippageBounds: SlippageBounds, dataValidatingFactory: SwapDataValidatorFactoryProtocol, localizationManager: LocalizationManagerProtocol, @@ -38,6 +39,7 @@ final class SwapConfirmPresenter: SwapBasePresenter { super.init( selectedWallet: selectedWallet, dataValidatingFactory: dataValidatingFactory, + priceStore: priceStore, logger: logger ) @@ -46,7 +48,7 @@ final class SwapConfirmPresenter: SwapBasePresenter { } override func getSpendingInputAmount() -> Decimal? { - quote?.amountIn.decimal(precision: initState.chainAssetIn.asset.precision) + quote?.route.amountIn.decimal(precision: initState.chainAssetIn.asset.precision) } override func getQuoteArgs() -> AssetConversion.QuoteArgs? { @@ -69,19 +71,12 @@ final class SwapConfirmPresenter: SwapBasePresenter { initState.feeChainAsset } - override func shouldHandleQuote(for quoteArgs: AssetConversion.QuoteArgs?) -> Bool { - self.quoteArgs == quoteArgs - } - - override func shouldHandleFee(for _: TransactionFeeId, feeChainAssetId _: ChainAssetId?) -> Bool { - true + override func shouldHandleRoute(for _: AssetConversion.QuoteArgs?) -> Bool { + quoteArgs == quoteArgs } override func estimateFee() { - guard let quote = quote, - let accountId = selectedWallet.fetch( - for: initState.chainAssetOut.chain.accountRequest() - )?.accountId else { + guard let quote else { return } @@ -89,22 +84,16 @@ final class SwapConfirmPresenter: SwapBasePresenter { provideFeeViewModel() interactor.calculateFee( - args: .init( - assetIn: quote.assetIn, - amountIn: quote.amountIn, - assetOut: quote.assetOut, - amountOut: quote.amountOut, - receiver: accountId, - direction: quoteArgs.direction, - slippage: initState.slippage, - context: quote.context - ) + for: quote.route, + slippage: initState.slippage, + feeAsset: initState.feeChainAsset ) } override func applySwapMax() { + let maxAmount = getMaxModel().calculate() + guard - let maxAmount = getMaxModel()?.calculate(), maxAmount > 0, let maxAmountInPlank = maxAmount.toSubstrateAmount( precision: initState.chainAssetIn.assetDisplayInfo.assetPrecision @@ -138,8 +127,9 @@ final class SwapConfirmPresenter: SwapBasePresenter { } } - override func handleNewQuote(_ quote: AssetConversion.Quote, for _: AssetConversion.QuoteArgs) { + override func handleNewQuote(_ quote: AssetExchangeQuote, for _: AssetConversion.QuoteArgs) { quoteResult = .success(quote) + fee = nil view?.didReceiveStopLoading() @@ -148,26 +138,25 @@ final class SwapConfirmPresenter: SwapBasePresenter { } override func handleNewFee( - _: AssetConversion.FeeModel?, - transactionFeeId _: TransactionFeeId, + _: AssetExchangeFee?, feeChainAssetId _: ChainAssetId? ) { + provideRouteViewModel() provideFeeViewModel() - provideNotificationViewModel() } - override func handleNewPrice(_: PriceData?, chainAssetId: ChainAssetId) { - if initState.chainAssetIn.chainAssetId == chainAssetId { + override func handleNewPrice(_: PriceData?, priceId: AssetModel.PriceId) { + if initState.chainAssetIn.asset.priceId == priceId { provideAssetInViewModel() providePriceDifferenceViewModel() } - if initState.chainAssetOut.chainAssetId == chainAssetId { + if initState.chainAssetOut.asset.priceId == priceId { provideAssetOutViewModel() providePriceDifferenceViewModel() } - if initState.feeChainAsset.chainAssetId == chainAssetId { + if initState.feeChainAsset.asset.priceId == priceId { provideFeeViewModel() } } @@ -175,13 +164,13 @@ final class SwapConfirmPresenter: SwapBasePresenter { extension SwapConfirmPresenter { private func provideAssetInViewModel() { - guard let quote = quote else { + guard let quote else { return } let viewModel = viewModelFactory.assetViewModel( chainAsset: initState.chainAssetIn, - amount: quote.amountIn, + amount: quote.route.amountIn, priceData: payAssetPriceData, locale: selectedLocale ) @@ -190,12 +179,12 @@ extension SwapConfirmPresenter { } private func provideAssetOutViewModel() { - guard let quote = quote else { + guard let quote else { return } let viewModel = viewModelFactory.assetViewModel( chainAsset: initState.chainAssetOut, - amount: quote.amountOut, + amount: quote.route.amountOut, priceData: receiveAssetPriceData, locale: selectedLocale ) @@ -203,7 +192,7 @@ extension SwapConfirmPresenter { } private func provideRateViewModel() { - guard let quote = quote else { + guard let quote else { view?.didReceiveRate(viewModel: .loading) return } @@ -211,16 +200,41 @@ extension SwapConfirmPresenter { let params = RateParams( assetDisplayInfoIn: initState.chainAssetIn.assetDisplayInfo, assetDisplayInfoOut: initState.chainAssetOut.assetDisplayInfo, - amountIn: quote.amountIn, - amountOut: quote.amountOut + amountIn: quote.route.amountIn, + amountOut: quote.route.amountOut ) let viewModel = viewModelFactory.rateViewModel(from: params, locale: selectedLocale) view?.didReceiveRate(viewModel: .loaded(value: viewModel)) } + private func provideRouteViewModel() { + guard let quote, fee != nil else { + view?.didReceiveRoute(viewModel: .loading) + return + } + + let viewModel = viewModelFactory.routeViewModel(from: quote.metaOperations) + + view?.didReceiveRoute(viewModel: .loaded(value: viewModel)) + } + + private func provideExecutionTimeViewModel() { + guard let quote else { + view?.didReceiveExecutionTime(viewModel: .loading) + return + } + + let viewModel = viewModelFactory.executionTimeViewModel( + from: quote.totalExecutionTime(), + locale: selectedLocale + ) + + view?.didReceiveExecutionTime(viewModel: .loaded(value: viewModel)) + } + private func providePriceDifferenceViewModel() { - guard let quote = quote else { + guard let quote else { view?.didReceivePriceDifference(viewModel: .loading) return } @@ -228,8 +242,8 @@ extension SwapConfirmPresenter { let params = RateParams( assetDisplayInfoIn: initState.chainAssetIn.assetDisplayInfo, assetDisplayInfoOut: initState.chainAssetOut.assetDisplayInfo, - amountIn: quote.amountIn, - amountOut: quote.amountOut + amountIn: quote.route.amountIn, + amountOut: quote.route.amountOut ) if let viewModel = viewModelFactory.priceDifferenceViewModel( @@ -251,36 +265,18 @@ extension SwapConfirmPresenter { view?.didReceiveWarning(viewModel: warning) } - private func provideNotificationViewModel() { - guard - let networkFeeAddition = fee?.networkNativeFeeAddition, - !initState.feeChainAsset.isUtilityAsset, - let utilityChainAsset = initState.feeChainAsset.chain.utilityChainAsset() else { - view?.didReceiveNotification(viewModel: nil) - return - } - - let message = viewModelFactory.minimalBalanceSwapForFeeMessage( - for: networkFeeAddition, - feeChainAsset: initState.feeChainAsset, - utilityChainAsset: utilityChainAsset, - utilityPriceData: prices[utilityChainAsset.chainAssetId], - locale: selectedLocale - ) - - view?.didReceiveNotification(viewModel: message) - } - private func provideFeeViewModel() { - guard let fee = fee else { + guard + let operations = quote?.metaOperations, + let fee = fee?.calculateTotalFeeInFiat(matching: operations, priceStore: priceStore) else { view?.didReceiveNetworkFee(viewModel: .loading) return } let viewModel = viewModelFactory.feeViewModel( - fee: fee.networkFee.targetAmount, - chainAsset: initState.feeChainAsset, - priceData: feeAssetPriceData, + amountInFiat: fee, + isEditable: false, + currencyId: feeAssetPriceData?.currencyId, locale: selectedLocale ) @@ -294,11 +290,7 @@ extension SwapConfirmPresenter { return } - guard let walletAddress = WalletDisplayAddress(response: chainAccountResponse) else { - view?.didReceiveWallet(viewModel: nil) - return - } - let viewModel = viewModelFactory.walletViewModel(walletAddress: walletAddress) + let viewModel = viewModelFactory.walletViewModel(metaAccountResponse: chainAccountResponse) view?.didReceiveWallet(viewModel: viewModel) } @@ -307,32 +299,28 @@ extension SwapConfirmPresenter { provideAssetInViewModel() provideAssetOutViewModel() provideRateViewModel() + provideRouteViewModel() + provideExecutionTimeViewModel() providePriceDifferenceViewModel() provideSlippageViewModel() - provideNotificationViewModel() provideFeeViewModel() provideWalletViewModel() } private func submit() { - guard let quote = quote, let accountId = accountId else { + guard let fee, let quote else { return } - let args = AssetConversion.CallArgs( - assetIn: quote.assetIn, - amountIn: quote.amountIn, - assetOut: quote.assetOut, - amountOut: quote.amountOut, - receiver: accountId, - direction: initState.quoteArgs.direction, - slippage: initState.slippage, - context: quote.context + let executionModel = SwapExecutionModel( + chainAssetIn: initState.chainAssetIn, + chainAssetOut: initState.chainAssetOut, + feeAsset: initState.feeChainAsset, + quote: quote, + fee: fee ) - view?.didReceiveStartLoading() - - interactor.submit(args: args, lastFee: fee?.networkFee.targetAmount) + wireframe.showSwapExecution(from: view, model: executionModel) } } @@ -370,7 +358,15 @@ extension SwapConfirmPresenter: SwapConfirmPresenterProtocol { } func showNetworkFeeInfo() { - wireframe.showFeeInfo(from: view) + guard let fee, let quote else { + return + } + + wireframe.showFeeDetails( + from: view, + operations: quote.metaOperations, + fee: fee + ) } func showAddressOptions() { @@ -390,6 +386,18 @@ extension SwapConfirmPresenter: SwapConfirmPresenterProtocol { ) } + func showRouteDetails() { + guard let fee, let quote else { + return + } + + wireframe.showRouteDetails( + from: view, + quote: quote, + fee: fee + ) + } + func confirm() { guard let swapModel = getSwapModel() else { return @@ -415,35 +423,6 @@ extension SwapConfirmPresenter: SwapConfirmPresenterProtocol { } } -extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { - func didReceive(error: SwapConfirmError) { - view?.didReceiveStopLoading() - switch error { - case let .submit(error): - wireframe.handleExtrinsicSigningErrorPresentationElseDefault( - error, - view: view, - closeAction: .dismiss, - locale: selectedLocale, - completionClosure: nil - ) - } - } - - func didReceiveConfirmation(hash _: String) { - view?.didReceiveStopLoading() - - guard let payChainAsset = getPayChainAsset() else { - return - } - wireframe.complete( - on: view, - payChainAsset: payChainAsset, - locale: selectedLocale - ) - } -} - extension SwapConfirmPresenter: Localizable { func applyLocalization() { if view?.isSetup == true { diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift index 6503259cbf..403bebf773 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift @@ -5,12 +5,13 @@ protocol SwapConfirmViewProtocol: ControllerBackedProtocol { func didReceiveAssetIn(viewModel: SwapAssetAmountViewModel) func didReceiveAssetOut(viewModel: SwapAssetAmountViewModel) func didReceiveRate(viewModel: LoadableViewModelState) + func didReceiveRoute(viewModel: LoadableViewModelState<[SwapRouteItemView.ItemViewModel]>) + func didReceiveExecutionTime(viewModel: LoadableViewModelState) func didReceivePriceDifference(viewModel: LoadableViewModelState?) func didReceiveSlippage(viewModel: String) func didReceiveNetworkFee(viewModel: LoadableViewModelState) func didReceiveWallet(viewModel: WalletAccountViewModel?) func didReceiveWarning(viewModel: String?) - func didReceiveNotification(viewModel: String?) func didReceiveStartLoading() func didReceiveStopLoading() } @@ -21,28 +22,29 @@ protocol SwapConfirmPresenterProtocol: AnyObject { func showPriceDifferenceInfo() func showSlippageInfo() func showNetworkFeeInfo() + func showRouteDetails() func showAddressOptions() func confirm() } -protocol SwapConfirmInteractorInputProtocol: SwapBaseInteractorInputProtocol { - func submit(args: AssetConversion.CallArgs, lastFee: BigUInt?) -} - -protocol SwapConfirmInteractorOutputProtocol: SwapBaseInteractorOutputProtocol { - func didReceiveConfirmation(hash: String) - func didReceive(error: SwapConfirmError) -} +protocol SwapConfirmInteractorInputProtocol: SwapBaseInteractorInputProtocol {} protocol SwapConfirmWireframeProtocol: SwapBaseWireframeProtocol, AddressOptionsPresentable, - ShortTextInfoPresentable, ModalAlertPresenting, MessageSheetPresentable, ExtrinsicSigningErrorHandling { - func complete( - on view: ControllerBackedProtocol?, - payChainAsset: ChainAsset, - locale: Locale + ShortTextInfoPresentable, MessageSheetPresentable, ExtrinsicSigningErrorHandling { + func showSwapExecution( + from view: SwapConfirmViewProtocol?, + model: SwapExecutionModel ) -} -enum SwapConfirmError: Error { - case submit(Error) + func showRouteDetails( + from view: ControllerBackedProtocol?, + quote: AssetExchangeQuote, + fee: AssetExchangeFee + ) + + func showFeeDetails( + from view: ControllerBackedProtocol?, + operations: [AssetExchangeMetaOperationProtocol], + fee: AssetExchangeFee + ) } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift index 8bba6f0954..5d41756a5e 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift @@ -42,6 +42,7 @@ final class SwapConfirmViewController: UIViewController, ViewHolder { private func setupHandlers() { rootView.rateCell.addTarget(self, action: #selector(rateAction), for: .touchUpInside) + rootView.routeCell.addTarget(self, action: #selector(routeAction), for: .touchUpInside) rootView.priceDifferenceCell.addTarget(self, action: #selector(priceDifferenceAction), for: .touchUpInside) rootView.slippageCell.addTarget(self, action: #selector(slippageAction), for: .touchUpInside) rootView.networkFeeCell.addTarget(self, action: #selector(networkFeeAction), for: .touchUpInside) @@ -65,6 +66,10 @@ final class SwapConfirmViewController: UIViewController, ViewHolder { presenter.showNetworkFeeInfo() } + @objc private func routeAction() { + presenter.showRouteDetails() + } + @objc private func addressAction() { presenter.showAddressOptions() } @@ -87,6 +92,14 @@ extension SwapConfirmViewController: SwapConfirmViewProtocol { rootView.rateCell.bind(loadableViewModel: viewModel) } + func didReceiveRoute(viewModel: LoadableViewModelState<[SwapRouteItemView.ItemViewModel]>) { + rootView.routeCell.bind(loadableRouteViewModel: viewModel) + } + + func didReceiveExecutionTime(viewModel: LoadableViewModelState) { + rootView.execTimeCell.bind(loadableViewModel: viewModel) + } + func didReceivePriceDifference(viewModel: LoadableViewModelState?) { if let viewModel = viewModel { rootView.priceDifferenceCell.isHidden = false @@ -125,10 +138,6 @@ extension SwapConfirmViewController: SwapConfirmViewProtocol { rootView.set(warning: viewModel) } - func didReceiveNotification(viewModel: String?) { - rootView.set(notification: viewModel) - } - func didReceiveStartLoading() { rootView.loadableActionView.startLoading() } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift index d9f73d6c5f..064e2c6cf0 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift @@ -5,7 +5,7 @@ import Operation_iOS struct SwapConfirmViewFactory { static func createView( initState: SwapConfirmInitState, - flowState: AssetConversionFlowFacadeProtocol, + flowState: SwapTokensFlowStateProtocol, completionClosure: SwapCompletionClosure? ) -> SwapConfirmViewProtocol? { guard let currencyManager = CurrencyManager.shared, let wallet = SelectedWalletSettings.shared.value else { @@ -20,14 +20,16 @@ struct SwapConfirmViewFactory { return nil } - let wireframe = SwapConfirmWireframe(completionClosure: completionClosure) + let wireframe = SwapConfirmWireframe(flowState: flowState, completionClosure: completionClosure) + let priceAssetInfoFactory = PriceAssetInfoFactory(currencyManager: currencyManager) let balanceViewModelFactoryFacade = BalanceViewModelFactoryFacade( - priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: currencyManager) + priceAssetInfoFactory: priceAssetInfoFactory ) - let viewModelFactory = SwapConfirmViewModelFactory( + let viewModelFactory = SwapDetailsViewModelFactory( balanceViewModelFactoryFacade: balanceViewModelFactoryFacade, + priceAssetInfoFactory: priceAssetInfoFactory, networkViewModelFactory: NetworkViewModelFactory(), assetIconViewModelFactory: AssetIconViewModelFactory(), percentForamatter: NumberFormatter.percentSingle.localizableResource(), @@ -45,6 +47,7 @@ struct SwapConfirmViewFactory { initState: initState, selectedWallet: wallet, viewModelFactory: viewModelFactory, + priceStore: flowState.priceStore, slippageBounds: .init(config: SlippageConfig.defaultConfig), dataValidatingFactory: dataValidatingFactory, localizationManager: LocalizationManager.shared, @@ -66,71 +69,31 @@ struct SwapConfirmViewFactory { private static func createInteractor( wallet: MetaAccountModel, initState: SwapConfirmInitState, - flowState: AssetConversionFlowFacadeProtocol + flowState: SwapTokensFlowStateProtocol ) -> SwapConfirmInteractor? { let chainRegistry = ChainRegistryFacade.sharedRegistry - let accountRequest = initState.chainAssetIn.chain.accountRequest() - let chain = initState.chainAssetIn.chain - - guard let connection = chainRegistry.getConnection(for: chain.chainId), - let runtimeService = chainRegistry.getRuntimeProvider(for: chain.chainId), - let currencyManager = CurrencyManager.shared, - let selectedAccount = wallet.fetchMetaChainAccount(for: accountRequest) else { + guard let currencyManager = CurrencyManager.shared else { return nil } let operationQueue = OperationManagerFacade.sharedDefaultQueue - let assetConversionAggregator = AssetConversionAggregationFactory( - chainRegistry: chainRegistry, - operationQueue: operationQueue - ) - - let extrinsicServiceFactory = ExtrinsicServiceFactory( - runtimeRegistry: runtimeService, - engine: connection, - operationQueue: OperationManagerFacade.sharedDefaultQueue, - userStorageFacade: UserDataStorageFacade.shared, - substrateStorageFacade: SubstrateDataStorageFacade.shared - ) - - guard let extrinsicService = try? flowState.createExtrinsicService(for: chain) else { - return nil - } - - let signingWrapper = SigningWrapperFactory().createSigningWrapper( - for: selectedAccount.metaId, - accountResponse: selectedAccount.chainAccount - ) - let assetStorageFactory = AssetStorageInfoOperationFactory( chainRegistry: chainRegistry, operationQueue: operationQueue ) - let transactionStorage = SubstrateRepositoryFactory().createTxRepository() - let persistExtrinsicService = PersistentExtrinsicService( - repository: transactionStorage, - operationQueue: OperationManagerFacade.sharedDefaultQueue - ) - let interactor = SwapConfirmInteractor( - flowState: flowState, + state: flowState, initState: initState, - assetConversionAggregator: assetConversionAggregator, - assetConversionExtrinsicService: extrinsicService, chainRegistry: chainRegistry, assetStorageFactory: assetStorageFactory, - priceLocalSubscriptionFactory: PriceProviderFactory.shared, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, - persistExtrinsicService: persistExtrinsicService, - eventCenter: EventCenter.shared, currencyManager: currencyManager, selectedWallet: wallet, operationQueue: operationQueue, - signer: signingWrapper, - callPathFactory: AssetHubCallPathFactory() + logger: Logger.shared ) return interactor diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmWireframe.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmWireframe.swift index ec59831f95..99a27b7920 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmWireframe.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmWireframe.swift @@ -1,25 +1,72 @@ import Foundation final class SwapConfirmWireframe: SwapConfirmWireframeProtocol { + let flowState: SwapTokensFlowStateProtocol let completionClosure: SwapCompletionClosure? - init(completionClosure: SwapCompletionClosure?) { + init( + flowState: SwapTokensFlowStateProtocol, + completionClosure: SwapCompletionClosure? + ) { + self.flowState = flowState self.completionClosure = completionClosure } - func complete( - on view: ControllerBackedProtocol?, - payChainAsset: ChainAsset, - locale: Locale + func showSwapExecution( + from view: SwapConfirmViewProtocol?, + model: SwapExecutionModel ) { - let title = R.string.localizable - .commonTransactionSubmitted(preferredLanguages: locale.rLanguages) + guard + let swapExecutionView = SwapExecutionViewFactory.createView( + for: model, + flowState: flowState, + completionClosure: completionClosure + ) else { + return + } let presenter = view?.controller.navigationController?.presentingViewController presenter?.dismiss(animated: true) { - self.completionClosure?(payChainAsset) - self.presentSuccessNotification(title, from: presenter, completion: nil) + presenter?.present(swapExecutionView.controller, animated: true) + } + } + + func showRouteDetails( + from view: ControllerBackedProtocol?, + quote: AssetExchangeQuote, + fee: AssetExchangeFee + ) { + guard + let routeDetailsView = SwapRouteDetailsViewFactory.createView( + for: quote, + fee: fee, + state: flowState + ) else { + return + } + + let navigationController = NovaNavigationController(rootViewController: routeDetailsView.controller) + + view?.controller.present(navigationController, animated: true) + } + + func showFeeDetails( + from view: ControllerBackedProtocol?, + operations: [AssetExchangeMetaOperationProtocol], + fee: AssetExchangeFee + ) { + guard + let routeDetailsView = SwapFeeDetailsViewFactory.createView( + for: operations, + fee: fee, + state: flowState + ) else { + return } + + let navigationController = NovaNavigationController(rootViewController: routeDetailsView.controller) + + view?.controller.present(navigationController, animated: true) } } diff --git a/novawallet/Modules/Swaps/Confirm/View/SwapConfirmViewLayout.swift b/novawallet/Modules/Swaps/Confirm/View/SwapConfirmViewLayout.swift index 21bf6effa2..5bc8bef9df 100644 --- a/novawallet/Modules/Swaps/Confirm/View/SwapConfirmViewLayout.swift +++ b/novawallet/Modules/Swaps/Confirm/View/SwapConfirmViewLayout.swift @@ -25,6 +25,18 @@ final class SwapConfirmViewLayout: ScrollableContainerLayoutView { $0.titleButton.imageWithTitleView?.titleFont = .regularFootnote } + let routeCell: SwapRouteViewCell = .create { + $0.titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() + $0.titleButton.imageWithTitleView?.titleFont = .regularFootnote + } + + let execTimeCell: SwapInfoViewCell = .create { + $0.titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() + $0.titleButton.imageWithTitleView?.titleFont = .regularFootnote + $0.rowContentView.selectable = false + $0.isUserInteractionEnabled = false + } + let networkFeeCell = SwapNetworkFeeViewCell() let walletTableView: StackTableView = .create { @@ -41,8 +53,6 @@ final class SwapConfirmViewLayout: ScrollableContainerLayoutView { private var warningView: InlineAlertView? - private var notificationView: InlineAlertView? - let loadableActionView = LoadableActionView() override func setupStyle() { @@ -58,6 +68,8 @@ final class SwapConfirmViewLayout: ScrollableContainerLayoutView { detailsTableView.addArrangedSubview(rateCell) detailsTableView.addArrangedSubview(priceDifferenceCell) detailsTableView.addArrangedSubview(slippageCell) + detailsTableView.addArrangedSubview(routeCell) + detailsTableView.addArrangedSubview(execTimeCell) detailsTableView.addArrangedSubview(networkFeeCell) addArrangedSubview(walletTableView, spacingAfter: 8) @@ -70,21 +82,36 @@ final class SwapConfirmViewLayout: ScrollableContainerLayoutView { make.bottom.equalTo(safeAreaLayoutGuide).inset(UIConstants.actionBottomInset) make.height.equalTo(UIConstants.actionHeight) } + + containerView.scrollBottomOffset = safeAreaInsets.bottom + UIConstants.actionBottomInset + + UIConstants.actionHeight + 8 } func setup(locale: Locale) { - slippageCell.titleButton.imageWithTitleView?.title = R.string.localizable.swapsSetupSlippage( - preferredLanguages: locale.rLanguages + slippageCell.titleButton.setTitle( + R.string.localizable.swapsSetupSlippage( + preferredLanguages: locale.rLanguages + ) ) - priceDifferenceCell.titleButton.imageWithTitleView?.title = R.string.localizable.swapsSetupPriceDifference( - preferredLanguages: locale.rLanguages + priceDifferenceCell.titleButton.setTitle( + R.string.localizable.swapsSetupPriceDifference( + preferredLanguages: locale.rLanguages + ) + ) + rateCell.titleButton.setTitle( + R.string.localizable.swapsSetupDetailsRate( + preferredLanguages: locale.rLanguages + ) + ) + routeCell.titleButton.setTitle( + R.string.localizable.swapsDetailsRoute(preferredLanguages: locale.rLanguages) + ) + execTimeCell.titleButton.setTitle( + R.string.localizable.swapsDetailsExecTime(preferredLanguages: locale.rLanguages) + ) + networkFeeCell.titleButton.setTitle( + R.string.localizable.swapsDetailsTotalFee(preferredLanguages: locale.rLanguages) ) - rateCell.titleButton.imageWithTitleView?.title = R.string.localizable.swapsSetupDetailsRate( - preferredLanguages: locale.rLanguages) - networkFeeCell.titleButton.imageWithTitleView?.title = R.string.localizable.commonNetworkFee( - preferredLanguages: locale.rLanguages) - rateCell.titleButton.invalidateLayout() - networkFeeCell.titleButton.invalidateLayout() walletCell.titleLabel.text = R.string.localizable.commonWallet( preferredLanguages: locale.rLanguages) @@ -103,13 +130,4 @@ final class SwapConfirmViewLayout: ScrollableContainerLayoutView { spacing: 8 ) } - - func set(notification: String?) { - applyInfo( - on: ¬ificationView, - after: warningView ?? walletTableView, - text: notification, - spacing: 8 - ) - } } diff --git a/novawallet/Modules/Swaps/Execution/Model/SwapExecutionModel.swift b/novawallet/Modules/Swaps/Execution/Model/SwapExecutionModel.swift new file mode 100644 index 0000000000..d44cb009f6 --- /dev/null +++ b/novawallet/Modules/Swaps/Execution/Model/SwapExecutionModel.swift @@ -0,0 +1,9 @@ +import Foundation + +struct SwapExecutionModel { + let chainAssetIn: ChainAsset + let chainAssetOut: ChainAsset + let feeAsset: ChainAsset + let quote: AssetExchangeQuote + let fee: AssetExchangeFee +} diff --git a/novawallet/Modules/Swaps/Execution/Model/SwapExecutionState.swift b/novawallet/Modules/Swaps/Execution/Model/SwapExecutionState.swift new file mode 100644 index 0000000000..6456d8a88e --- /dev/null +++ b/novawallet/Modules/Swaps/Execution/Model/SwapExecutionState.swift @@ -0,0 +1,8 @@ +import Foundation +import SoraFoundation + +enum SwapExecutionState { + case inProgress(Int) + case completed(Date) + case failed(Int, Date) +} diff --git a/novawallet/Modules/Swaps/Execution/SwapExecutionInteractor.swift b/novawallet/Modules/Swaps/Execution/SwapExecutionInteractor.swift new file mode 100644 index 0000000000..957299b62c --- /dev/null +++ b/novawallet/Modules/Swaps/Execution/SwapExecutionInteractor.swift @@ -0,0 +1,40 @@ +import UIKit + +final class SwapExecutionInteractor { + weak var presenter: SwapExecutionInteractorOutputProtocol? + + let assetsExchangeService: AssetsExchangeServiceProtocol + let operationQueue: OperationQueue + + init( + assetsExchangeService: AssetsExchangeServiceProtocol, + operationQueue: OperationQueue + ) { + self.assetsExchangeService = assetsExchangeService + self.operationQueue = operationQueue + } +} + +extension SwapExecutionInteractor: SwapExecutionInteractorInputProtocol { + func submit(using estimation: AssetExchangeFee) { + let wrapper = assetsExchangeService.submit( + using: estimation, + notifyingIn: .main + ) { [weak self] newOperationIndex in + self?.presenter?.didStartExecution(for: newOperationIndex) + } + + execute( + wrapper: wrapper, + inOperationQueue: operationQueue, + runningCallbackIn: .main + ) { [weak self] result in + switch result { + case let .success(amount): + self?.presenter?.didCompleteFullExecution(received: amount) + case let .failure(error): + self?.presenter?.didFailExecution(with: error) + } + } + } +} diff --git a/novawallet/Modules/Swaps/Execution/SwapExecutionPresenter.swift b/novawallet/Modules/Swaps/Execution/SwapExecutionPresenter.swift new file mode 100644 index 0000000000..4c3b41844a --- /dev/null +++ b/novawallet/Modules/Swaps/Execution/SwapExecutionPresenter.swift @@ -0,0 +1,354 @@ +import Foundation +import SoraFoundation + +final class SwapExecutionPresenter { + weak var view: SwapExecutionViewProtocol? + let wireframe: SwapExecutionWireframeProtocol + let interactor: SwapExecutionInteractorInputProtocol + + let model: SwapExecutionModel + let executionViewModelFactory: SwapExecutionViewModelFactoryProtocol + let detailsViewModelFactory: SwapDetailsViewModelFactoryProtocol + let priceStore: AssetExchangePriceStoring + + var quote: AssetExchangeQuote { + model.quote + } + + var chainAssetIn: ChainAsset { + model.chainAssetIn + } + + var chainAssetOut: ChainAsset { + model.chainAssetOut + } + + var payAssetPrice: PriceData? { + priceStore.fetchPrice(for: model.chainAssetIn.chainAssetId) + } + + var receiveAssetPrice: PriceData? { + priceStore.fetchPrice(for: model.chainAssetOut.chainAssetId) + } + + var feeAssetPrice: PriceData? { + priceStore.fetchPrice(for: model.feeAsset.chainAssetId) + } + + private var state: SwapExecutionState? + private var execTimer: CountdownTimer? + + init( + model: SwapExecutionModel, + interactor: SwapExecutionInteractorInputProtocol, + wireframe: SwapExecutionWireframeProtocol, + executionViewModelFactory: SwapExecutionViewModelFactoryProtocol, + detailsViewModelFactory: SwapDetailsViewModelFactoryProtocol, + priceStore: AssetExchangePriceStoring, + localizationManager: LocalizationManagerProtocol + ) { + self.model = model + self.interactor = interactor + self.wireframe = wireframe + self.executionViewModelFactory = executionViewModelFactory + self.detailsViewModelFactory = detailsViewModelFactory + self.priceStore = priceStore + self.localizationManager = localizationManager + } + + deinit { + clearTimer() + } + + private func provideExecutionViewModel() { + let viewModel: SwapExecutionViewModel? = switch state { + case let .inProgress(operationIndex): + executionViewModelFactory.createInProgressViewModel( + from: quote, + currentOperationIndex: operationIndex, + remainedTime: execTimer?.remainedInterval ?? 0, + locale: selectedLocale + ) + case let .completed(date): + executionViewModelFactory.createCompletedViewModel( + quote: quote, + for: date, + locale: selectedLocale + ) + case let .failed(operationIndex, date): + executionViewModelFactory.createFailedViewModel( + quote: quote, + currentOperationIndex: operationIndex, + for: date, + locale: selectedLocale + ) + case nil: + nil + } + + guard let viewModel else { + return + } + + view?.didReceiveExecution(viewModel: viewModel) + } + + private func provideAssetInViewModel() { + let viewModel = detailsViewModelFactory.assetViewModel( + chainAsset: chainAssetIn, + amount: model.quote.route.amountIn, + priceData: payAssetPrice, + locale: selectedLocale + ) + + view?.didReceiveAssetIn(viewModel: viewModel) + } + + private func provideAssetOutViewModel() { + let viewModel = detailsViewModelFactory.assetViewModel( + chainAsset: chainAssetOut, + amount: quote.route.amountOut, + priceData: receiveAssetPrice, + locale: selectedLocale + ) + + view?.didReceiveAssetOut(viewModel: viewModel) + } + + private func provideRateViewModel() { + let params = RateParams( + assetDisplayInfoIn: chainAssetIn.assetDisplayInfo, + assetDisplayInfoOut: chainAssetOut.assetDisplayInfo, + amountIn: model.quote.route.amountIn, + amountOut: model.quote.route.amountOut + ) + + let viewModel = detailsViewModelFactory.rateViewModel(from: params, locale: selectedLocale) + + view?.didReceiveRate(viewModel: .loaded(value: viewModel)) + } + + private func provideRouteViewModel() { + let viewModel = detailsViewModelFactory.routeViewModel(from: model.quote.metaOperations) + + view?.didReceiveRoute(viewModel: .loaded(value: viewModel)) + } + + private func providePriceDifferenceViewModel() { + let params = RateParams( + assetDisplayInfoIn: chainAssetIn.assetDisplayInfo, + assetDisplayInfoOut: chainAssetOut.assetDisplayInfo, + amountIn: quote.route.amountIn, + amountOut: quote.route.amountOut + ) + + if let viewModel = detailsViewModelFactory.priceDifferenceViewModel( + rateParams: params, + priceIn: payAssetPrice, + priceOut: receiveAssetPrice, + locale: selectedLocale + ) { + view?.didReceivePriceDifference(viewModel: .loaded(value: viewModel)) + } else { + view?.didReceivePriceDifference(viewModel: nil) + } + } + + private func provideSlippageViewModel() { + let viewModel = detailsViewModelFactory.slippageViewModel(slippage: model.fee.slippage, locale: selectedLocale) + view?.didReceiveSlippage(viewModel: viewModel) + } + + private func provideFeeViewModel() { + let feeInFiat = model.fee.calculateTotalFeeInFiat( + matching: model.quote.metaOperations, + priceStore: priceStore + ) + + let viewModel = detailsViewModelFactory.feeViewModel( + amountInFiat: feeInFiat, + isEditable: false, + currencyId: feeAssetPrice?.currencyId, + locale: selectedLocale + ) + + view?.didReceiveTotalFee(viewModel: .loaded(value: viewModel)) + } + + private func updateSwapDetails() { + provideRateViewModel() + providePriceDifferenceViewModel() + provideSlippageViewModel() + provideRouteViewModel() + provideFeeViewModel() + } + + private func updateSwapAssets() { + provideAssetInViewModel() + provideAssetOutViewModel() + } + + private func clearTimer() { + execTimer?.delegate = nil + execTimer?.stop() + execTimer = nil + } + + private func restartCountdownTimer(for reminedExecutionTime: TimeInterval) { + let currentTimer = execTimer ?? CountdownTimer() + + currentTimer.stop() + currentTimer.delegate = self + currentTimer.start(with: reminedExecutionTime) + + execTimer = currentTimer + } + + private func updateInProgressStateIfNeeded(for newOperationIndex: Int) { + if case let .inProgress(operationIndex) = state, operationIndex == newOperationIndex { + return + } + + let remainedExecutionTime = model.quote.totalExecutionTime(from: newOperationIndex) + + state = .inProgress(newOperationIndex) + + restartCountdownTimer(for: remainedExecutionTime) + + provideExecutionViewModel() + } + + private func updateCompletedStateIfNeeded() { + clearTimer() + + state = .completed(Date()) + + provideExecutionViewModel() + } + + private func updateFailedStateIfNeeded() { + guard case let .inProgress(operationIndex) = state else { return } + + clearTimer() + + state = .failed(operationIndex, Date()) + + provideExecutionViewModel() + } +} + +extension SwapExecutionPresenter: CountdownTimerDelegate { + func didStart(with _: TimeInterval) {} + + func didCountdown(remainedInterval: TimeInterval) { + view?.didUpdateExecution(remainedTime: UInt(remainedInterval.rounded(.up))) + } + + func didStop(with remainedInterval: TimeInterval) { + view?.didUpdateExecution(remainedTime: UInt(remainedInterval.rounded(.up))) + } +} + +extension SwapExecutionPresenter: SwapExecutionPresenterProtocol { + func setup() { + provideExecutionViewModel() + updateSwapDetails() + updateSwapAssets() + + updateInProgressStateIfNeeded(for: 0) + + interactor.submit(using: model.fee) + } + + func showRateInfo() { + wireframe.showRateInfo(from: view) + } + + func showRouteDetails() { + wireframe.showRouteDetails( + from: view, + quote: model.quote, + fee: model.fee + ) + } + + func showPriceDifferenceInfo() { + let title = LocalizableResource { + R.string.localizable.swapsSetupPriceDifference( + preferredLanguages: $0.rLanguages + ) + } + let details = LocalizableResource { + R.string.localizable.swapsSetupPriceDifferenceDescription( + preferredLanguages: $0.rLanguages + ) + } + wireframe.showInfo( + from: view, + title: title, + details: details + ) + } + + func showSlippageInfo() { + wireframe.showSlippageInfo(from: view) + } + + func showTotalFeeInfo() { + wireframe.showFeeDetails( + from: view, + operations: model.quote.metaOperations, + fee: model.fee + ) + } + + func activateDone() { + wireframe.complete(on: view, receiveChainAsset: chainAssetOut) + } + + func activateTryAgain() { + guard case let .failed(operationIndex, _) = state else { + return + } + + let payChainAsset = quote.metaOperations[operationIndex].assetIn + let receiveChainAsset = chainAssetOut + + wireframe.showSwapSetup( + from: view, + payChainAsset: payChainAsset, + receiveChainAsset: receiveChainAsset + ) + } +} + +extension SwapExecutionPresenter: SwapExecutionInteractorOutputProtocol { + func didStartExecution(for operationIndex: Int) { + updateInProgressStateIfNeeded(for: operationIndex) + } + + func didCompleteFullExecution(received _: Balance) { + updateCompletedStateIfNeeded() + } + + func didFailExecution(with error: Error) { + updateFailedStateIfNeeded() + + _ = wireframe.handleExtrinsicSigningErrorPresentation( + error, + view: view, + closeAction: .dismissAllModals, + completionClosure: nil + ) + } +} + +extension SwapExecutionPresenter: Localizable { + func applyLocalization() { + if let view, view.isSetup { + provideExecutionViewModel() + updateSwapAssets() + updateSwapDetails() + } + } +} diff --git a/novawallet/Modules/Swaps/Execution/SwapExecutionProtocols.swift b/novawallet/Modules/Swaps/Execution/SwapExecutionProtocols.swift new file mode 100644 index 0000000000..476584c926 --- /dev/null +++ b/novawallet/Modules/Swaps/Execution/SwapExecutionProtocols.swift @@ -0,0 +1,58 @@ +protocol SwapExecutionViewProtocol: ControllerBackedProtocol { + func didReceiveExecution(viewModel: SwapExecutionViewModel) + func didUpdateExecution(remainedTime: UInt) + func didReceiveAssetIn(viewModel: SwapAssetAmountViewModel) + func didReceiveAssetOut(viewModel: SwapAssetAmountViewModel) + func didReceiveRate(viewModel: LoadableViewModelState) + func didReceiveRoute(viewModel: LoadableViewModelState<[SwapRouteItemView.ItemViewModel]>) + func didReceivePriceDifference(viewModel: LoadableViewModelState?) + func didReceiveSlippage(viewModel: String) + func didReceiveTotalFee(viewModel: LoadableViewModelState) +} + +protocol SwapExecutionPresenterProtocol: AnyObject { + func setup() + func showRateInfo() + func showPriceDifferenceInfo() + func showSlippageInfo() + func showTotalFeeInfo() + func showRouteDetails() + func activateDone() + func activateTryAgain() +} + +protocol SwapExecutionInteractorInputProtocol: AnyObject { + func submit(using estimation: AssetExchangeFee) +} + +protocol SwapExecutionInteractorOutputProtocol: AnyObject { + func didStartExecution(for operationIndex: Int) + func didCompleteFullExecution(received amount: Balance) + func didFailExecution(with error: Error) +} + +protocol SwapExecutionWireframeProtocol: ShortTextInfoPresentable, MessageSheetPresentable, AlertPresentable, + ErrorPresentable, ExtrinsicSigningErrorHandling { + func complete( + on view: ControllerBackedProtocol?, + receiveChainAsset: ChainAsset + ) + + func showSwapSetup( + from view: SwapExecutionViewProtocol?, + payChainAsset: ChainAsset, + receiveChainAsset: ChainAsset + ) + + func showRouteDetails( + from view: ControllerBackedProtocol?, + quote: AssetExchangeQuote, + fee: AssetExchangeFee + ) + + func showFeeDetails( + from view: ControllerBackedProtocol?, + operations: [AssetExchangeMetaOperationProtocol], + fee: AssetExchangeFee + ) +} diff --git a/novawallet/Modules/Swaps/Execution/SwapExecutionViewController.swift b/novawallet/Modules/Swaps/Execution/SwapExecutionViewController.swift new file mode 100644 index 0000000000..23d431dd2e --- /dev/null +++ b/novawallet/Modules/Swaps/Execution/SwapExecutionViewController.swift @@ -0,0 +1,162 @@ +import UIKit +import SoraFoundation + +final class SwapExecutionViewController: UIViewController, ViewHolder { + typealias RootViewType = SwapExecutionViewLayout + + let presenter: SwapExecutionPresenterProtocol + + init( + presenter: SwapExecutionPresenterProtocol, + 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 = SwapExecutionViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupHandlers() + setupLocalization() + + presenter.setup() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + rootView.statusView.updateAnimationOnAppear() + } + + private func setupHandlers() { + presentationController?.delegate = self + + rootView.detailsView.delegate = self + + rootView.rateCell.addTarget(self, action: #selector(rateAction), for: .touchUpInside) + rootView.routeCell.addTarget(self, action: #selector(routeAction), for: .touchUpInside) + rootView.priceDifferenceCell.addTarget(self, action: #selector(priceDifferenceAction), for: .touchUpInside) + rootView.slippageCell.addTarget(self, action: #selector(slippageAction), for: .touchUpInside) + rootView.totalFeeCell.addTarget(self, action: #selector(totalFeeAction), for: .touchUpInside) + } + + private func setupLocalization() { + rootView.setup(locale: selectedLocale) + } + + @objc private func rateAction() { + presenter.showRateInfo() + } + + @objc private func priceDifferenceAction() { + presenter.showPriceDifferenceInfo() + } + + @objc private func routeAction() { + presenter.showRouteDetails() + } + + @objc private func slippageAction() { + presenter.showSlippageInfo() + } + + @objc private func totalFeeAction() { + presenter.showTotalFeeInfo() + } + + @objc private func actionDone() { + presenter.activateDone() + } + + @objc private func actionTryAgain() { + presenter.activateTryAgain() + } +} + +extension SwapExecutionViewController: SwapExecutionViewProtocol { + func didReceiveExecution(viewModel: SwapExecutionViewModel) { + rootView.statusView.bind(viewModel: viewModel, locale: selectedLocale) + + switch viewModel { + case .completed: + let doneButton = rootView.setupDoneButton(for: selectedLocale) + doneButton.addTarget(self, action: #selector(actionDone), for: .touchUpInside) + case .failed: + let tryAgainButton = rootView.setupTryAgainButton(for: selectedLocale) + tryAgainButton.addTarget(self, action: #selector(actionTryAgain), for: .touchUpInside) + case .inProgress: + break + } + } + + func didUpdateExecution(remainedTime: UInt) { + rootView.statusView.updateProgress(remainedTime: remainedTime) + } + + func didReceiveAssetIn(viewModel: SwapAssetAmountViewModel) { + rootView.pairsView.leftAssetView.bind(viewModel: viewModel) + } + + func didReceiveAssetOut(viewModel: SwapAssetAmountViewModel) { + rootView.pairsView.rigthAssetView.bind(viewModel: viewModel) + } + + func didReceiveRate(viewModel: LoadableViewModelState) { + rootView.rateCell.bind(loadableViewModel: viewModel) + } + + func didReceiveRoute(viewModel: LoadableViewModelState<[SwapRouteItemView.ItemViewModel]>) { + rootView.routeCell.bind(loadableRouteViewModel: viewModel) + } + + func didReceivePriceDifference(viewModel: LoadableViewModelState?) { + if let viewModel { + rootView.priceDifferenceCell.isHidden = false + rootView.priceDifferenceCell.bind(differenceViewModel: viewModel) + } else { + rootView.priceDifferenceCell.isHidden = true + } + } + + func didReceiveSlippage(viewModel: String) { + rootView.slippageCell.bind(loadableViewModel: .loaded(value: viewModel)) + } + + func didReceiveTotalFee(viewModel: LoadableViewModelState) { + rootView.totalFeeCell.bind(loadableViewModel: viewModel) + } +} + +extension SwapExecutionViewController: CollapsableContainerViewDelegate { + func animateAlongsideWithInfo(sender _: AnyObject?) { + rootView.containerView.scrollView.layoutIfNeeded() + } + + func didChangeExpansion(isExpanded _: Bool, sender _: AnyObject) {} +} + +extension SwapExecutionViewController: UIAdaptivePresentationControllerDelegate { + func presentationControllerShouldDismiss(_: UIPresentationController) -> Bool { + false + } +} + +extension SwapExecutionViewController: Localizable { + func applyLocalization() { + if isViewLoaded { + setupLocalization() + } + } +} diff --git a/novawallet/Modules/Swaps/Execution/SwapExecutionViewFactory.swift b/novawallet/Modules/Swaps/Execution/SwapExecutionViewFactory.swift new file mode 100644 index 0000000000..1e15f78349 --- /dev/null +++ b/novawallet/Modules/Swaps/Execution/SwapExecutionViewFactory.swift @@ -0,0 +1,53 @@ +import Foundation +import SoraFoundation + +struct SwapExecutionViewFactory { + static func createView( + for model: SwapExecutionModel, + flowState: SwapTokensFlowStateProtocol, + completionClosure: SwapCompletionClosure? + ) -> SwapExecutionViewProtocol? { + guard let currencyManager = CurrencyManager.shared else { return nil } + + let interactor = SwapExecutionInteractor( + assetsExchangeService: flowState.setupAssetExchangeService(), + operationQueue: OperationManagerFacade.sharedDefaultQueue + ) + + let wireframe = SwapExecutionWireframe(flowState: flowState, completionClosure: completionClosure) + + let priceAssetInfoFactory = PriceAssetInfoFactory(currencyManager: currencyManager) + let balanceViewModelFactoryFacade = BalanceViewModelFactoryFacade( + priceAssetInfoFactory: priceAssetInfoFactory + ) + + let detailsViewModelFactory = SwapDetailsViewModelFactory( + balanceViewModelFactoryFacade: balanceViewModelFactoryFacade, + priceAssetInfoFactory: priceAssetInfoFactory, + networkViewModelFactory: NetworkViewModelFactory(), + assetIconViewModelFactory: AssetIconViewModelFactory(), + percentForamatter: NumberFormatter.percentSingle.localizableResource(), + priceDifferenceConfig: .defaultConfig + ) + + let presenter = SwapExecutionPresenter( + model: model, + interactor: interactor, + wireframe: wireframe, + executionViewModelFactory: SwapExecutionViewModelFactory(), + detailsViewModelFactory: detailsViewModelFactory, + priceStore: flowState.priceStore, + localizationManager: LocalizationManager.shared + ) + + let view = SwapExecutionViewController( + presenter: presenter, + localizationManager: LocalizationManager.shared + ) + + presenter.view = view + interactor.presenter = presenter + + return view + } +} diff --git a/novawallet/Modules/Swaps/Execution/SwapExecutionViewLayout.swift b/novawallet/Modules/Swaps/Execution/SwapExecutionViewLayout.swift new file mode 100644 index 0000000000..08baba5caf --- /dev/null +++ b/novawallet/Modules/Swaps/Execution/SwapExecutionViewLayout.swift @@ -0,0 +1,120 @@ +import UIKit + +final class SwapExecutionViewLayout: ScrollableContainerLayoutView { + let statusView = SwapExecutionView() + + let pairsView = SwapPairView() + + let detailsView: SwapExecutionDetailsView = .create { + $0.contentInsets = .zero + $0.setExpanded(false, animated: false) + } + + var rateCell: SwapInfoViewCell { + detailsView.rateCell + } + + var routeCell: SwapRouteViewCell { + detailsView.routeCell + } + + var priceDifferenceCell: SwapInfoViewCell { + detailsView.priceDifferenceCell + } + + var slippageCell: SwapInfoViewCell { + detailsView.slippageCell + } + + var totalFeeCell: SwapNetworkFeeViewCell { + detailsView.totalFeeCell + } + + private var actionButton: TriangularedButton? + + func setupDoneButton(for locale: Locale) -> TriangularedButton { + let button = setupActionButton() + + button.setTitle(R.string.localizable.commonDone(preferredLanguages: locale.rLanguages)) + + containerView.scrollBottomOffset = safeAreaInsets.bottom + UIConstants.actionBottomInset + + UIConstants.actionHeight + 8 + + return button + } + + func setupTryAgainButton(for locale: Locale) -> TriangularedButton { + let button = setupActionButton() + + button.setTitle(R.string.localizable.commonTryAgain(preferredLanguages: locale.rLanguages)) + + containerView.scrollBottomOffset = safeAreaInsets.bottom + UIConstants.actionBottomInset + + UIConstants.actionHeight + 8 + + return button + } + + func setup(locale: Locale) { + detailsView.titleControl.titleLabel.text = R.string.localizable.swapsSetupDetailsTitle( + preferredLanguages: locale.rLanguages + ) + + slippageCell.titleButton.setTitle( + R.string.localizable.swapsSetupSlippage( + preferredLanguages: locale.rLanguages + ) + ) + priceDifferenceCell.titleButton.setTitle( + R.string.localizable.swapsSetupPriceDifference( + preferredLanguages: locale.rLanguages + ) + ) + rateCell.titleButton.setTitle( + R.string.localizable.swapsSetupDetailsRate( + preferredLanguages: locale.rLanguages + ) + ) + routeCell.titleButton.setTitle( + R.string.localizable.swapsDetailsRoute(preferredLanguages: locale.rLanguages) + ) + + totalFeeCell.titleButton.setTitle( + R.string.localizable.swapsDetailsTotalFee(preferredLanguages: locale.rLanguages) + ) + } + + override func setupStyle() { + backgroundColor = R.color.colorSecondaryScreenBackground() + } + + override func setupLayout() { + super.setupLayout() + + stackView.layoutMargins = UIEdgeInsets(top: 76, left: 16, bottom: 0, right: 16) + + addArrangedSubview(statusView, spacingAfter: 24) + addArrangedSubview(pairsView, spacingAfter: 24) + addArrangedSubview(detailsView) + } + + private func setupActionButton() -> TriangularedButton { + actionButton?.removeFromSuperview() + actionButton = nil + + let button = TriangularedButton() + button.applyDefaultStyle() + + addSubview(button) + + addSubview(button) + button.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(UIConstants.horizontalInset) + make.bottom.equalTo(safeAreaLayoutGuide).inset(UIConstants.actionBottomInset) + make.height.equalTo(UIConstants.actionHeight) + } + + actionButton = button + + return button + } +} diff --git a/novawallet/Modules/Swaps/Execution/SwapExecutionWireframe.swift b/novawallet/Modules/Swaps/Execution/SwapExecutionWireframe.swift new file mode 100644 index 0000000000..a46e68cfe3 --- /dev/null +++ b/novawallet/Modules/Swaps/Execution/SwapExecutionWireframe.swift @@ -0,0 +1,85 @@ +import Foundation + +final class SwapExecutionWireframe: SwapExecutionWireframeProtocol { + let flowState: SwapTokensFlowStateProtocol + let completionClosure: SwapCompletionClosure? + + init( + flowState: SwapTokensFlowStateProtocol, + completionClosure: SwapCompletionClosure? + ) { + self.flowState = flowState + self.completionClosure = completionClosure + } + + func complete( + on view: ControllerBackedProtocol?, + receiveChainAsset: ChainAsset + ) { + let presenter = view?.controller.presentingViewController + + presenter?.dismiss(animated: true) { + self.completionClosure?(receiveChainAsset) + } + } + + func showSwapSetup( + from view: SwapExecutionViewProtocol?, + payChainAsset: ChainAsset, + receiveChainAsset: ChainAsset + ) { + guard let swapView = SwapSetupViewFactory.createView( + state: flowState, + initState: .init(payChainAsset: payChainAsset, receiveChainAsset: receiveChainAsset), + swapCompletionClosure: completionClosure + ) else { + return + } + + let presenter = view?.controller.presentingViewController + + let navigationController = NovaNavigationController(rootViewController: swapView.controller) + + presenter?.dismiss(animated: true) { + presenter?.present(navigationController, animated: true) + } + } + + func showRouteDetails( + from view: ControllerBackedProtocol?, + quote: AssetExchangeQuote, + fee: AssetExchangeFee + ) { + guard + let routeDetailsView = SwapRouteDetailsViewFactory.createView( + for: quote, + fee: fee, + state: flowState + ) else { + return + } + + let navigationController = NovaNavigationController(rootViewController: routeDetailsView.controller) + + view?.controller.present(navigationController, animated: true) + } + + func showFeeDetails( + from view: ControllerBackedProtocol?, + operations: [AssetExchangeMetaOperationProtocol], + fee: AssetExchangeFee + ) { + guard + let routeDetailsView = SwapFeeDetailsViewFactory.createView( + for: operations, + fee: fee, + state: flowState + ) else { + return + } + + let navigationController = NovaNavigationController(rootViewController: routeDetailsView.controller) + + view?.controller.present(navigationController, animated: true) + } +} diff --git a/novawallet/Modules/Swaps/Execution/View/SwapExecutionDetailsView.swift b/novawallet/Modules/Swaps/Execution/View/SwapExecutionDetailsView.swift new file mode 100644 index 0000000000..6e7265ae2e --- /dev/null +++ b/novawallet/Modules/Swaps/Execution/View/SwapExecutionDetailsView.swift @@ -0,0 +1,53 @@ +import UIKit + +final class SwapExecutionDetailsView: CollapsableContainerView { + let rateCell: SwapInfoViewCell = .create { + $0.titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() + $0.titleButton.imageWithTitleView?.titleFont = .regularFootnote + $0.contentInsets = .init(top: 8, left: 16, bottom: 8, right: 16) + $0.borderView.borderType = .bottom + $0.roundedBackgroundView.cornerRadius = 12 + $0.roundedBackgroundView.roundingCorners = [.topLeft, .topRight] + } + + let priceDifferenceCell: SwapInfoViewCell = .create { + $0.titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() + $0.titleButton.imageWithTitleView?.titleFont = .regularFootnote + $0.contentInsets = .init(top: 8, left: 16, bottom: 8, right: 16) + $0.borderView.borderType = .bottom + $0.roundedBackgroundView.cornerRadius = 0 + } + + let slippageCell: SwapInfoViewCell = .create { + $0.titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() + $0.titleButton.imageWithTitleView?.titleFont = .regularFootnote + $0.contentInsets = .init(top: 8, left: 16, bottom: 8, right: 16) + $0.borderView.borderType = .bottom + $0.roundedBackgroundView.cornerRadius = 0 + } + + let routeCell: SwapRouteViewCell = .create { + $0.titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() + $0.titleButton.imageWithTitleView?.titleFont = .regularFootnote + $0.contentInsets = .init(top: 8, left: 16, bottom: 8, right: 16) + $0.borderView.borderType = .bottom + $0.roundedBackgroundView.cornerRadius = 0 + } + + let totalFeeCell: SwapNetworkFeeViewCell = .create { + $0.contentInsets = .init(top: 8, left: 16, bottom: 8, right: 16) + $0.borderView.borderType = .none + $0.roundedBackgroundView.cornerRadius = 12 + $0.roundedBackgroundView.roundingCorners = [.bottomLeft, .bottomRight] + } + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundView.sideLength = 12 + } + + override var rows: [UIView] { + [rateCell, priceDifferenceCell, slippageCell, routeCell, totalFeeCell] + } +} diff --git a/novawallet/Modules/Swaps/Execution/View/SwapExecutionView.swift b/novawallet/Modules/Swaps/Execution/View/SwapExecutionView.swift new file mode 100644 index 0000000000..f0b2e71ed4 --- /dev/null +++ b/novawallet/Modules/Swaps/Execution/View/SwapExecutionView.swift @@ -0,0 +1,161 @@ +import UIKit + +final class SwapExecutionView: UIView { + let progressView = OperationExecutionProgressView() + + let statusTitleView: MultiValueView = .create { view in + view.valueTop.textAlignment = .center + view.valueBottom.textAlignment = .center + + view.spacing = 4 + } + + let statusDetailsView: GenericBorderedView = .create { view in + view.contentInsets = UIEdgeInsets(verticalInset: 0, horizontalInset: 16) + view.contentView.textAlignment = .center + view.backgroundView.cornerRadius = 12 + } + + convenience init() { + self.init(frame: .zero) + } + + override init(frame: CGRect) { + super.init(frame: frame) + + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func bind(viewModel: SwapExecutionViewModel, locale: Locale) { + switch viewModel { + case let .inProgress(inProgress): + progressView.bind(viewModel: .inProgress(inProgress.remainedTimeViewModel)) + + statusTitleView.bind( + viewModel: .init( + topValue: R.string.localizable.swapsExecutionDontCloseApp( + preferredLanguages: locale.rLanguages + ), + bottomValue: inProgress.currentOperation + ) + ) + + statusDetailsView.contentView.text = inProgress.details + + applyInProgressStyle() + + statusDetailsView.contentView.startShimmering() + + case let .completed(completed): + progressView.bind(viewModel: .completed) + + statusTitleView.bind( + viewModel: .init( + topValue: R.string.localizable.transactionStatusCompleted( + preferredLanguages: locale.rLanguages + ), + bottomValue: completed.time + ) + ) + + statusDetailsView.contentView.stopShimmering() + statusDetailsView.contentView.text = completed.details + + applyCompletedStyle() + + case let .failed(failed): + progressView.bind(viewModel: .failed) + + statusTitleView.bind( + viewModel: .init( + topValue: R.string.localizable.transactionStatusFailed( + preferredLanguages: locale.rLanguages + ), + bottomValue: failed.time + ) + ) + + statusDetailsView.contentView.stopShimmering() + statusDetailsView.contentView.text = failed.details + + applyFailedStyle() + } + } + + func updateProgress(remainedTime: UInt) { + progressView.updateProgress(remainedTime: remainedTime) + } + + func updateAnimationOnAppear() { + progressView.updateAnimationOnAppear() + } + + private func applyInProgressStyle() { + statusTitleView.apply( + style: .init( + topLabel: .boldTitle1Primary, + bottomLabel: .semiboldBodyButtonAccent + ) + ) + + statusDetailsView.backgroundView.applyCellBackgroundStyle() + statusDetailsView.contentView.applyShimmer(style: .regularSubheadlineSecondary) + statusDetailsView.contentView.apply(style: .regularSubhedlineSecondary) + } + + private func applyFailedStyle() { + statusTitleView.apply( + style: .init( + topLabel: .boldTitle1Negative, + bottomLabel: .semiboldBodySecondary + ) + ) + + statusDetailsView.backgroundView.applyErrorBlockBackgroundStyle() + statusDetailsView.contentView.applyShimmer(style: .regularSubheadlinePrimary) + statusDetailsView.contentView.apply(style: .regularSubhedlinePrimary) + } + + private func applyCompletedStyle() { + statusTitleView.apply( + style: .init( + topLabel: .boldTitle1Positive, + bottomLabel: .semiboldBodySecondary + ) + ) + + statusDetailsView.backgroundView.applyCellBackgroundStyle() + statusDetailsView.contentView.applyShimmer(style: .regularSubheadlineSecondary) + statusDetailsView.contentView.apply(style: .regularSubhedlineSecondary) + } + + private func setupLayout() { + let progressViewContainer = UIView.vStack(alignment: .center, [progressView]) + + let contentView = UIView.vStack( + alignment: .fill, + [ + progressViewContainer, + statusTitleView, + statusDetailsView + ] + ) + + addSubview(contentView) + contentView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + contentView.setCustomSpacing(16, after: progressViewContainer) + contentView.setCustomSpacing(24, after: statusTitleView) + + statusDetailsView.snp.makeConstraints { make in + make.height.equalTo(48) + } + } +} diff --git a/novawallet/Modules/Swaps/Execution/ViewModel/AssetExchangeMetaOperationLabel+Display.swift b/novawallet/Modules/Swaps/Execution/ViewModel/AssetExchangeMetaOperationLabel+Display.swift new file mode 100644 index 0000000000..7a886a4e87 --- /dev/null +++ b/novawallet/Modules/Swaps/Execution/ViewModel/AssetExchangeMetaOperationLabel+Display.swift @@ -0,0 +1,12 @@ +import Foundation + +extension AssetExchangeMetaOperationLabel { + func getTitle(for locale: Locale) -> String { + switch self { + case .swap: + R.string.localizable.swapsLabelSwap(preferredLanguages: locale.rLanguages) + case .transfer: + R.string.localizable.swapsLabelCrosschain(preferredLanguages: locale.rLanguages) + } + } +} diff --git a/novawallet/Modules/Swaps/Execution/ViewModel/SwapExecutionViewModel.swift b/novawallet/Modules/Swaps/Execution/ViewModel/SwapExecutionViewModel.swift new file mode 100644 index 0000000000..50b555292b --- /dev/null +++ b/novawallet/Modules/Swaps/Execution/ViewModel/SwapExecutionViewModel.swift @@ -0,0 +1,23 @@ +import Foundation + +enum SwapExecutionViewModel { + struct InProgress { + let remainedTimeViewModel: CountdownLoadingView.ViewModel + let currentOperation: String + let details: String + } + + struct Completed { + let time: String + let details: String + } + + struct Failed { + let time: String + let details: String + } + + case inProgress(InProgress) + case completed(Completed) + case failed(Failed) +} diff --git a/novawallet/Modules/Swaps/Execution/ViewModel/SwapExecutionViewModelFactory.swift b/novawallet/Modules/Swaps/Execution/ViewModel/SwapExecutionViewModelFactory.swift new file mode 100644 index 0000000000..1ae0513a6f --- /dev/null +++ b/novawallet/Modules/Swaps/Execution/ViewModel/SwapExecutionViewModelFactory.swift @@ -0,0 +1,125 @@ +import Foundation +import SoraFoundation + +protocol SwapExecutionViewModelFactoryProtocol { + func createInProgressViewModel( + from quote: AssetExchangeQuote, + currentOperationIndex: Int, + remainedTime: TimeInterval, + locale: Locale + ) -> SwapExecutionViewModel + + func createFailedViewModel( + quote: AssetExchangeQuote, + currentOperationIndex: Int, + for date: Date, + locale: Locale + ) -> SwapExecutionViewModel + + func createCompletedViewModel( + quote: AssetExchangeQuote, + for date: Date, + locale: Locale + ) -> SwapExecutionViewModel +} + +final class SwapExecutionViewModelFactory { + let dateFormatter: LocalizableResource + + init(dateFormatter: LocalizableResource = DateFormatter.shortDateAndTime) { + self.dateFormatter = dateFormatter + } + + private func createOperationDetails( + _ operation: AssetExchangeMetaOperationProtocol, + locale: Locale + ) -> String { + switch operation.label { + case .transfer: + return R.string.localizable.swapsExecutionTransferDetails( + operation.assetIn.asset.symbol, + operation.assetOut.chain.name, + preferredLanguages: locale.rLanguages + ) + case .swap: + return R.string.localizable.swapsExecutionSwapDetails( + operation.assetIn.asset.symbol, + operation.assetOut.asset.symbol, + operation.assetOut.chain.name, + preferredLanguages: locale.rLanguages + ) + } + } +} + +extension SwapExecutionViewModelFactory: SwapExecutionViewModelFactoryProtocol { + func createInProgressViewModel( + from quote: AssetExchangeQuote, + currentOperationIndex: Int, + remainedTime: TimeInterval, + locale: Locale + ) -> SwapExecutionViewModel { + let remainedTimeViewModel = CountdownLoadingView.ViewModel( + duration: UInt(remainedTime.rounded(.up)), + units: R.string.localizable.secTimeUnits(preferredLanguages: locale.rLanguages) + ) + + let currentOperationString = createOperationDetails( + quote.metaOperations[currentOperationIndex], + locale: locale + ) + + let totalOperations = R.string.localizable.commonOperations( + format: quote.metaOperations.count, + preferredLanguages: locale.rLanguages + ) + + let details = R.string.localizable.commonOf( + String(currentOperationIndex + 1), + totalOperations, + preferredLanguages: locale.rLanguages + ) + + return .inProgress( + .init( + remainedTimeViewModel: remainedTimeViewModel, + currentOperation: currentOperationString, + details: details + ) + ) + } + + func createFailedViewModel( + quote: AssetExchangeQuote, + currentOperationIndex: Int, + for date: Date, + locale: Locale + ) -> SwapExecutionViewModel { + let time = dateFormatter.value(for: locale).string(from: date) + + let operationLabel = quote.metaOperations[currentOperationIndex].label.getTitle(for: locale) + + let details = R.string.localizable.swapsExecutionSwapFailure( + String(currentOperationIndex + 1), + operationLabel, + preferredLanguages: locale.rLanguages + ) + + return .failed(.init(time: time, details: details)) + } + + func createCompletedViewModel( + quote: AssetExchangeQuote, + for date: Date, + locale: Locale + ) -> SwapExecutionViewModel { + let time = dateFormatter.value(for: locale).string(from: date) + + let operationsString = R.string.localizable.commonOperations( + format: quote.metaOperations.count, + preferredLanguages: locale.rLanguages + ) + + return .completed(.init(time: time, details: operationsString)) + } +} diff --git a/novawallet/Modules/Swaps/FeeDetails/SwapFeeDetailsPresenter.swift b/novawallet/Modules/Swaps/FeeDetails/SwapFeeDetailsPresenter.swift new file mode 100644 index 0000000000..472265331f --- /dev/null +++ b/novawallet/Modules/Swaps/FeeDetails/SwapFeeDetailsPresenter.swift @@ -0,0 +1,46 @@ +import Foundation +import SoraFoundation + +final class SwapFeeDetailsPresenter { + weak var view: SwapFeeDetailsViewProtocol? + + let operations: [AssetExchangeMetaOperationProtocol] + let fee: AssetExchangeFee + let viewModelFactory: SwapFeeDetailsViewModelFactoryProtocol + + init( + operations: [AssetExchangeMetaOperationProtocol], + fee: AssetExchangeFee, + viewModelFactory: SwapFeeDetailsViewModelFactoryProtocol, + localizationManager: LocalizationManagerProtocol + ) { + self.operations = operations + self.fee = fee + self.viewModelFactory = viewModelFactory + self.localizationManager = localizationManager + } + + private func provideViewModel() { + let viewModel = viewModelFactory.createViewModel( + from: operations, + fee: fee, + locale: selectedLocale + ) + + view?.didReceive(viewModel: viewModel) + } +} + +extension SwapFeeDetailsPresenter: SwapFeeDetailsPresenterProtocol { + func setup() { + provideViewModel() + } +} + +extension SwapFeeDetailsPresenter: Localizable { + func applyLocalization() { + if let view, view.isSetup { + provideViewModel() + } + } +} diff --git a/novawallet/Modules/Swaps/FeeDetails/SwapFeeDetailsProtocols.swift b/novawallet/Modules/Swaps/FeeDetails/SwapFeeDetailsProtocols.swift new file mode 100644 index 0000000000..d527157d0f --- /dev/null +++ b/novawallet/Modules/Swaps/FeeDetails/SwapFeeDetailsProtocols.swift @@ -0,0 +1,7 @@ +protocol SwapFeeDetailsViewProtocol: ControllerBackedProtocol { + func didReceive(viewModel: SwapFeeDetailsViewModel) +} + +protocol SwapFeeDetailsPresenterProtocol: AnyObject { + func setup() +} diff --git a/novawallet/Modules/Swaps/FeeDetails/SwapFeeDetailsViewController.swift b/novawallet/Modules/Swaps/FeeDetails/SwapFeeDetailsViewController.swift new file mode 100644 index 0000000000..ffb93c3af5 --- /dev/null +++ b/novawallet/Modules/Swaps/FeeDetails/SwapFeeDetailsViewController.swift @@ -0,0 +1,57 @@ +import UIKit +import SoraFoundation + +final class SwapFeeDetailsViewController: UIViewController, ViewHolder { + typealias RootViewType = SwapFeeDetailsViewLayout + + let presenter: SwapFeeDetailsPresenterProtocol + + init( + presenter: SwapFeeDetailsPresenterProtocol, + 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 = SwapFeeDetailsViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupLocalization() + + presenter.setup() + } +} + +private extension SwapFeeDetailsViewController { + func setupLocalization() { + rootView.totalFeeView.titleView.text = R.string.localizable.swapsDetailsTotalFee( + preferredLanguages: selectedLocale.rLanguages + ) + } +} + +extension SwapFeeDetailsViewController: SwapFeeDetailsViewProtocol { + func didReceive(viewModel: SwapFeeDetailsViewModel) { + rootView.bind(viewModel: viewModel) + } +} + +extension SwapFeeDetailsViewController: Localizable { + func applyLocalization() { + if isViewLoaded { + setupLocalization() + } + } +} diff --git a/novawallet/Modules/Swaps/FeeDetails/SwapFeeDetailsViewFactory.swift b/novawallet/Modules/Swaps/FeeDetails/SwapFeeDetailsViewFactory.swift new file mode 100644 index 0000000000..0bba4e3a9a --- /dev/null +++ b/novawallet/Modules/Swaps/FeeDetails/SwapFeeDetailsViewFactory.swift @@ -0,0 +1,33 @@ +import Foundation +import SoraFoundation + +struct SwapFeeDetailsViewFactory { + static func createView( + for operations: [AssetExchangeMetaOperationProtocol], + fee: AssetExchangeFee, + state: SwapTokensFlowStateProtocol + ) -> SwapFeeDetailsViewProtocol? { + guard let currencyManager = CurrencyManager.shared else { return nil } + + let viewModelFactory = SwapFeeDetailsViewModelFactory( + priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: currencyManager), + priceStore: state.priceStore + ) + + let presenter = SwapFeeDetailsPresenter( + operations: operations, + fee: fee, + viewModelFactory: viewModelFactory, + localizationManager: LocalizationManager.shared + ) + + let view = SwapFeeDetailsViewController( + presenter: presenter, + localizationManager: LocalizationManager.shared + ) + + presenter.view = view + + return view + } +} diff --git a/novawallet/Modules/Swaps/FeeDetails/SwapFeeDetailsViewLayout.swift b/novawallet/Modules/Swaps/FeeDetails/SwapFeeDetailsViewLayout.swift new file mode 100644 index 0000000000..680c5eea56 --- /dev/null +++ b/novawallet/Modules/Swaps/FeeDetails/SwapFeeDetailsViewLayout.swift @@ -0,0 +1,55 @@ +import UIKit + +final class SwapFeeDetailsViewLayout: ScrollableContainerLayoutView { + let totalFeeView: GenericTitleValueView = .create { view in + view.titleView.apply(style: .semiboldBodyPrimary) + view.valueView.apply(style: .regularSubhedlinePrimary) + } + + private var operationFeeViewList: [SwapOperationFeeView] = [] + + override func setupStyle() { + super.setupStyle() + + stackView.layoutMargins = UIEdgeInsets(verticalInset: 0, horizontalInset: 16) + } + + override func setupLayout() { + super.setupLayout() + + addArrangedSubview(totalFeeView, spacingAfter: 8) + totalFeeView.snp.makeConstraints { make in + make.height.equalTo(42) + } + } + + func bind(viewModel: SwapFeeDetailsViewModel) { + totalFeeView.valueView.text = viewModel.total + + bindOperationFees(from: viewModel.operationFees) + } +} + +private extension SwapFeeDetailsViewLayout { + func bindOperationFees(from viewModels: [SwapOperationFeeView.ViewModel]) { + let itemsToInsert = max(0, viewModels.count - operationFeeViewList.count) + let itemsToRemove = max(0, operationFeeViewList.count - viewModels.count) + + if itemsToRemove > 0 { + operationFeeViewList.suffix(itemsToRemove).forEach { $0.removeFromSuperview() } + operationFeeViewList.removeLast(itemsToRemove) + } + + if itemsToInsert > 0 { + (0 ..< itemsToInsert).forEach { _ in + let feeView = SwapOperationFeeView() + addArrangedSubview(feeView, spacingAfter: 12) + operationFeeViewList.append(feeView) + } + } + + zip(viewModels, operationFeeViewList).forEach { viewModel, feeView in + feeView.bind(viewModel: viewModel) + } + } +} diff --git a/novawallet/Modules/Swaps/FeeDetails/View/SwapOperationFeeView.swift b/novawallet/Modules/Swaps/FeeDetails/View/SwapOperationFeeView.swift new file mode 100644 index 0000000000..564e6e04bd --- /dev/null +++ b/novawallet/Modules/Swaps/FeeDetails/View/SwapOperationFeeView.swift @@ -0,0 +1,103 @@ +import UIKit + +final class SwapOperationFeeView: UIView { + private let tableView: StackTableView = .create { view in + view.cellHeight = 44 + view.hasSeparators = true + view.contentInsets = UIEdgeInsets(top: 0, left: 16, bottom: 4, right: 16) + } + + private let routeCell: SwapRouteViewCell = .create { cell in + cell.titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() + cell.titleButton.imageWithTitleView?.titleFont = .regularFootnote + cell.rowContentView.selectable = false + cell.isUserInteractionEnabled = false + } + + private var feeCells: [StackTitleMultiValueCell] = [] + + override init(frame: CGRect) { + super.init(frame: frame) + + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func bind(viewModel: ViewModel) { + tableView.resetSeparators() + + bindRoute(from: viewModel) + bindFeeGroups(from: viewModel) + } +} + +private extension SwapOperationFeeView { + func bindRoute(from viewModel: ViewModel) { + routeCell.titleButton.setTitle(viewModel.type) + routeCell.bind(loadableRouteViewModel: .loaded(value: viewModel.route)) + + routeCell.routeView.getItems().forEach { routeItem in + routeItem.spacing = 6 + } + } + + func bindFeeGroups(from viewModel: ViewModel) { + clearFeeGroups() + + viewModel.feeGroups.forEach { addFeeGroup($0) } + } + + func clearFeeGroups() { + feeCells.forEach { $0.removeFromSuperview() } + feeCells = [] + } + + func addFeeGroup(_ feeGroup: FeeGroup) { + let cells = feeGroup.amounts.map { amount in + let feeCell = StackTitleMultiValueCell() + feeCell.canSelect = false + feeCell.bind(viewModel: amount) + return feeCell + } + + cells.first?.titleLabel.text = feeGroup.title + + cells.enumerated().forEach { index, cell in + let insertingIndex = feeCells.count + 1 + index + + tableView.addArrangedSubview(cell) + + let showsSeparator = index == cells.count - 1 + tableView.setShowsSeparator(showsSeparator, at: insertingIndex) + } + + feeCells.append(contentsOf: cells) + } + + func setupLayout() { + addSubview(tableView) + + tableView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + tableView.addArrangedSubview(routeCell) + } +} + +extension SwapOperationFeeView { + struct FeeGroup { + let title: String + let amounts: [BalanceViewModelProtocol] + } + + struct ViewModel { + let type: String + let route: [SwapRouteItemView.ItemViewModel] + let feeGroups: [FeeGroup] + } +} diff --git a/novawallet/Modules/Swaps/FeeDetails/ViewModel/SwapFeeDetailsViewModel.swift b/novawallet/Modules/Swaps/FeeDetails/ViewModel/SwapFeeDetailsViewModel.swift new file mode 100644 index 0000000000..9e47c09fd5 --- /dev/null +++ b/novawallet/Modules/Swaps/FeeDetails/ViewModel/SwapFeeDetailsViewModel.swift @@ -0,0 +1,6 @@ +import Foundation + +struct SwapFeeDetailsViewModel { + let total: String + let operationFees: [SwapOperationFeeView.ViewModel] +} diff --git a/novawallet/Modules/Swaps/FeeDetails/ViewModel/SwapFeeDetailsViewModelFactory.swift b/novawallet/Modules/Swaps/FeeDetails/ViewModel/SwapFeeDetailsViewModelFactory.swift new file mode 100644 index 0000000000..f1f9878a93 --- /dev/null +++ b/novawallet/Modules/Swaps/FeeDetails/ViewModel/SwapFeeDetailsViewModelFactory.swift @@ -0,0 +1,211 @@ +import Foundation + +protocol SwapFeeDetailsViewModelFactoryProtocol { + func createViewModel( + from operations: [AssetExchangeMetaOperationProtocol], + fee: AssetExchangeFee, + locale: Locale + ) -> SwapFeeDetailsViewModel +} + +final class SwapFeeDetailsViewModelFactory { + let balanceViewModelFacade: BalanceViewModelFactoryFacadeProtocol + let priceAssetInfoFactory: PriceAssetInfoFactoryProtocol + let priceStore: AssetExchangePriceStoring + + init(priceAssetInfoFactory: PriceAssetInfoFactoryProtocol, priceStore: AssetExchangePriceStoring) { + self.priceAssetInfoFactory = priceAssetInfoFactory + self.priceStore = priceStore + balanceViewModelFacade = BalanceViewModelFactoryFacade(priceAssetInfoFactory: priceAssetInfoFactory) + } +} + +private extension SwapFeeDetailsViewModelFactory { + func createType( + from operation: AssetExchangeMetaOperationProtocol, + locale: Locale + ) -> String { + switch operation.label { + case .swap: + R.string.localizable.swapsLabelSwap(preferredLanguages: locale.rLanguages) + case .transfer: + R.string.localizable.swapsLabelTransfer(preferredLanguages: locale.rLanguages) + } + } + + func createRoute( + from operation: AssetExchangeMetaOperationProtocol, + locale _: Locale + ) -> [SwapRouteItemView.ViewModel] { + switch operation.label { + case .swap: + [ + SwapRouteItemView.ViewModel( + title: operation.assetIn.chain.name, + icon: ImageViewModelFactory.createChainIconOrDefault(from: operation.assetIn.chain.icon) + ) + ] + case .transfer: + [ + operation.assetIn.chain, + operation.assetOut.chain + ].map { + SwapRouteItemView.ViewModel( + title: $0.name, + icon: ImageViewModelFactory.createChainIconOrDefault(from: $0.icon) + ) + } + } + } + + func createFee( + for amount: Balance, + feeAssetId: ChainAssetId, + chain: ChainModel, + locale: Locale + ) -> BalanceViewModelProtocol? { + guard + feeAssetId.chainId == chain.chainId, + let assetDisplayInfo = chain.chainAsset(for: feeAssetId.assetId)?.assetDisplayInfo else { + return nil + } + + return balanceViewModelFacade.balanceFromPrice( + targetAssetInfo: assetDisplayInfo, + amount: amount.decimal(assetInfo: assetDisplayInfo), + priceData: priceStore.fetchPrice(for: feeAssetId) + ).value(for: locale) + } + + func createNetworkFees( + for operation: AssetExchangeMetaOperationProtocol, + fee: AssetExchangeOperationFee, + locale: Locale + ) -> [BalanceViewModelProtocol] { + if let networkFee = createFee( + for: fee.submissionFee.amount, + feeAssetId: fee.submissionFee.amountWithAsset.asset, + chain: operation.assetIn.chain, + locale: locale + ) { + return [networkFee] + } else { + return [] + } + } + + func createCrosschainFees( + for operation: AssetExchangeMetaOperationProtocol, + fee: AssetExchangeOperationFee, + locale: Locale + ) -> [BalanceViewModelProtocol] { + var crosschainFees: [BalanceViewModelProtocol] = [] + + let postSubmissionFeeAssets = fee.postSubmissionFee.paidByAccount.map(\.amountWithAsset.asset) + + fee.postSubmissionFee.paidFromAmount.map(\.asset) + + var groupByToken: [ChainAssetId: Balance] = [:] + fee.postSubmissionFee.addAmount(to: &groupByToken) + + var addedFeeInAsset: Set = Set() + + for asset in postSubmissionFeeAssets { + if !addedFeeInAsset.contains(asset) { + if let fee = createFee( + for: groupByToken[asset] ?? 0, + feeAssetId: asset, + chain: operation.assetIn.chain, + locale: locale + ) { + crosschainFees.append(fee) + addedFeeInAsset.insert(asset) + } + } + } + + return crosschainFees + } + + func createOperationViewModel( + for operation: AssetExchangeMetaOperationProtocol, + fee: AssetExchangeOperationFee, + locale: Locale + ) -> SwapOperationFeeView.ViewModel { + let type = createType(from: operation, locale: locale) + let route = createRoute(from: operation, locale: locale) + + let networkFees = createNetworkFees( + for: operation, + fee: fee, + locale: locale + ) + + let crosschainFees = createCrosschainFees( + for: operation, + fee: fee, + locale: locale + ) + + var feeGroups: [SwapOperationFeeView.FeeGroup] = [] + + if !networkFees.isEmpty { + feeGroups.append( + SwapOperationFeeView.FeeGroup( + title: R.string.localizable.commonNetworkFee(preferredLanguages: locale.rLanguages), + amounts: networkFees + ) + ) + } + + if !crosschainFees.isEmpty { + feeGroups.append( + SwapOperationFeeView.FeeGroup( + title: R.string.localizable.commonCrossChainFee(preferredLanguages: locale.rLanguages), + amounts: crosschainFees + ) + ) + } + + return SwapOperationFeeView.ViewModel( + type: type, + route: route, + feeGroups: feeGroups + ) + } + + func createTotalFee( + operations: [AssetExchangeMetaOperationProtocol], + fee: AssetExchangeFee, + locale: Locale + ) -> String { + let totalAmountInFiat = fee.calculateTotalFeeInFiat( + matching: operations, + priceStore: priceStore + ) + + return balanceViewModelFacade.priceFromFiatAmount( + totalAmountInFiat, + currencyId: priceStore.getCurrencyId() + ).value(for: locale) + } +} + +extension SwapFeeDetailsViewModelFactory: SwapFeeDetailsViewModelFactoryProtocol { + func createViewModel( + from operations: [AssetExchangeMetaOperationProtocol], + fee: AssetExchangeFee, + locale: Locale + ) -> SwapFeeDetailsViewModel { + let operationFeeViewModels = zip(operations, fee.operationFees).map { operation, fee in + createOperationViewModel(for: operation, fee: fee, locale: locale) + } + + let totalFee = createTotalFee( + operations: operations, + fee: fee, + locale: locale + ) + + return SwapFeeDetailsViewModel(total: totalFee, operationFees: operationFeeViewModels) + } +} diff --git a/novawallet/Modules/Swaps/Model/SwapTokensFlowState.swift b/novawallet/Modules/Swaps/Model/SwapTokensFlowState.swift new file mode 100644 index 0000000000..3c01b9c030 --- /dev/null +++ b/novawallet/Modules/Swaps/Model/SwapTokensFlowState.swift @@ -0,0 +1,89 @@ +import Foundation +import Operation_iOS + +protocol SwapTokensFlowStateProtocol { + var assetListObservable: AssetListModelObservable { get } + + var priceStore: AssetExchangePriceStoring { get } + + var generalLocalSubscriptionFactory: GeneralStorageSubscriptionFactoryProtocol { get } + + func setupAssetExchangeService() -> AssetsExchangeServiceProtocol +} + +final class SwapTokensFlowState { + let assetListObservable: AssetListModelObservable + let priceStore: AssetExchangePriceStoring + let assetExchangeParams: AssetExchangeGraphProvidingParams + let generalLocalSubscriptionFactory: GeneralStorageSubscriptionFactoryProtocol + + private var assetExchangeService: AssetsExchangeServiceProtocol? + + init( + assetListObservable: AssetListModelObservable, + assetExchangeParams: AssetExchangeGraphProvidingParams + ) { + self.assetListObservable = assetListObservable + self.assetExchangeParams = assetExchangeParams + priceStore = AssetExchangePriceStore(assetListObservable: assetListObservable) + + generalLocalSubscriptionFactory = GeneralStorageSubscriptionFactory( + chainRegistry: assetExchangeParams.chainRegistry, + storageFacade: assetExchangeParams.substrateStorageFacade, + operationManager: OperationManager(operationQueue: assetExchangeParams.operationQueue), + logger: assetExchangeParams.logger + ) + } + + deinit { + assetExchangeService?.throttle() + assetExchangeService = nil + } +} + +extension SwapTokensFlowState: SwapTokensFlowStateProtocol { + func setupAssetExchangeService() -> AssetsExchangeServiceProtocol { + if let assetExchangeService { + return assetExchangeService + } + + let exchangesStateMediator = AssetsExchangeStateMediator() + + let feeSupportProvider = AssetsExchangeFeeSupportProvider( + feeSupportFetchersProvider: AssetExchangeFeeSupportFetchersProvider( + chainRegistry: assetExchangeParams.chainRegistry, + operationQueue: assetExchangeParams.operationQueue, + logger: assetExchangeParams.logger + ), + operationQueue: assetExchangeParams.operationQueue, + logger: assetExchangeParams.logger + ) + + let pathCostEstimator = AssetsExchangePathCostEstimator( + priceStore: priceStore, + chainRegistry: assetExchangeParams.chainRegistry + ) + + let graphProvider = AssetExchangeFacade.createGraphProvider( + for: assetExchangeParams, + feeSupportProvider: feeSupportProvider, + exchangesStateMediator: exchangesStateMediator, + pathCostEstimator: pathCostEstimator + ) + + let service = AssetsExchangeService( + graphProvider: graphProvider, + feeSupportProvider: feeSupportProvider, + exchangesStateMediator: exchangesStateMediator, + pathCostEstimator: pathCostEstimator, + operationQueue: assetExchangeParams.operationQueue, + logger: Logger.shared + ) + + service.setup() + + assetExchangeService = service + + return service + } +} diff --git a/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsPresenter.swift b/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsPresenter.swift new file mode 100644 index 0000000000..57ef9299c5 --- /dev/null +++ b/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsPresenter.swift @@ -0,0 +1,53 @@ +import Foundation +import SoraFoundation + +final class SwapRouteDetailsPresenter { + weak var view: SwapRouteDetailsViewProtocol? + + let quote: AssetExchangeQuote + let fee: AssetExchangeFee + let prices: [ChainAssetId: PriceData] + let viewModelFactory: SwapRouteDetailsViewModelFactoryProtocol + + init( + quote: AssetExchangeQuote, + fee: AssetExchangeFee, + prices: [ChainAssetId: PriceData], + viewModelFactory: SwapRouteDetailsViewModelFactoryProtocol, + localizationManager: LocalizationManagerProtocol + ) { + self.quote = quote + self.fee = fee + self.prices = prices + self.viewModelFactory = viewModelFactory + self.localizationManager = localizationManager + } + + private func provideViewModel() { + let viewModel = quote.metaOperations.enumerated().map { index, operation in + let fee = fee.operationFees[index] + + return viewModelFactory.createViewModel( + for: operation, + fee: fee, + locale: selectedLocale + ) + } + + view?.didReceive(viewModel: viewModel) + } +} + +extension SwapRouteDetailsPresenter: SwapRouteDetailsPresenterProtocol { + func setup() { + provideViewModel() + } +} + +extension SwapRouteDetailsPresenter: Localizable { + func applyLocalization() { + if let view, view.isSetup { + provideViewModel() + } + } +} diff --git a/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsProtocols.swift b/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsProtocols.swift new file mode 100644 index 0000000000..a9a2c32264 --- /dev/null +++ b/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsProtocols.swift @@ -0,0 +1,7 @@ +protocol SwapRouteDetailsViewProtocol: ControllerBackedProtocol { + func didReceive(viewModel: SwapRouteDetailsViewModel) +} + +protocol SwapRouteDetailsPresenterProtocol: AnyObject { + func setup() +} diff --git a/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsViewController.swift b/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsViewController.swift new file mode 100644 index 0000000000..284f290ea6 --- /dev/null +++ b/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsViewController.swift @@ -0,0 +1,57 @@ +import UIKit +import SoraFoundation + +final class SwapRouteDetailsViewController: UIViewController, ViewHolder { + typealias RootViewType = SwapRouteDetailsViewLayout + + let presenter: SwapRouteDetailsPresenterProtocol + + init(presenter: SwapRouteDetailsPresenterProtocol, 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 = SwapRouteDetailsViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupLocalization() + + presenter.setup() + } + + private func setupLocalization() { + rootView.titleView.bind( + topValue: R.string.localizable.swapsDetailsRoute( + preferredLanguages: selectedLocale.rLanguages + ), + bottomValue: R.string.localizable.swapRouteDetailsSubtitle( + preferredLanguages: selectedLocale.rLanguages + ) + ) + } +} + +extension SwapRouteDetailsViewController: SwapRouteDetailsViewProtocol { + func didReceive(viewModel: SwapRouteDetailsViewModel) { + rootView.routeDetailsView.bind(viewModel: viewModel) + } +} + +extension SwapRouteDetailsViewController: Localizable { + func applyLocalization() { + if isViewLoaded { + setupLocalization() + } + } +} diff --git a/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsViewFactory.swift b/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsViewFactory.swift new file mode 100644 index 0000000000..63cc6012c0 --- /dev/null +++ b/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsViewFactory.swift @@ -0,0 +1,36 @@ +import Foundation +import SoraFoundation + +struct SwapRouteDetailsViewFactory { + static func createView( + for quote: AssetExchangeQuote, + fee: AssetExchangeFee, + state: SwapTokensFlowStateProtocol + ) -> SwapRouteDetailsViewProtocol? { + guard let currencyManager = CurrencyManager.shared else { return nil } + + let prices = (try? state.assetListObservable.state.value.priceResult?.get()) ?? [:] + + let viewModelFactory = SwapRouteDetailsViewModelFactory( + priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: currencyManager), + priceStore: state.priceStore + ) + + let presenter = SwapRouteDetailsPresenter( + quote: quote, + fee: fee, + prices: prices, + viewModelFactory: viewModelFactory, + localizationManager: LocalizationManager.shared + ) + + let view = SwapRouteDetailsViewController( + presenter: presenter, + localizationManager: LocalizationManager.shared + ) + + presenter.view = view + + return view + } +} diff --git a/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsViewLayout.swift b/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsViewLayout.swift new file mode 100644 index 0000000000..c8e4dd0fc3 --- /dev/null +++ b/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsViewLayout.swift @@ -0,0 +1,21 @@ +import UIKit + +final class SwapRouteDetailsViewLayout: ScrollableContainerLayoutView { + let titleView: MultiValueView = .create { view in + view.valueTop.apply(style: .boldTitle1Primary) + view.valueTop.textAlignment = .left + view.valueBottom.textAlignment = .left + view.valueBottom.apply(style: .regularSubhedlineSecondary) + view.valueBottom.numberOfLines = 0 + view.spacing = 8 + } + + let routeDetailsView = SwapRouteDetailsView() + + override func setupLayout() { + super.setupLayout() + + addArrangedSubview(titleView, spacingAfter: 24) + addArrangedSubview(routeDetailsView) + } +} diff --git a/novawallet/Modules/Swaps/RouteDetails/View/SwapRouteDetailsItemView.swift b/novawallet/Modules/Swaps/RouteDetails/View/SwapRouteDetailsItemView.swift new file mode 100644 index 0000000000..61c06b7721 --- /dev/null +++ b/novawallet/Modules/Swaps/RouteDetails/View/SwapRouteDetailsItemView.swift @@ -0,0 +1,110 @@ +import UIKit + +final class SwapRouteDetailsItemView: GenericBorderedView { + override init(frame: CGRect) { + super.init(frame: frame) + + configure() + } + + private func configure() { + contentInsets = UIEdgeInsets(verticalInset: 12, horizontalInset: 16) + backgroundView.cornerRadius = 12 + } +} + +final class SwapRouteDetailsItemContent: GenericMultiValueView< + GenericPairValueView< + RouteView, + GenericTitleValueView> + > +> { + var titleLabel: UILabel { valueTop } + + var amountView: RouteView { valueBottom.fView } + + var feeView: UILabel { valueBottom.sView.titleView } + + var networkView: RouteView { valueBottom.sView.valueView } + + override init(frame: CGRect) { + super.init(frame: frame) + + configure() + } + + private func configure() { + titleLabel.apply(style: .regularSubhedlinePrimary) + titleLabel.textAlignment = .left + + feeView.apply(style: .caption1Secondary) + + spacing = 12 + valueBottom.setVerticalAndSpacing(12) + } + + private func configureAmountItemsConstraints(_ items: [AssetAmountRouteItemView]) { + items.dropLast().forEach { itemView in + itemView.amountLabel.setContentHuggingPriority(.high, for: .horizontal) + itemView.amountLabel.setContentCompressionResistancePriority(.high, for: .horizontal) + } + + guard let lastView = items.last else { return } + + lastView.amountLabel.setContentHuggingPriority(.low, for: .horizontal) + lastView.amountLabel.setContentCompressionResistancePriority(.low, for: .horizontal) + } + + private func configureAmountSeparatorConstraints(_ separators: [SwapRouteSeparatorView]) { + separators.forEach { separator in + separator.contentMode = .center + separator.setContentHuggingPriority(.required, for: .horizontal) + separator.setContentCompressionResistancePriority(.required, for: .horizontal) + } + } + + private func configureNetworkSeparatorConstraints(_ separators: [SwapRouteSeparatorView]) { + separators.forEach { separator in + separator.contentMode = .scaleAspectFit + + separator.snp.remakeConstraints { make in + make.width.equalTo(12) + } + } + } + + func bind(viewModel: ViewModel) { + titleLabel.text = viewModel.type + + amountView.bind( + items: viewModel.amountItems, + itemStyle: AssetAmountRouteItemView.Style( + imageSize: 24, + amountStyle: .semiboldBodyPrimary, + spacing: 4 + ), + separatorStyle: R.image.iconForward() + ) + + feeView.text = viewModel.fee + + networkView.bind( + items: viewModel.networkItems, + itemStyle: .caption1Secondary, + separatorStyle: R.image.iconForward() + ) + + configureAmountItemsConstraints(amountView.getItems()) + configureAmountSeparatorConstraints(amountView.getSeparators()) + configureNetworkSeparatorConstraints(networkView.getSeparators()) + } +} + +extension SwapRouteDetailsItemContent { + struct ViewModel { + let type: String + let amountItems: [AssetAmountRouteItemView.ViewModel] + let fee: String + let networkItems: [LabelRouteItemView.ViewModel] + } +} diff --git a/novawallet/Modules/Swaps/RouteDetails/View/SwapRouteDetailsView.swift b/novawallet/Modules/Swaps/RouteDetails/View/SwapRouteDetailsView.swift new file mode 100644 index 0000000000..4d9ca167ba --- /dev/null +++ b/novawallet/Modules/Swaps/RouteDetails/View/SwapRouteDetailsView.swift @@ -0,0 +1,102 @@ +import UIKit + +final class SwapRouteDetailsView: UIView { + private var itemListView: UIStackView? + private var stepsLineView: LinePatternView? + private var stepViews: [BorderedLabelView] = [] + + func bind(viewModel: SwapRouteDetailsViewModel) { + updateItemsView(for: viewModel) + updateLineView() + updateStepViews() + } +} + +private extension SwapRouteDetailsView { + func updateItemsView(for itemViewModels: SwapRouteDetailsViewModel) { + itemListView?.removeFromSuperview() + + let itemViews = itemViewModels.map { viewModel in + let itemView = SwapRouteDetailsItemView() + itemView.contentView.bind(viewModel: viewModel) + return itemView + } + + let itemsView = UIView.vStack( + alignment: .fill, + distribution: .fill, + spacing: 12, + margins: nil, + itemViews + ) + + addSubview(itemsView) + + itemsView.snp.makeConstraints { make in + make.top.bottom.equalToSuperview() + make.leading.equalToSuperview().inset(Constants.stepsContainerWidth + Constants.itemsHorOffset) + make.trailing.equalToSuperview() + } + + itemListView = itemsView + } + + func updateLineView() { + stepsLineView?.removeFromSuperview() + stepsLineView = nil + + guard + let items = itemListView?.arrangedSubviews, + let itemFirst = items.first, + let itemLast = items.last else { + return + } + + let lineView = LinePatternView() + addSubview(lineView) + + lineView.snp.makeConstraints { make in + make.leading.equalToSuperview() + make.width.equalTo(Constants.stepsContainerWidth) + make.top.equalTo(itemFirst.snp.top).offset(Constants.stepsTopOffset) + make.bottom.equalTo(itemLast.snp.top).offset(Constants.stepsTopOffset) + } + + stepsLineView = lineView + } + + func updateStepViews() { + stepViews.forEach { $0.removeFromSuperview() } + stepViews = [] + + guard let stepsLineView, let itemListView else { return } + + itemListView.arrangedSubviews.enumerated().forEach { index, itemView in + let stepView = BorderedLabelView() + stepView.apply(style: .stepNumber) + stepView.backgroundView.cornerRadius = Constants.stepWidth / 2 + stepView.titleLabel.text = String(index + 1) + stepView.titleLabel.textAlignment = .center + stepView.contentInsets = .zero + addSubview(stepView) + stepViews.append(stepView) + + stepView.snp.makeConstraints { make in + make.centerX.equalTo(stepsLineView) + make.width.height.equalTo(Constants.stepWidth) + make.top.equalTo(itemView).offset(Constants.stepsTopOffset) + } + } + } +} + +extension SwapRouteDetailsView { + enum Constants { + static let stepsContainerWidth: CGFloat = 24 + static let stepWidth: CGFloat = 20 + static let stepsTopOffset: CGFloat = 11 + static let itemsHorOffset: CGFloat = 18 + } +} + +typealias SwapRouteDetailsViewModel = [SwapRouteDetailsItemContent.ViewModel] diff --git a/novawallet/Modules/Swaps/RouteDetails/ViewModel/SwapRouteDetailsViewModelFactory.swift b/novawallet/Modules/Swaps/RouteDetails/ViewModel/SwapRouteDetailsViewModelFactory.swift new file mode 100644 index 0000000000..da64beba12 --- /dev/null +++ b/novawallet/Modules/Swaps/RouteDetails/ViewModel/SwapRouteDetailsViewModelFactory.swift @@ -0,0 +1,142 @@ +import Foundation + +protocol SwapRouteDetailsViewModelFactoryProtocol { + func createViewModel( + for operation: AssetExchangeMetaOperationProtocol, + fee: AssetExchangeOperationFee, + locale: Locale + ) -> SwapRouteDetailsItemContent.ViewModel +} + +final class SwapRouteDetailsViewModelFactory { + let assetIconViewModelFactory: AssetIconViewModelFactoryProtocol + let balanceViewModelFacade: BalanceViewModelFactoryFacadeProtocol + let priceAssetInfoFactory: PriceAssetInfoFactoryProtocol + let priceStore: AssetExchangePriceStoring + + init( + priceAssetInfoFactory: PriceAssetInfoFactoryProtocol, + assetIconViewModelFactory: AssetIconViewModelFactoryProtocol = AssetIconViewModelFactory(), + priceStore: AssetExchangePriceStoring + ) { + self.priceAssetInfoFactory = priceAssetInfoFactory + balanceViewModelFacade = BalanceViewModelFactoryFacade(priceAssetInfoFactory: priceAssetInfoFactory) + self.assetIconViewModelFactory = assetIconViewModelFactory + self.priceStore = priceStore + } +} + +private extension SwapRouteDetailsViewModelFactory { + func createType( + from operation: AssetExchangeMetaOperationProtocol, + locale: Locale + ) -> String { + switch operation.label { + case .swap: + R.string.localizable.swapsLabelSwap(preferredLanguages: locale.rLanguages) + case .transfer: + R.string.localizable.swapsLabelTransfer(preferredLanguages: locale.rLanguages) + } + } + + func createAmountItem( + from chainAsset: ChainAsset, + amount: Balance, + locale: Locale + ) -> AssetAmountRouteItemView.ViewModel { + let assetDisplayInfo = chainAsset.assetDisplayInfo + + let imageViewModel = assetIconViewModelFactory.createAssetIconViewModel( + from: assetDisplayInfo + ) + + let amount = balanceViewModelFacade.amountFromValue( + targetAssetInfo: assetDisplayInfo, + value: amount.decimal(assetInfo: assetDisplayInfo) + ).value(for: locale) + + return AssetAmountRouteItemView.ViewModel(imageViewModel: imageViewModel, amount: amount) + } + + func createAmountItems( + from operation: AssetExchangeMetaOperationProtocol, + locale: Locale + ) -> [AssetAmountRouteItemView.ViewModel] { + switch operation.label { + case .swap: + [ + createAmountItem( + from: operation.assetIn, + amount: operation.amountIn, + locale: locale + ), + createAmountItem( + from: operation.assetOut, + amount: operation.amountOut, + locale: locale + ) + ] + case .transfer: + [ + createAmountItem( + from: operation.assetOut, + amount: operation.amountOut, + locale: locale + ) + ] + } + } + + func createNetworkItems(from operation: AssetExchangeMetaOperationProtocol) -> [LabelRouteItemView.ViewModel] { + switch operation.label { + case .swap: + [ + operation.assetIn.chain.name + ] + case .transfer: + [ + operation.assetIn.chain.name, + operation.assetOut.chain.name + ] + } + } + + func createFee( + from fee: AssetExchangeOperationFee, + chain: ChainModel, + locale: Locale + ) -> String { + let totalAmountInFiat = fee.totalInFiat(in: chain, priceStore: priceStore) + + let amount = balanceViewModelFacade.priceFromFiatAmount( + totalAmountInFiat, + currencyId: priceStore.getCurrencyId() + ).value(for: locale) + + return R.string.localizable.commonFeeAmountPrefixed( + amount, + preferredLanguages: locale.rLanguages + ) + } +} + +extension SwapRouteDetailsViewModelFactory: SwapRouteDetailsViewModelFactoryProtocol { + func createViewModel( + for operation: AssetExchangeMetaOperationProtocol, + fee: AssetExchangeOperationFee, + locale: Locale + ) -> SwapRouteDetailsItemContent.ViewModel { + let fee = createFee( + from: fee, + chain: operation.assetIn.chain, + locale: locale + ) + + return SwapRouteDetailsItemContent.ViewModel( + type: createType(from: operation, locale: locale), + amountItems: createAmountItems(from: operation, locale: locale), + fee: fee, + networkItems: createNetworkItems(from: operation) + ) + } +} diff --git a/novawallet/Modules/Swaps/Setup/Model/SwapIssueCheckParams.swift b/novawallet/Modules/Swaps/Setup/Model/SwapIssueCheckParams.swift index f2f55686c4..c002ad4a92 100644 --- a/novawallet/Modules/Swaps/Setup/Model/SwapIssueCheckParams.swift +++ b/novawallet/Modules/Swaps/Setup/Model/SwapIssueCheckParams.swift @@ -9,5 +9,6 @@ struct SwapIssueCheckParams { let receiveAssetBalance: AssetBalance? let payAssetExistense: AssetBalanceExistence? let receiveAssetExistense: AssetBalanceExistence? - let quoteResult: Result? + let quoteResult: Result? + let fee: AssetExchangeFee? } diff --git a/novawallet/Modules/Swaps/Setup/Model/SwapIssueViewModelFactory.swift b/novawallet/Modules/Swaps/Setup/Model/SwapIssueViewModelFactory.swift index 371f75d4d8..40ff95fa07 100644 --- a/novawallet/Modules/Swaps/Setup/Model/SwapIssueViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Setup/Model/SwapIssueViewModelFactory.swift @@ -28,14 +28,19 @@ final class SwapIssueViewModelFactory { } func detectInsufficientBalance(in model: SwapIssueCheckParams) -> SwapSetupViewIssue? { - if let payAmount = model.payAmount, - let payChainAsset = model.payChainAsset, - let balance = model.payAssetBalance?.transferable.decimal(precision: payChainAsset.asset.precision), - payAmount > balance { - return .insufficientBalance - } else { + guard + let payAmount = model.payAmount, + payAmount > 0, + let payChainAsset = model.payChainAsset + else { return nil } + + let assetDisplayInfo = payChainAsset.assetDisplayInfo + let balance = model.payAssetBalance?.transferable.decimal(assetInfo: assetDisplayInfo) ?? 0 + let fee = model.fee?.totalFeeInAssetIn(payChainAsset).decimal(assetInfo: assetDisplayInfo) ?? 0 + + return payAmount + fee > balance ? .insufficientBalance : nil } func detectMinBalanceViolationOnReceive(in model: SwapIssueCheckParams, locale: Locale) -> SwapSetupViewIssue? { @@ -45,7 +50,7 @@ final class SwapIssueViewModelFactory { let minBalance = model.receiveAssetExistense?.minBalance.decimal( precision: receiveChainAsset.asset.precision ), - let beforeSwapBalance = model.receiveAssetBalance?.freeInPlank.decimal( + let beforeSwapBalance = model.receiveAssetBalance?.balanceCountingEd.decimal( precision: receiveChainAsset.asset.precision ) else { return nil diff --git a/novawallet/Modules/Swaps/Setup/Model/SwapModels.swift b/novawallet/Modules/Swaps/Setup/Model/SwapModels.swift index ae33aa71ef..fd98c4cce3 100644 --- a/novawallet/Modules/Swaps/Setup/Model/SwapModels.swift +++ b/novawallet/Modules/Swaps/Setup/Model/SwapModels.swift @@ -22,11 +22,6 @@ struct SwapSetupInitState { } } -struct SwapSetupFeeIdentifier: Equatable { - let transactionId: String - let feeChainAssetId: ChainAssetId? -} - struct DepositOperationModel { let operation: TokenOperation let active: Bool diff --git a/novawallet/Modules/Swaps/Setup/Model/SwapPreferredFeeAssetModel.swift b/novawallet/Modules/Swaps/Setup/Model/SwapPreferredFeeAssetModel.swift new file mode 100644 index 0000000000..db81b788dd --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/Model/SwapPreferredFeeAssetModel.swift @@ -0,0 +1,83 @@ +import Foundation + +struct SwapPreferredFeeAssetModel { + let payChainAsset: ChainAsset + let feeChainAsset: ChainAsset + let utilityChainAsset: ChainAsset + let utilityAssetBalance: AssetBalance? + let payAssetBalance: AssetBalance? + let utilityExistenceBalance: AssetBalanceExistence + let feeModel: AssetExchangeFee + let canPayFeeInPayAsset: Bool + + init?( + payChainAsset: ChainAsset?, + feeChainAsset: ChainAsset?, + utilityAssetBalance: AssetBalance?, + payAssetBalance: AssetBalance?, + utilityExistenceBalance: AssetBalanceExistence?, + feeModel: AssetExchangeFee?, + canPayFeeInPayAsset: Bool + ) { + guard + let payChainAsset, + let feeChainAsset, + let utilityChainAsset = feeChainAsset.chain.utilityChainAsset(), + let utilityExistenceBalance, + let feeModel else { + return nil + } + + self.payChainAsset = payChainAsset + self.feeChainAsset = feeChainAsset + self.utilityChainAsset = utilityChainAsset + self.utilityAssetBalance = utilityAssetBalance + self.payAssetBalance = payAssetBalance + self.utilityExistenceBalance = utilityExistenceBalance + self.feeModel = feeModel + self.canPayFeeInPayAsset = canPayFeeInPayAsset + } + + private var isFeeInNativeAsset: Bool { + feeChainAsset.chainAssetId == utilityChainAsset.chainAssetId + } + + private var hasPayAssetBalance: Bool { + let balance = payAssetBalance?.transferable ?? 0 + + return balance > 0 + } + + private func canPayFeeInNativeAsset() -> Bool { + let fee = feeModel.originFeeInAsset(utilityChainAsset) + + let balanceCountingEd = utilityAssetBalance?.balanceCountingEd ?? 0 + + let comparingBalance = min( + utilityAssetBalance?.transferable ?? 0, + balanceCountingEd.subtractOrZero(utilityExistenceBalance.minBalance) + ) + + return comparingBalance >= fee + } +} + +extension SwapPreferredFeeAssetModel { + func deriveNewFeeAsset() -> ChainAsset { + guard payChainAsset.chainAssetId != utilityChainAsset.chainAssetId else { + return utilityChainAsset + } + + if isFeeInNativeAsset { + if canPayFeeInNativeAsset() { + return utilityChainAsset + } else if hasPayAssetBalance, canPayFeeInPayAsset { + return payChainAsset + } else { + return utilityChainAsset + } + } else { + return canPayFeeInPayAsset && hasPayAssetBalance ? payChainAsset : utilityChainAsset + } + } +} diff --git a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift index d5db672668..fa6dcbb807 100644 --- a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift @@ -6,7 +6,7 @@ protocol SwapsSetupViewModelFactoryProtocol: SwapBaseViewModelFactoryProtocol, S func payTitleViewModel( assetDisplayInfo: AssetBalanceDisplayInfo?, - maxValue: BigUInt?, + maxValue: Decimal?, locale: Locale ) -> TitleHorizontalMultiValueView.Model @@ -28,14 +28,6 @@ protocol SwapsSetupViewModelFactoryProtocol: SwapBaseViewModelFactoryProtocol, S locale: Locale ) -> AmountInputViewModelProtocol - func feeViewModel( - amount: BigUInt, - assetDisplayInfo: AssetBalanceDisplayInfo, - isEditable: Bool, - priceData: PriceData?, - locale: Locale - ) -> NetworkFeeInfoViewModel - func amountFromValue(_ decimal: Decimal, chainAsset: ChainAsset, locale: Locale) -> String } @@ -46,6 +38,7 @@ final class SwapsSetupViewModelFactory: SwapBaseViewModelFactory { init( balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol, + priceAssetInfoFactory: PriceAssetInfoFactoryProtocol, issuesViewModelFactory: SwapIssueViewModelFactoryProtocol, networkViewModelFactory: NetworkViewModelFactoryProtocol, assetIconViewModelFactory: AssetIconViewModelFactoryProtocol, @@ -58,6 +51,7 @@ final class SwapsSetupViewModelFactory: SwapBaseViewModelFactory { super.init( balanceViewModelFactoryFacade: balanceViewModelFactoryFacade, + priceAssetInfoFactory: priceAssetInfoFactory, percentForamatter: percentForamatter, priceDifferenceConfig: priceDifferenceConfig ) @@ -136,7 +130,7 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { func payTitleViewModel( assetDisplayInfo: AssetBalanceDisplayInfo?, - maxValue: BigUInt?, + maxValue: Decimal?, locale: Locale ) -> TitleHorizontalMultiValueView.Model { let title = R.string.localizable.swapsSetupAssetSelectPayTitle( @@ -144,13 +138,9 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { ) if let assetDisplayInfo = assetDisplayInfo, let maxValue = maxValue { - let amountDecimal = Decimal.fromSubstrateAmount( - maxValue, - precision: Int16(assetDisplayInfo.assetPrecision) - ) ?? 0 let maxValueString = balanceViewModelFactoryFacade.amountFromValue( targetAssetInfo: assetDisplayInfo, - value: amountDecimal + value: maxValue ).value(for: locale) return .init( @@ -221,26 +211,6 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { ).value(for: locale) } - func feeViewModel( - amount: BigUInt, - assetDisplayInfo: AssetBalanceDisplayInfo, - isEditable: Bool, - priceData: PriceData?, - locale: Locale - ) -> NetworkFeeInfoViewModel { - let amountDecimal = Decimal.fromSubstrateAmount( - amount, - precision: assetDisplayInfo.assetPrecision - ) ?? 0 - let balanceViewModel = balanceViewModelFactoryFacade.balanceFromPrice( - targetAssetInfo: assetDisplayInfo, - amount: amountDecimal, - priceData: priceData - ).value(for: locale) - - return .init(isEditable: isEditable, balanceViewModel: balanceViewModel) - } - func amountFromValue(_ decimal: Decimal, chainAsset: ChainAsset, locale: Locale) -> String { balanceViewModelFactoryFacade.amountFromValue( targetAssetInfo: chainAsset.assetDisplayInfo, diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift index 6295be1cf0..c9be8ef836 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift @@ -10,32 +10,7 @@ final class SwapSetupInteractor: SwapBaseInteractor { private var remoteSubscription: CallbackBatchStorageSubscription? - init( - flowState: AssetConversionFlowFacadeProtocol, - assetConversionAggregatorFactory: AssetConversionAggregationFactoryProtocol, - chainRegistry: ChainRegistryProtocol, - assetStorageFactory: AssetStorageInfoOperationFactoryProtocol, - priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, - walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, - storageRepository: AnyDataProviderRepository, - currencyManager: CurrencyManagerProtocol, - selectedWallet: MetaAccountModel, - operationQueue: OperationQueue - ) { - self.storageRepository = storageRepository - - super.init( - flowState: flowState, - assetConversionAggregator: assetConversionAggregatorFactory, - chainRegistry: chainRegistry, - assetStorageFactory: assetStorageFactory, - priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, - walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, - currencyManager: currencyManager, - selectedWallet: selectedWallet, - operationQueue: operationQueue - ) - } + private var requoteChange = Debouncer(delay: 4) weak var presenter: SwapSetupInteractorOutputProtocol? { basePresenter as? SwapSetupInteractorOutputProtocol @@ -43,19 +18,19 @@ final class SwapSetupInteractor: SwapBaseInteractor { private var receiveChainAsset: ChainAsset? { didSet { - updateSubscriptions(activeChainAssets: activeChainAssets) + clearSubscriptionsByAssets(activeChainAssets) } } private var payChainAsset: ChainAsset? { didSet { - updateSubscriptions(activeChainAssets: activeChainAssets) + clearSubscriptionsByAssets(activeChainAssets) } } private var feeChainAsset: ChainAsset? { didSet { - updateSubscriptions(activeChainAssets: activeChainAssets) + clearSubscriptionsByAssets(activeChainAssets) } } @@ -70,9 +45,46 @@ final class SwapSetupInteractor: SwapBaseInteractor { ) } + private var activePriceIds: Set { + Set( + [ + receiveChainAsset?.asset.priceId, + payChainAsset?.asset.priceId, + feeChainAsset?.asset.priceId, + feeChainAsset?.chain.utilityAsset()?.priceId + ].compactMap { $0 } + ) + } + + init( + state: SwapTokensFlowStateProtocol, + chainRegistry: ChainRegistryProtocol, + assetStorageFactory: AssetStorageInfoOperationFactoryProtocol, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, + storageRepository: AnyDataProviderRepository, + currencyManager: CurrencyManagerProtocol, + selectedWallet: MetaAccountModel, + operationQueue: OperationQueue, + logger: LoggerProtocol + ) { + self.storageRepository = storageRepository + + super.init( + state: state, + chainRegistry: chainRegistry, + assetStorageFactory: assetStorageFactory, + walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, + currencyManager: currencyManager, + selectedWallet: selectedWallet, + operationQueue: operationQueue, + logger: logger + ) + } + deinit { canPayFeeInAssetCall.cancel() clearRemoteSubscription() + requoteChange.cancel() } private func provideCanPayFee(for asset: ChainAsset) { @@ -84,7 +96,7 @@ final class SwapSetupInteractor: SwapBaseInteractor { return } - let wrapper = assetConversionAggregator.createCanPayFeeWrapper(in: asset) + let wrapper = assetsExchangeService.canPayFee(in: asset) executeCancellable( wrapper: wrapper, @@ -93,10 +105,14 @@ final class SwapSetupInteractor: SwapBaseInteractor { runningCallbackIn: .main ) { [weak self] result in switch result { - case let .success(canPayFee): - self?.presenter?.didReceiveCanPayFeeInPayAsset(canPayFee, chainAssetId: asset.chainAssetId) + case let .success(isFeeSupported): + self?.presenter?.didReceiveCanPayFeeInPayAsset( + isFeeSupported, + chainAssetId: asset.chainAssetId + ) case let .failure(error): - self?.presenter?.didReceive(setupError: .payAssetSetFailed(error)) + self?.logger.error("Unexpected error: \(error)") + self?.presenter?.didReceiveCanPayFeeInPayAsset(false, chainAssetId: asset.chainAssetId) } } } @@ -106,7 +122,7 @@ final class SwapSetupInteractor: SwapBaseInteractor { remoteSubscription = nil } - private func setupRemoteSubscription(for chain: ChainModel) throws { + private func setupRemoteSubscription(for chain: ChainModel) { guard let accountId = selectedWallet.fetch(for: chain.accountRequest())?.accountId, let connection = chainRegistry.getConnection(for: chain.chainId), @@ -114,66 +130,61 @@ final class SwapSetupInteractor: SwapBaseInteractor { return } - let localKeyFactory = LocalStorageKeyFactory() - - let accountInfoKey = try localKeyFactory.createFromStoragePath( - .account, - accountId: accountId, - chainId: chain.chainId - ) - let accountInfoRequest = BatchStorageSubscriptionRequest( - innerRequest: MapSubscriptionRequest( - storagePath: .account, - localKey: accountInfoKey, - keyParamClosure: { - BytesCodable(wrappedValue: accountId) + do { + let localKeyFactory = LocalStorageKeyFactory() + + let accountInfoKey = try localKeyFactory.createFromStoragePath( + SystemPallet.accountPath, + accountId: accountId, + chainId: chain.chainId + ) + + let accountInfoRequest = BatchStorageSubscriptionRequest( + innerRequest: MapSubscriptionRequest( + storagePath: SystemPallet.accountPath, + localKey: accountInfoKey, + keyParamClosure: { + BytesCodable(wrappedValue: accountId) + } + ), + mappingKey: nil + ) + + remoteSubscription = CallbackBatchStorageSubscription( + requests: [accountInfoRequest], + connection: connection, + runtimeService: runtimeService, + repository: storageRepository, + operationQueue: operationQueue, + callbackQueue: .main, + callbackClosure: { _ in + // we are listening remote subscription via database } - ), - mappingKey: nil - ) - - remoteSubscription = CallbackBatchStorageSubscription( - requests: [accountInfoRequest], - connection: connection, - runtimeService: runtimeService, - repository: storageRepository, - operationQueue: operationQueue, - callbackQueue: .main, - callbackClosure: { _ in - // we are listening remote subscription via database - } - ) + ) - remoteSubscription?.subscribe() + remoteSubscription?.subscribe() + } catch { + logger.error("Unexpected error: \(error)") + } } - override func updateChain(with newChain: ChainModel) { - let oldChainId = currentChain?.chainId + override func setupReQuoteSubscription(for _: ChainAssetId, assetOut _: ChainAssetId) { + requoteChange.cancel() - super.updateChain(with: newChain) - - if newChain.chainId != oldChainId { - do { - clearRemoteSubscription() - try setupRemoteSubscription(for: newChain) - } catch { - presenter?.didReceive(setupError: .remoteSubscription(error)) + assetsExchangeService.subscribeRequoteService( + for: self, + ignoreIfAlreadyAdded: true, + notifyingIn: .main + ) { [weak self] in + self?.requoteChange.debounce { + self?.presenter?.didReceiveQuoteDataChanged() } } } - override func setupReQuoteSubscription(for assetIn: ChainAssetId, assetOut: ChainAssetId) { - if - let reQuoteService = flowState.getReQuoteService(for: assetIn, assetOut: assetOut), - !reQuoteService.hasSubscription(for: self) { - reQuoteService.subscribeSyncState( - self, - queue: .main - ) { [weak self] oldIsSyncing, newIsSyncing in - if oldIsSyncing, !newIsSyncing { - self?.presenter?.didReceiveQuoteDataChanged() - } - } + override func performUpdateOnGraphChange() { + if let payChainAsset { + provideCanPayFee(for: payChainAsset) } } } @@ -182,36 +193,35 @@ extension SwapSetupInteractor: SwapSetupInteractorInputProtocol { func update(receiveChainAsset: ChainAsset?) { self.receiveChainAsset = receiveChainAsset receiveChainAsset.map { - set(receiveChainAsset: $0) + setReceiveChainAssetSubscriptions($0) } + + assetsExchangeService.throttleRequoteService() } func update(payChainAsset: ChainAsset?) { + guard self.payChainAsset?.chainAssetId != payChainAsset?.chainAssetId else { + return + } + + clearRemoteSubscription() + self.payChainAsset = payChainAsset if let payChainAsset = payChainAsset { - set(payChainAsset: payChainAsset) + setupRemoteSubscription(for: payChainAsset.chain) + + setPayChainAssetSubscriptions(payChainAsset) provideCanPayFee(for: payChainAsset) } + + assetsExchangeService.throttleRequoteService() } func update(feeChainAsset: ChainAsset?) { self.feeChainAsset = feeChainAsset feeChainAsset.map { - set(feeChainAsset: $0) - } - } - - func retryRemoteSubscription() { - guard let chain = currentChain else { - return - } - - do { - clearRemoteSubscription() - try setupRemoteSubscription(for: chain) - } catch { - presenter?.didReceive(setupError: .remoteSubscription(error)) + setFeeChainAssetSubscriptions($0) } } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 6453e134fa..f0ea73fb7b 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -24,9 +24,7 @@ final class SwapSetupPresenter: SwapBasePresenter { private var receiveChainAsset: ChainAsset? private var feeChainAsset: ChainAsset? - private var feeIdentifier: SwapSetupFeeIdentifier? private var slippage: BigRational - private var isManualFeeSet: Bool = false private var detailsAvailable: Bool { !quoteResult.hasError() && quoteArgs != nil @@ -44,6 +42,7 @@ final class SwapSetupPresenter: SwapBasePresenter { wireframe: SwapSetupWireframeProtocol, viewModelFactory: SwapsSetupViewModelFactoryProtocol, dataValidatingFactory: SwapDataValidatorFactoryProtocol, + priceStore: AssetExchangePriceStoring, localizationManager: LocalizationManagerProtocol, selectedWallet: MetaAccountModel, slippageConfig: SlippageConfig, @@ -62,6 +61,7 @@ final class SwapSetupPresenter: SwapBasePresenter { super.init( selectedWallet: selectedWallet, dataValidatingFactory: dataValidatingFactory, + priceStore: priceStore, logger: logger ) @@ -75,7 +75,7 @@ final class SwapSetupPresenter: SwapBasePresenter { return nil } - let maxAmount = getMaxModel()?.calculate() ?? 0 + let maxAmount = getMaxModel().calculate() return payAmountInput.absoluteValue(from: maxAmount) } @@ -99,48 +99,19 @@ final class SwapSetupPresenter: SwapBasePresenter { slippage } - override func shouldHandleQuote(for args: AssetConversion.QuoteArgs?) -> Bool { + override func shouldHandleRoute(for args: AssetConversion.QuoteArgs?) -> Bool { quoteArgs == args } - override func shouldHandleFee(for feeIdentifier: TransactionFeeId, feeChainAssetId: ChainAssetId?) -> Bool { - self.feeIdentifier == SwapSetupFeeIdentifier(transactionId: feeIdentifier, feeChainAssetId: feeChainAssetId) - } - override func estimateFee() { - guard let quote = quote, - let receiveChain = receiveChainAsset?.chain, - let accountId = selectedWallet.fetch(for: receiveChain.accountRequest())?.accountId, - let quoteArgs = quoteArgs else { - return - } - - let args = AssetConversion.CallArgs( - assetIn: quote.assetIn, - amountIn: quote.amountIn, - assetOut: quote.assetOut, - amountOut: quote.amountOut, - receiver: accountId, - direction: quoteArgs.direction, - slippage: slippage, - context: quote.context - ) - - let newIdentifier = SwapSetupFeeIdentifier( - transactionId: args.identifier, - feeChainAssetId: feeChainAsset?.chainAssetId - ) - - guard newIdentifier != feeIdentifier || fee == nil else { + guard let quote, let feeChainAsset else { return } fee = nil provideFeeViewModel() - provideNotification() - feeIdentifier = newIdentifier - interactor.calculateFee(args: args) + interactor.calculateFee(for: quote.route, slippage: slippage, feeAsset: feeChainAsset) } override func applySwapMax() { @@ -164,42 +135,45 @@ final class SwapSetupPresenter: SwapBasePresenter { provideDetailsViewModel() } - override func handleNewQuote(_ quote: AssetConversion.Quote, for quoteArgs: AssetConversion.QuoteArgs) { + override func handleNewQuote(_ quote: AssetExchangeQuote, for quoteArgs: AssetConversion.QuoteArgs) { logger.debug("New quote: \(quote)") + if let fee, !quote.hasSamePath(other: fee.route) { + // we need to keep fee in sync with quote + self.fee = nil + maxCorrectionCounter.resetCounter() + } + switch quoteArgs.direction { case .buy: let payAmount = payChainAsset.map { - Decimal.fromSubstrateAmount( - quote.amountIn, - precision: Int16($0.asset.precision) - ) ?? 0 + quote.route.quote.decimal(assetInfo: $0.asset.displayInfo) } + payAmountInput = payAmount.map { .absolute($0) } providePayAmountInputViewModel() providePayInputPriceViewModel() provideReceiveInputPriceViewModel() case .sell: receiveAmountInput = receiveChainAsset.map { - Decimal.fromSubstrateAmount( - quote.amountOut, - precision: $0.asset.displayInfo.assetPrecision - ) ?? 0 + quote.route.quote.decimal(assetInfo: $0.asset.displayInfo) } + provideReceiveAmountInputViewModel() provideReceiveInputPriceViewModel() providePayInputPriceViewModel() } provideRateViewModel() + provideRouteViewModel() + provideExecutionTimeViewModel() provideButtonState() provideDetailsViewModel() estimateFee() } override func handleNewFee( - _: AssetConversion.FeeModel?, - transactionFeeId _: TransactionFeeId, + _: AssetExchangeFee?, feeChainAssetId _: ChainAssetId? ) { provideFeeViewModel() @@ -221,24 +195,24 @@ final class SwapSetupPresenter: SwapBasePresenter { provideButtonState() provideIssues() - provideNotification() + provideFeeViewModel() + provideRouteViewModel() + providePayTitle() switchFeeChainAssetIfNecessary() } - override func handleNewPrice(_: PriceData?, chainAssetId: ChainAssetId) { - if payChainAsset?.chainAssetId == chainAssetId { + override func handleNewPrice(_: PriceData?, priceId: AssetModel.PriceId) { + if payChainAsset?.asset.priceId == priceId { providePayInputPriceViewModel() } - if receiveChainAsset?.chainAssetId == chainAssetId { + if receiveChainAsset?.asset.priceId == priceId { provideReceiveInputPriceViewModel() } - if feeChainAsset?.chainAssetId == chainAssetId { + if feeChainAsset?.asset.priceId == priceId { provideFeeViewModel() } - - provideNotification() } override func handleNewBalance(_: AssetBalance?, for chainAsset: ChainAssetId) { @@ -263,6 +237,7 @@ final class SwapSetupPresenter: SwapBasePresenter { provideButtonState() } + providePayTitle() provideIssues() } @@ -281,8 +256,8 @@ extension SwapSetupPresenter { return nil } - let maxAmount = getMaxModel()?.calculate() - return input.absoluteValue(from: maxAmount ?? 0) + let maxAmount = getMaxModel().calculate() + return input.absoluteValue(from: maxAmount) } func getIssueParams() -> SwapIssueCheckParams { @@ -295,16 +270,26 @@ extension SwapSetupPresenter { receiveAssetBalance: receiveAssetBalance, payAssetExistense: payAssetBalanceExistense, receiveAssetExistense: receiveAssetBalanceExistense, - quoteResult: quoteResult + quoteResult: quoteResult, + fee: fee ) } private func providePayTitle() { - let payTitleViewModel = viewModelFactory.payTitleViewModel( - assetDisplayInfo: payChainAsset?.assetDisplayInfo, - maxValue: payAssetBalance?.transferable, - locale: selectedLocale - ) + let payTitleViewModel = if let payChainAsset, payAssetBalance != nil { + viewModelFactory.payTitleViewModel( + assetDisplayInfo: payChainAsset.assetDisplayInfo, + maxValue: getMaxModel().calculate(), + locale: selectedLocale + ) + } else { + viewModelFactory.payTitleViewModel( + assetDisplayInfo: nil, + maxValue: nil, + locale: selectedLocale + ) + } + view?.didReceiveTitle(payViewModel: payTitleViewModel) } @@ -391,12 +376,12 @@ extension SwapSetupPresenter { ) let differenceViewModel: DifferenceViewModel? - if let quote = quote, let payAssetDisplayInfo = payChainAsset?.assetDisplayInfo { + if let quote, let payAssetDisplayInfo = payChainAsset?.assetDisplayInfo { let params = RateParams( assetDisplayInfoIn: payAssetDisplayInfo, assetDisplayInfoOut: assetDisplayInfo, - amountIn: quote.amountIn, - amountOut: quote.amountOut + amountIn: quote.route.amountIn, + amountOut: quote.route.amountOut ) differenceViewModel = viewModelFactory.priceDifferenceViewModel( @@ -453,7 +438,7 @@ extension SwapSetupPresenter { guard let assetDisplayInfoIn = payChainAsset?.assetDisplayInfo, let assetDisplayInfoOut = receiveChainAsset?.assetDisplayInfo, - let quote = quote else { + let quote else { view?.didReceiveRate(viewModel: .loading) return } @@ -461,8 +446,8 @@ extension SwapSetupPresenter { from: .init( assetDisplayInfoIn: assetDisplayInfoIn, assetDisplayInfoOut: assetDisplayInfoOut, - amountIn: quote.amountIn, - amountOut: quote.amountOut + amountIn: quote.route.amountIn, + amountOut: quote.route.amountOut ), locale: selectedLocale ) @@ -470,50 +455,55 @@ extension SwapSetupPresenter { view?.didReceiveRate(viewModel: .loaded(value: rateViewModel)) } - private func provideFeeViewModel() { - guard quoteArgs != nil, let feeChainAsset = feeChainAsset else { + private func provideRouteViewModel() { + guard let quote, fee != nil else { + view?.didReceiveRoute(viewModel: .loading) return } - guard let fee = fee?.networkFee.targetAmount else { + + let viewModel = viewModelFactory.routeViewModel(from: quote.metaOperations) + + view?.didReceiveRoute(viewModel: .loaded(value: viewModel)) + } + + private func provideFeeViewModel() { + guard + let operations = quote?.metaOperations, + let totalFeeInFiat = fee?.calculateTotalFeeInFiat( + matching: operations, + priceStore: priceStore + ) else { view?.didReceiveNetworkFee(viewModel: .loading) return } - let isEditable = (payChainAsset?.isUtilityAsset == false) && canPayFeeInPayAsset + let viewModel = viewModelFactory.feeViewModel( - amount: fee, - assetDisplayInfo: feeChainAsset.assetDisplayInfo, - isEditable: isEditable, - priceData: feeAssetPriceData, + amountInFiat: totalFeeInFiat, + isEditable: false, + currencyId: feeAssetPriceData?.currencyId, locale: selectedLocale ) view?.didReceiveNetworkFee(viewModel: .loaded(value: viewModel)) } - private func provideIssues() { - let issues = viewModelFactory.detectIssues(in: getIssueParams(), locale: selectedLocale) - view?.didReceive(issues: issues) - } - - private func provideNotification() { - guard - let networkFeeAddition = fee?.networkNativeFeeAddition, - let feeChainAsset = feeChainAsset, - !feeChainAsset.isUtilityAsset, - let utilityChainAsset = feeChainAsset.chain.utilityChainAsset() else { - view?.didSetNotification(message: nil) + private func provideExecutionTimeViewModel() { + guard let quote else { + view?.didReceiveExecutionTime(viewModel: .loading) return } - let message = viewModelFactory.minimalBalanceSwapForFeeMessage( - for: networkFeeAddition, - feeChainAsset: feeChainAsset, - utilityChainAsset: utilityChainAsset, - utilityPriceData: prices[utilityChainAsset.chainAssetId], + let viewModel = viewModelFactory.executionTimeViewModel( + from: quote.totalExecutionTime(), locale: selectedLocale ) - view?.didSetNotification(message: message) + view?.didReceiveExecutionTime(viewModel: .loaded(value: viewModel)) + } + + private func provideIssues() { + let issues = viewModelFactory.detectIssues(in: getIssueParams(), locale: selectedLocale) + view?.didReceive(issues: issues) } func refreshQuote(direction: AssetConversion.Direction, forceUpdate: Bool = true) { @@ -543,6 +533,8 @@ extension SwapSetupPresenter { } provideRateViewModel() + provideRouteViewModel() + provideExecutionTimeViewModel() provideFeeViewModel() } @@ -568,7 +560,6 @@ extension SwapSetupPresenter { providePayAmountInputViewModel() provideIssues() provideFeeViewModel() - provideNotification() } else { refreshQuote(direction: .sell) } @@ -595,7 +586,6 @@ extension SwapSetupPresenter { provideReceiveInputPriceViewModel() provideIssues() provideFeeViewModel() - provideNotification() } else { refreshQuote(direction: .buy) } @@ -609,7 +599,6 @@ extension SwapSetupPresenter { fee = nil provideFeeViewModel() - provideNotification() estimateFee() } @@ -621,27 +610,27 @@ extension SwapSetupPresenter { provideButtonState() provideSettingsState() provideIssues() - provideNotification() } private func switchFeeChainAssetIfNecessary() { - guard - canPayFeeInPayAsset, - !isManualFeeSet, - let payChainAsset = getPayChainAsset(), - !payChainAsset.isUtilityAsset, - let feeChainAsset = getFeeChainAsset(), - feeChainAsset.isUtilityAsset, - let feeAssetBalance = feeAssetBalance, - let payAssetBalance = payAssetBalance, - payAssetBalance.transferable > 0, - let fee = fee?.totalFee.nativeAmount, - let nativeMinBalance = utilityAssetBalanceExistense?.minBalance else { + guard let preferredFeeAssetModel = SwapPreferredFeeAssetModel( + payChainAsset: payChainAsset, + feeChainAsset: feeChainAsset, + utilityAssetBalance: utilityAssetBalance, + payAssetBalance: payAssetBalance, + utilityExistenceBalance: utilityAssetBalanceExistense, + feeModel: fee, + canPayFeeInPayAsset: canPayFeeInPayAsset + ) else { return } - if feeAssetBalance.freeInPlank < fee + nativeMinBalance { - updateFeeChainAsset(payChainAsset) + let newFeeAsset = preferredFeeAssetModel.deriveNewFeeAsset() + + if newFeeAsset.chainAssetId != feeChainAsset?.chainAssetId { + logger.debug("New fee token: \(newFeeAsset.asset.symbol)") + + updateFeeChainAsset(newFeeAsset) } } } @@ -669,9 +658,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { func selectPayToken() { wireframe.showPayTokenSelection(from: view, chainAsset: receiveChainAsset) { [weak self] chainAsset in self?.payChainAsset = chainAsset - let feeChainAsset = chainAsset.chain.utilityAsset().map { - ChainAsset(chain: chainAsset.chain, asset: $0) - } + let feeChainAsset = chainAsset.chain.utilityChainAsset() self?.feeChainAsset = feeChainAsset self?.fee = nil @@ -685,7 +672,6 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { self?.interactor.update(payChainAsset: chainAsset) self?.interactor.update(feeChainAsset: feeChainAsset) - self?.isManualFeeSet = false if let direction = self?.quoteArgs?.direction { self?.refreshQuote(direction: direction, forceUpdate: false) @@ -723,7 +709,6 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { provideReceiveInputPriceViewModel() provideButtonState() provideIssues() - provideNotification() } func updateReceiveAmount(_ amount: Decimal?) { @@ -733,7 +718,6 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { providePayInputPriceViewModel() provideButtonState() provideIssues() - provideNotification() } func flip(currentFocus: TextFieldFocus?) { @@ -743,6 +727,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { Swift.swap(&payChainAsset, &receiveChainAsset) feeChainAsset = payChainAsset?.chain.utilityChainAsset() canPayFeeInPayAsset = false + fee = nil interactor.update(payChainAsset: payChainAsset) interactor.update(receiveChainAsset: receiveChainAsset) @@ -790,36 +775,34 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { applySwapMax() } - func showFeeActions() { - guard - let payChainAsset = payChainAsset, - let utilityAsset = payChainAsset.chain.utilityChainAsset() - else { + func showFeeInfo() { + guard let quote, let fee else { return } - wireframe.showFeeAssetSelection( + wireframe.showFeeDetails( from: view, - utilityAsset: utilityAsset, - sendingAsset: payChainAsset, - currentFeeAsset: feeChainAsset, - onFeeAssetSelect: { [weak self] selectedAsset in - if selectedAsset.chainAssetId != self?.feeChainAsset?.chainAssetId { - self?.isManualFeeSet = true - } - self?.updateFeeChainAsset(selectedAsset) - } + operations: quote.metaOperations, + fee: fee ) } - func showFeeInfo() { - wireframe.showFeeInfo(from: view) - } - func showRateInfo() { wireframe.showRateInfo(from: view) } + func showRouteDetails() { + guard let quote, let fee else { + return + } + + wireframe.showRouteDetails( + from: view, + quote: quote, + fee: fee + ) + } + func proceed() { guard let swapModel = getSwapModel() else { return @@ -891,24 +874,9 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { } extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { - func didReceive(setupError: SwapSetupError) { - logger.error("Did receive setup error: \(setupError)") - - switch setupError { - case .payAssetSetFailed: - wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in - if let payChainAsset = self?.payChainAsset { - self?.interactor.update(payChainAsset: payChainAsset) - } - } - case .remoteSubscription: - wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in - self?.interactor.retryRemoteSubscription() - } - } - } - func didReceiveCanPayFeeInPayAsset(_ value: Bool, chainAssetId: ChainAssetId) { + logger.debug("Can pay fee in \(chainAssetId.assetId): \(value)") + if payChainAsset?.chainAssetId == chainAssetId { canPayFeeInPayAsset = value diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 6b23ac0cb9..7181228b68 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -12,11 +12,12 @@ protocol SwapSetupViewProtocol: ControllerBackedProtocol { func didReceiveAmountInputPrice(receiveViewModel: SwapPriceDifferenceViewModel?) func didReceiveTitle(receiveViewModel viewModel: TitleHorizontalMultiValueView.Model) func didReceiveRate(viewModel: LoadableViewModelState) + func didReceiveRoute(viewModel: LoadableViewModelState<[SwapRouteItemView.ItemViewModel]>) + func didReceiveExecutionTime(viewModel: LoadableViewModelState) func didReceiveNetworkFee(viewModel: LoadableViewModelState) func didReceiveDetailsState(isAvailable: Bool) func didReceiveSettingsState(isAvailable: Bool) func didReceive(issues: [SwapSetupViewIssue]) - func didSetNotification(message: String?) func didReceive(focus: TextFieldFocus?) func didStartLoading() func didStopLoading() @@ -30,10 +31,10 @@ protocol SwapSetupPresenterProtocol: AnyObject { func flip(currentFocus: TextFieldFocus?) func updatePayAmount(_ amount: Decimal?) func updateReceiveAmount(_ amount: Decimal?) - func showFeeActions() func showFeeInfo() func showRateInfo() func showSettings() + func showRouteDetails() func selectMaxPayAmount() func depositInsufficientToken() } @@ -43,13 +44,11 @@ protocol SwapSetupInteractorInputProtocol: SwapBaseInteractorInputProtocol { func update(receiveChainAsset: ChainAsset?) func update(payChainAsset: ChainAsset?) func update(feeChainAsset: ChainAsset?) - func retryRemoteSubscription() } protocol SwapSetupInteractorOutputProtocol: SwapBaseInteractorOutputProtocol { func didReceiveCanPayFeeInPayAsset(_ value: Bool, chainAssetId: ChainAssetId) func didReceiveQuoteDataChanged() - func didReceive(setupError: SwapSetupError) } protocol SwapSetupWireframeProtocol: SwapBaseWireframeProtocol, @@ -88,11 +87,18 @@ protocol SwapSetupWireframeProtocol: SwapBaseWireframeProtocol, destinationChainAsset: ChainAsset, locale: Locale ) -} -enum SwapSetupError: Error { - case payAssetSetFailed(Error) - case remoteSubscription(Error) + func showRouteDetails( + from view: ControllerBackedProtocol?, + quote: AssetExchangeQuote, + fee: AssetExchangeFee + ) + + func showFeeDetails( + from view: ControllerBackedProtocol?, + operations: [AssetExchangeMetaOperationProtocol], + fee: AssetExchangeFee + ) } enum SwapSetupViewIssue: Equatable { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index ed6be01c75..c3e14035d9 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -76,9 +76,9 @@ final class SwapSetupViewController: UIViewController, ViewHolder { action: #selector(rateInfoAction), for: .touchUpInside ) - rootView.networkFeeCell.valueTopButton.addTarget( + rootView.routeCell.addTarget( self, - action: #selector(changeNetworkFeeAction), + action: #selector(routeDetailsAction), for: .touchUpInside ) rootView.networkFeeCell.addTarget( @@ -158,10 +158,6 @@ final class SwapSetupViewController: UIViewController, ViewHolder { presenter.updateReceiveAmount(amount) } - @objc private func changeNetworkFeeAction() { - presenter.showFeeActions() - } - @objc private func networkFeeInfoAction() { presenter.showFeeInfo() } @@ -170,6 +166,10 @@ final class SwapSetupViewController: UIViewController, ViewHolder { presenter.showRateInfo() } + @objc private func routeDetailsAction() { + presenter.showRouteDetails() + } + @objc private func payMaxAction() { presenter.selectMaxPayAmount() } @@ -251,6 +251,14 @@ extension SwapSetupViewController: SwapSetupViewProtocol { rootView.rateCell.bind(loadableViewModel: viewModel) } + func didReceiveRoute(viewModel: LoadableViewModelState<[SwapRouteItemView.ItemViewModel]>) { + rootView.routeCell.bind(loadableRouteViewModel: viewModel) + } + + func didReceiveExecutionTime(viewModel: LoadableViewModelState) { + rootView.execTimeCell.bind(loadableViewModel: viewModel) + } + func didReceiveNetworkFee(viewModel: LoadableViewModelState) { rootView.networkFeeCell.bind(loadableViewModel: viewModel) @@ -314,14 +322,6 @@ extension SwapSetupViewController: SwapSetupViewProtocol { } } - func didSetNotification(message: String?) { - if let message = message { - rootView.displayInfoNotification(with: message) - } else { - rootView.hideNotification() - } - } - func didStartLoading() { rootView.loadableActionView.startLoading() } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift index 74c0bec356..ba1d08c5b3 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift @@ -4,19 +4,19 @@ import Operation_iOS struct SwapSetupViewFactory { static func createView( - assetListObservable: AssetListModelObservable, + state: SwapTokensFlowStateProtocol, payChainAsset: ChainAsset, swapCompletionClosure: SwapCompletionClosure? ) -> SwapSetupViewProtocol? { createView( - assetListObservable: assetListObservable, + state: state, initState: .init(payChainAsset: payChainAsset), swapCompletionClosure: swapCompletionClosure ) } static func createView( - assetListObservable: AssetListModelObservable, + state: SwapTokensFlowStateProtocol, initState: SwapSetupInitState, swapCompletionClosure: SwapCompletionClosure? ) -> SwapSetupViewProtocol? { @@ -26,32 +26,13 @@ struct SwapSetupViewFactory { return nil } - let balanceViewModelFactoryFacade = BalanceViewModelFactoryFacade( - priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: currencyManager)) + let priceInfoFactory = PriceAssetInfoFactory(currencyManager: currencyManager) + let balanceViewModelFactoryFacade = BalanceViewModelFactoryFacade(priceAssetInfoFactory: priceInfoFactory) - let generalLocalSubscriptionFactory = GeneralStorageSubscriptionFactory( - chainRegistry: ChainRegistryFacade.sharedRegistry, - storageFacade: SubstrateDataStorageFacade.shared, - operationManager: OperationManager(operationQueue: OperationManagerFacade.sharedDefaultQueue), - logger: Logger.shared - ) - - let flowState = AssetConversionFlowFacade( - wallet: selectedWallet, - chainRegistry: ChainRegistryFacade.sharedRegistry, - userStorageFacade: UserDataStorageFacade.shared, - substrateStorageFacade: SubstrateDataStorageFacade.shared, - generalSubscriptonFactory: generalLocalSubscriptionFactory, - operationQueue: OperationManagerFacade.sharedDefaultQueue - ) - - guard let interactor = createInteractor(for: flowState) else { - return nil - } + guard let interactor = createInteractor(for: state) else { return nil } let wireframe = SwapSetupWireframe( - assetListObservable: assetListObservable, - flowState: flowState, + state: state, swapCompletionClosure: swapCompletionClosure ) @@ -61,6 +42,7 @@ struct SwapSetupViewFactory { let viewModelFactory = SwapsSetupViewModelFactory( balanceViewModelFactoryFacade: balanceViewModelFactoryFacade, + priceAssetInfoFactory: priceInfoFactory, issuesViewModelFactory: issuesViewModelFactory, networkViewModelFactory: NetworkViewModelFactory(), assetIconViewModelFactory: AssetIconViewModelFactory(), @@ -79,6 +61,7 @@ struct SwapSetupViewFactory { wireframe: wireframe, viewModelFactory: viewModelFactory, dataValidatingFactory: dataValidatingFactory, + priceStore: state.priceStore, localizationManager: LocalizationManager.shared, selectedWallet: selectedWallet, slippageConfig: .defaultConfig, @@ -97,7 +80,7 @@ struct SwapSetupViewFactory { return view } - private static func createInteractor(for flowState: AssetConversionFlowFacadeProtocol) -> SwapSetupInteractor? { + private static func createInteractor(for flowState: SwapTokensFlowStateProtocol) -> SwapSetupInteractor? { guard let currencyManager = CurrencyManager.shared, let selectedWallet = SelectedWalletSettings.shared.value else { return nil @@ -106,27 +89,21 @@ struct SwapSetupViewFactory { let chainRegistry = ChainRegistryFacade.sharedRegistry let operationQueue = OperationManagerFacade.sharedDefaultQueue - let assetConversionAggregator = AssetConversionAggregationFactory( - chainRegistry: chainRegistry, - operationQueue: operationQueue - ) - let assetStorageFactory = AssetStorageInfoOperationFactory( chainRegistry: chainRegistry, operationQueue: operationQueue ) let interactor = SwapSetupInteractor( - flowState: flowState, - assetConversionAggregatorFactory: assetConversionAggregator, + state: flowState, chainRegistry: ChainRegistryFacade.sharedRegistry, assetStorageFactory: assetStorageFactory, - priceLocalSubscriptionFactory: PriceProviderFactory.shared, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, storageRepository: SubstrateRepositoryFactory().createChainStorageItemRepository(), currencyManager: currencyManager, selectedWallet: selectedWallet, - operationQueue: operationQueue + operationQueue: operationQueue, + logger: Logger.shared ) return interactor diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift index f5ede159a8..a55529d435 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift @@ -3,17 +3,14 @@ import SoraFoundation import SoraUI final class SwapSetupWireframe: SwapSetupWireframeProtocol { - let assetListObservable: AssetListModelObservable - let flowState: AssetConversionFlowFacadeProtocol + let state: SwapTokensFlowStateProtocol let swapCompletionClosure: SwapCompletionClosure? init( - assetListObservable: AssetListModelObservable, - flowState: AssetConversionFlowFacadeProtocol, + state: SwapTokensFlowStateProtocol, swapCompletionClosure: SwapCompletionClosure? ) { - self.assetListObservable = assetListObservable - self.flowState = flowState + self.state = state self.swapCompletionClosure = swapCompletionClosure } @@ -22,10 +19,12 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { chainAsset: ChainAsset?, completionHandler: @escaping (ChainAsset) -> Void ) { - guard let selectTokenView = SwapAssetsOperationViewFactory.createSelectPayTokenView( - for: assetListObservable, - chainAsset: chainAsset, - selectClosure: completionHandler + guard let selectTokenView = SwapAssetsOperationViewFactory.createSelectPayTokenViewWithState( + state, + selectionModel: .payForAsset(chainAsset), + selectClosure: { chainAsset, _ in + completionHandler(chainAsset) + } ) else { return } @@ -42,10 +41,12 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { chainAsset: ChainAsset?, completionHandler: @escaping (ChainAsset) -> Void ) { - guard let selectTokenView = SwapAssetsOperationViewFactory.createSelectReceiveTokenView( - for: assetListObservable, - chainAsset: chainAsset, - selectClosure: completionHandler + guard let selectTokenView = SwapAssetsOperationViewFactory.createSelectReceiveTokenViewWithState( + state, + selectionModel: .receivePayingWith(chainAsset), + selectClosure: { chainAsset, _ in + completionHandler(chainAsset) + } ) else { return } @@ -83,7 +84,7 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { ) { guard let confimView = SwapConfirmViewFactory.createView( initState: initState, - flowState: flowState, + flowState: state, completionClosure: swapCompletionClosure ) else { return @@ -132,7 +133,7 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { guard let bottomSheet = GetTokenOptionsViewFactory.createView( from: destinationChainAsset, - assetModelObservable: assetListObservable, + assetModelObservable: state.assetListObservable, completion: completion ) else { return @@ -151,7 +152,7 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { from: origins, to: destination, xcmTransfers: xcmTransfers, - assetListObservable: assetListObservable, + assetListObservable: state.assetListObservable, transferCompletion: nil ) else { return @@ -178,4 +179,42 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { view?.controller.present(navigationController, animated: true) } + + func showRouteDetails( + from view: ControllerBackedProtocol?, + quote: AssetExchangeQuote, + fee: AssetExchangeFee + ) { + guard + let routeDetailsView = SwapRouteDetailsViewFactory.createView( + for: quote, + fee: fee, + state: state + ) else { + return + } + + let navigationController = NovaNavigationController(rootViewController: routeDetailsView.controller) + + view?.controller.present(navigationController, animated: true) + } + + func showFeeDetails( + from view: ControllerBackedProtocol?, + operations: [AssetExchangeMetaOperationProtocol], + fee: AssetExchangeFee + ) { + guard + let routeDetailsView = SwapFeeDetailsViewFactory.createView( + for: operations, + fee: fee, + state: state + ) else { + return + } + + let navigationController = NovaNavigationController(rootViewController: routeDetailsView.controller) + + view?.controller.present(navigationController, animated: true) + } } diff --git a/novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift b/novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift index f0b171312a..c63fa02717 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift @@ -10,6 +10,24 @@ final class SwapDetailsView: CollapsableContainerView { $0.roundedBackgroundView.roundingCorners = [.topLeft, .topRight] } + let routeCell: SwapRouteViewCell = .create { + $0.titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() + $0.titleButton.imageWithTitleView?.titleFont = .regularFootnote + $0.contentInsets = .init(top: 8, left: 16, bottom: 8, right: 16) + $0.borderView.borderType = .bottom + $0.roundedBackgroundView.cornerRadius = 0 + } + + let execTimeCell: SwapInfoViewCell = .create { + $0.titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() + $0.titleButton.imageWithTitleView?.titleFont = .regularFootnote + $0.rowContentView.selectable = false + $0.isUserInteractionEnabled = false + $0.contentInsets = .init(top: 8, left: 16, bottom: 8, right: 16) + $0.borderView.borderType = .bottom + $0.roundedBackgroundView.cornerRadius = 0 + } + let networkFeeCell: SwapNetworkFeeViewCell = .create { $0.contentInsets = .init(top: 8, left: 16, bottom: 8, right: 16) $0.borderView.borderType = .none @@ -24,6 +42,6 @@ final class SwapDetailsView: CollapsableContainerView { } override var rows: [UIView] { - [rateCell, networkFeeCell] + [rateCell, routeCell, execTimeCell, networkFeeCell] } } diff --git a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift index f72eeb9212..d19611ac4a 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift @@ -39,6 +39,14 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { detailsView.rateCell } + var routeCell: SwapRouteViewCell { + detailsView.routeCell + } + + var execTimeCell: SwapInfoViewCell { + detailsView.execTimeCell + } + var networkFeeCell: SwapNetworkFeeViewCell { detailsView.networkFeeCell } @@ -47,8 +55,6 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { var receiveIssueLabel: UILabel? - var notificationView: InlineAlertView? - private func setupPayIssueLabel() -> UILabel { if let payIssueLabel = payIssueLabel { return payIssueLabel @@ -81,20 +87,6 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { return label } - private func setupNotificationView() -> InlineAlertView { - if let notificationView = notificationView { - return notificationView - } - - let view = InlineAlertView.info() - insertArrangedSubview(view, after: detailsView, spacingAfter: 8) - stackView.setCustomSpacing(16, after: detailsView) - - notificationView = view - - return view - } - override func setupStyle() { backgroundColor = R.color.colorSecondaryScreenBackground() } @@ -146,11 +138,25 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { detailsView.titleControl.titleLabel.text = R.string.localizable.swapsSetupDetailsTitle( preferredLanguages: locale.rLanguages ) + rateCell.titleButton.imageWithTitleView?.title = R.string.localizable.swapsSetupDetailsRate( preferredLanguages: locale.rLanguages) - networkFeeCell.titleButton.imageWithTitleView?.title = R.string.localizable.commonNetworkFee( - preferredLanguages: locale.rLanguages) + + routeCell.titleButton.imageWithTitleView?.title = R.string.localizable.swapsDetailsRoute( + preferredLanguages: locale.rLanguages + ) + + execTimeCell.titleButton.imageWithTitleView?.title = R.string.localizable.swapsDetailsExecTime( + preferredLanguages: locale.rLanguages + ) + + networkFeeCell.titleButton.imageWithTitleView?.title = R.string.localizable.swapsDetailsTotalFee( + preferredLanguages: locale.rLanguages + ) + rateCell.titleButton.invalidateLayout() + routeCell.titleButton.invalidateLayout() + execTimeCell.titleButton.invalidateLayout() networkFeeCell.titleButton.invalidateLayout() } @@ -200,16 +206,4 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { receiveAmountInputView.applyInput(style: .normal) } - - func displayInfoNotification(with text: String) { - let notificationView = setupNotificationView() - notificationView.contentView.detailsLabel.text = text - } - - func hideNotification() { - notificationView?.removeFromSuperview() - notificationView = nil - - stackView.setCustomSpacing(8, after: detailsView) - } } diff --git a/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift b/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift index 07d04c3e1f..b7eda845a8 100644 --- a/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift +++ b/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift @@ -4,6 +4,13 @@ import SoraFoundation typealias SwapRemoteValidatingClosure = (AssetConversion.QuoteArgs, @escaping SwapModel.QuoteValidateClosure) -> Void +struct SwapInterEDValidatingParams { + let operations: [AssetExchangeMetaOperationProtocol] + let completionClosure: SwapInterEDCheckClosure +} + +typealias SwapInterEDValidatingClosure = (SwapInterEDValidatingParams) -> Void + protocol SwapDataValidatorFactoryProtocol: BaseDataValidatingFactoryProtocol { func hasSufficientBalance( params: SwapModel, @@ -22,7 +29,13 @@ protocol SwapDataValidatorFactoryProtocol: BaseDataValidatingFactoryProtocol { func passesRealtimeQuoteValidation( params: SwapModel, remoteValidatingClosure: @escaping SwapRemoteValidatingClosure, - onQuoteUpdate: @escaping (AssetConversion.Quote) -> Void, + onQuoteUpdate: @escaping (AssetExchangeQuote) -> Void, + locale: Locale + ) -> DataValidating + + func passesIntermediateEDValidation( + params: SwapModel, + remoteValidatingClosure: @escaping SwapInterEDValidatingClosure, locale: Locale ) -> DataValidating } @@ -63,15 +76,27 @@ final class SwapDataValidatorFactory: SwapDataValidatorFactoryProtocol { case .amountToHigh: self?.presentable.presentAmountTooHigh(from: view, locale: locale) case let .feeInNativeAsset(model): - let params = SwapDisplayError.InsufficientBalanceDueFeeNativeAsset( - available: viewModelFactory.amountFromValue( - targetAssetInfo: params.payChainAsset.assetDisplayInfo, + let available: String + let fee: String + + if let utilityAsset = params.utilityChainAsset { + available = viewModelFactory.amountFromValue( + targetAssetInfo: utilityAsset.assetDisplayInfo, value: model.available - ).value(for: locale), - fee: viewModelFactory.amountFromValue( - targetAssetInfo: params.feeChainAsset.assetDisplayInfo, + ).value(for: locale) + + fee = viewModelFactory.amountFromValue( + targetAssetInfo: utilityAsset.assetDisplayInfo, value: model.fee ).value(for: locale) + } else { + available = "" + fee = "" + } + + let params = SwapDisplayError.InsufficientBalanceDueFeeNativeAsset( + available: available, + fee: fee ) self?.presentable.presentInsufficientBalance( @@ -81,8 +106,6 @@ final class SwapDataValidatorFactory: SwapDataValidatorFactoryProtocol { locale: locale ) case let .feeInPayAsset(model): - let utilityChainAsset = params.utilityChainAsset ?? params.feeChainAsset - let params = SwapDisplayError.InsufficientBalanceDueFeePayAsset( available: viewModelFactory.amountFromValue( targetAssetInfo: params.payChainAsset.assetDisplayInfo, @@ -91,16 +114,7 @@ final class SwapDataValidatorFactory: SwapDataValidatorFactoryProtocol { fee: viewModelFactory.amountFromValue( targetAssetInfo: params.feeChainAsset.assetDisplayInfo, value: model.feeInPayAsset - ).value(for: locale), - minBalanceInPayAsset: viewModelFactory.amountFromValue( - targetAssetInfo: params.payChainAsset.assetDisplayInfo, - value: model.minBalanceInPayAsset - ).value(for: locale), - minBalanceInUtilityAsset: viewModelFactory.amountFromValue( - targetAssetInfo: utilityChainAsset.assetDisplayInfo, - value: model.minBalanceInNativeAsset - ).value(for: locale), - tokenSymbol: utilityChainAsset.asset.symbol + ).value(for: locale) ) self?.presentable.presentInsufficientBalance( @@ -110,17 +124,27 @@ final class SwapDataValidatorFactory: SwapDataValidatorFactoryProtocol { locale: locale ) case let .violatingConsumers(model): - let utilityChainAsset = params.utilityChainAsset ?? params.feeChainAsset + let minBalance: String + let fee: String - let params = SwapDisplayError.InsufficientBalanceDueConsumers( - minBalance: viewModelFactory.amountFromValue( + if let utilityChainAsset = params.utilityChainAsset { + minBalance = viewModelFactory.amountFromValue( targetAssetInfo: utilityChainAsset.assetDisplayInfo, value: model.minBalance - ).value(for: locale), - fee: viewModelFactory.amountFromValue( - targetAssetInfo: params.feeChainAsset.assetDisplayInfo, + ).value(for: locale) + + fee = viewModelFactory.amountFromValue( + targetAssetInfo: utilityChainAsset.assetDisplayInfo, value: model.fee ).value(for: locale) + } else { + minBalance = "" + fee = "" + } + + let params = SwapDisplayError.InsufficientBalanceDueConsumers( + minBalance: minBalance, + fee: fee ) self?.presentable.presentInsufficientBalance( @@ -129,9 +153,20 @@ final class SwapDataValidatorFactory: SwapDataValidatorFactoryProtocol { action: swapMaxAction, locale: locale ) - } + case let .deliveryFee(model): + let minBalance = params.utilityChainAsset.map { + viewModelFactory.amountFromValue( + targetAssetInfo: $0.assetDisplayInfo, + value: model.minBalance + ).value(for: locale) + } - self?.presentable.presentNotEnoughLiquidity(from: view, locale: locale) + self?.presentable.presentMinBalanceViolatedDueDeliveryFee( + from: view, + minBalance: minBalance ?? "", + locale: locale + ) + } }, preservesCondition: { insufficientReason == nil }) @@ -177,7 +212,6 @@ final class SwapDataValidatorFactory: SwapDataValidatorFactoryProtocol { }) } - // swiftlint:disable:next function_body_length func noDustRemains( params: SwapModel, swapMaxAction: @escaping () -> Void, @@ -197,7 +231,7 @@ final class SwapDataValidatorFactory: SwapDataValidatorFactoryProtocol { switch reason { case let .swap(model): - let params = SwapDisplayError.DustRemainsDueNativeSwap( + let params = SwapDisplayError.DustRemainsDueSwap( remaining: viewModelFactory.amountFromValue( targetAssetInfo: params.payChainAsset.assetDisplayInfo, value: model.dust @@ -208,35 +242,7 @@ final class SwapDataValidatorFactory: SwapDataValidatorFactoryProtocol { ).value(for: locale) ) - errorReason = .dueNativeSwap(params) - case let .swapAndFee(model): - let utilityChainAsset = params.utilityChainAsset ?? params.feeChainAsset - - let params = SwapDisplayError.DustRemainsDueFeeSwap( - remaining: viewModelFactory.amountFromValue( - targetAssetInfo: params.payChainAsset.assetDisplayInfo, - value: model.dust - ).value(for: locale), - minBalanceOfPayAsset: viewModelFactory.amountFromValue( - targetAssetInfo: params.payChainAsset.assetDisplayInfo, - value: model.minBalance - ).value(for: locale), - fee: viewModelFactory.amountFromValue( - targetAssetInfo: params.feeChainAsset.assetDisplayInfo, - value: model.fee - ).value(for: locale), - minBalanceInPayAsset: viewModelFactory.amountFromValue( - targetAssetInfo: params.payChainAsset.assetDisplayInfo, - value: model.minBalanceInPayAsset - ).value(for: locale), - minBalanceInUtilityAsset: viewModelFactory.amountFromValue( - targetAssetInfo: utilityChainAsset.assetDisplayInfo, - value: model.minBalanceInNativeAsset - ).value(for: locale), - utilitySymbol: utilityChainAsset.asset.symbol - ) - - errorReason = .dueFeeSwap(params) + errorReason = .dueSwap(params) } self?.presentable.presentDustRemains( @@ -258,7 +264,7 @@ final class SwapDataValidatorFactory: SwapDataValidatorFactoryProtocol { func passesRealtimeQuoteValidation( params: SwapModel, remoteValidatingClosure: @escaping SwapRemoteValidatingClosure, - onQuoteUpdate: @escaping (AssetConversion.Quote) -> Void, + onQuoteUpdate: @escaping (AssetExchangeQuote) -> Void, locale: Locale ) -> DataValidating { var reason: SwapModel.InvalidQuoteReason? @@ -275,8 +281,8 @@ final class SwapDataValidatorFactory: SwapDataValidatorFactoryProtocol { switch reason { case let .rateChange(rateUpdate): let oldRate = Decimal.rateFromSubstrate( - amount1: rateUpdate.oldQuote.amountIn, - amount2: rateUpdate.oldQuote.amountOut, + amount1: rateUpdate.oldQuote.route.amountIn, + amount2: rateUpdate.oldQuote.route.amountOut, precision1: params.payChainAsset.assetDisplayInfo.assetPrecision, precision2: params.receiveChainAsset.assetDisplayInfo.assetPrecision ) ?? 0 @@ -288,8 +294,8 @@ final class SwapDataValidatorFactory: SwapDataValidatorFactoryProtocol { ).value(for: locale) let newRate = Decimal.rateFromSubstrate( - amount1: rateUpdate.newQuote.amountIn, - amount2: rateUpdate.newQuote.amountOut, + amount1: rateUpdate.newQuote.route.amountIn, + amount2: rateUpdate.newQuote.route.amountOut, precision1: params.payChainAsset.assetDisplayInfo.assetPrecision, precision2: params.receiveChainAsset.assetDisplayInfo.assetPrecision ) ?? 0 @@ -328,4 +334,67 @@ final class SwapDataValidatorFactory: SwapDataValidatorFactoryProtocol { } ) } + + func passesIntermediateEDValidation( + params: SwapModel, + remoteValidatingClosure: @escaping SwapInterEDValidatingClosure, + locale: Locale + ) -> DataValidating { + var reason: SwapInterEDNotMet? + + return AsyncErrorConditionViolation( + onError: { [weak self] in + guard + let reason, + let operations = params.quote?.metaOperations, + let viewModelFactory = self?.balanceViewModelFactoryFacade, + let view = self?.view + else { + return + } + + let operation = operations[reason.operationIndex] + let amount = operation.amountOut + let outAssetDisplayInfo = operations[reason.operationIndex].assetOut.assetDisplayInfo + + let amountString = viewModelFactory.amountFromValue( + targetAssetInfo: outAssetDisplayInfo, + value: amount.decimal(assetInfo: outAssetDisplayInfo) + ).value(for: locale) + + let minBalanceString: String = switch reason.minBalanceResult { + case let .success(minBalance): + viewModelFactory.amountFromValue( + targetAssetInfo: outAssetDisplayInfo, + value: minBalance.decimal(assetInfo: outAssetDisplayInfo) + ).value(for: locale) + case .failure: + "" + } + + self?.presentable.presentIntemediateAmountBelowMinimum( + from: view, + amount: amountString, + minAmount: minBalanceString, + locale: locale + ) + + }, + preservesCondition: { preservationCallback in + guard let operations = params.quote?.metaOperations else { + preservationCallback(true) + return + } + + let closureParams = SwapInterEDValidatingParams(operations: operations) { result in + let preserves = result == nil + reason = result + + preservationCallback(preserves) + } + + remoteValidatingClosure(closureParams) + } + ) + } } diff --git a/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift b/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift index 06f50b700a..a7afa3b34a 100644 --- a/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift +++ b/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift @@ -38,6 +38,19 @@ protocol SwapErrorPresentable: BaseErrorPresentable { minBalance: String, locale: Locale ) + + func presentMinBalanceViolatedDueDeliveryFee( + from view: ControllerBackedProtocol, + minBalance: String, + locale: Locale + ) + + func presentIntemediateAmountBelowMinimum( + from view: ControllerBackedProtocol, + amount: String, + minAmount: String, + locale: Locale + ) } extension SwapErrorPresentable where Self: AlertPresentable & ErrorPresentable { @@ -115,6 +128,21 @@ extension SwapErrorPresentable where Self: AlertPresentable & ErrorPresentable { present(message: message, title: title, closeAction: closeAction, from: view) } + func presentMinBalanceViolatedDueDeliveryFee( + from view: ControllerBackedProtocol, + minBalance: String, + locale: Locale + ) { + let title = R.string.localizable.commonErrorGeneralTitle(preferredLanguages: locale.rLanguages) + let message = R.string.localizable.swapDeliveryFeeErrorMessage( + minBalance, + preferredLanguages: locale.rLanguages + ) + let closeAction = R.string.localizable.commonClose(preferredLanguages: locale.rLanguages) + + present(message: message, title: title, closeAction: closeAction, from: view) + } + func presentInsufficientBalance( from view: ControllerBackedProtocol?, reason: SwapDisplayError.InsufficientBalance, @@ -126,18 +154,15 @@ extension SwapErrorPresentable where Self: AlertPresentable & ErrorPresentable { switch reason { case let .dueFeePayAsset(value): - message = R.string.localizable.swapsSetupErrorInsufficientBalanceFeeSwapMessage( - value.available, + message = R.string.localizable.commonNotEnoughToPayFeeMessage( value.fee, - value.minBalanceInPayAsset, - value.minBalanceInUtilityAsset, - value.tokenSymbol, + value.available, preferredLanguages: locale.rLanguages ) case let .dueFeeNativeAsset(value): - message = R.string.localizable.swapsSetupErrorInsufficientBalanceFeeNativeMessage( - value.available, + message = R.string.localizable.commonNotEnoughToPayFeeMessage( value.fee, + value.available, preferredLanguages: locale.rLanguages ) case let .dueConsumers(value): @@ -178,17 +203,7 @@ extension SwapErrorPresentable where Self: AlertPresentable & ErrorPresentable { let message: String switch reason { - case let .dueFeeSwap(value): - message = R.string.localizable.swapsDustRemainsFeePayAssetMessage( - value.minBalanceOfPayAsset, - value.fee, - value.minBalanceInPayAsset, - value.minBalanceInUtilityAsset, - value.utilitySymbol, - value.remaining, - preferredLanguages: locale.rLanguages - ) - case let .dueNativeSwap(value): + case let .dueSwap(value): message = R.string.localizable.swapsDustRemainsFeeNativeAssetMessage( value.minBalance, value.remaining, @@ -215,4 +230,22 @@ extension SwapErrorPresentable where Self: AlertPresentable & ErrorPresentable { present(viewModel: viewModel, style: .alert, from: view) } + + func presentIntemediateAmountBelowMinimum( + from view: ControllerBackedProtocol, + amount: String, + minAmount: String, + locale: Locale + ) { + let title = R.string.localizable.commonErrorGeneralTitle(preferredLanguages: locale.rLanguages) + let message = R.string.localizable.swapIntermediateTooLowAmountToStayAbowEdMessage( + amount, + minAmount, + preferredLanguages: locale.rLanguages + ) + + let closeAction = R.string.localizable.commonClose(preferredLanguages: locale.rLanguages) + + present(message: message, title: title, closeAction: closeAction, from: view) + } } diff --git a/novawallet/Modules/Swaps/Validation/SwapErrorPresentableParams.swift b/novawallet/Modules/Swaps/Validation/SwapErrorPresentableParams.swift index 4865401240..90d986e95e 100644 --- a/novawallet/Modules/Swaps/Validation/SwapErrorPresentableParams.swift +++ b/novawallet/Modules/Swaps/Validation/SwapErrorPresentableParams.swift @@ -2,9 +2,6 @@ enum SwapDisplayError { struct InsufficientBalanceDueFeePayAsset { let available: String let fee: String - let minBalanceInPayAsset: String - let minBalanceInUtilityAsset: String - let tokenSymbol: String } struct InsufficientBalanceDueFeeNativeAsset { @@ -23,22 +20,12 @@ enum SwapDisplayError { case dueConsumers(InsufficientBalanceDueConsumers) } - struct DustRemainsDueNativeSwap { + struct DustRemainsDueSwap { let remaining: String let minBalance: String } - struct DustRemainsDueFeeSwap { - let remaining: String - let minBalanceOfPayAsset: String - let fee: String - let minBalanceInPayAsset: String - let minBalanceInUtilityAsset: String - let utilitySymbol: String - } - enum DustRemains { - case dueNativeSwap(DustRemainsDueNativeSwap) - case dueFeeSwap(DustRemainsDueFeeSwap) + case dueSwap(DustRemainsDueSwap) } } diff --git a/novawallet/Modules/Swaps/Validation/SwapModel.swift b/novawallet/Modules/Swaps/Validation/SwapModel.swift index 8c8bb3573f..2cc9424ed5 100644 --- a/novawallet/Modules/Swaps/Validation/SwapModel.swift +++ b/novawallet/Modules/Swaps/Validation/SwapModel.swift @@ -14,8 +14,6 @@ struct SwapModel { struct InsufficientDuePayAssetFee { let available: Decimal let feeInPayAsset: Decimal - let minBalanceInPayAsset: Decimal - let minBalanceInNativeAsset: Decimal } struct InsufficientDueConsumers { @@ -23,10 +21,15 @@ struct SwapModel { let fee: Decimal } + struct InsufficientDueDeliveryFee { + let minBalance: Decimal + } + enum InsufficientBalanceReason { case amountToHigh(InsufficientDueBalance) case feeInNativeAsset(InsufficientDueNativeFee) case feeInPayAsset(InsufficientDuePayAssetFee) + case deliveryFee(InsufficientDueDeliveryFee) case violatingConsumers(InsufficientDueConsumers) } @@ -35,17 +38,8 @@ struct SwapModel { let minBalance: Decimal } - struct DustAfterSwapAndFee { - let dust: Decimal - let minBalance: Decimal - let fee: Decimal - let minBalanceInPayAsset: Decimal - let minBalanceInNativeAsset: Decimal - } - enum DustReason { case swap(DustAfterSwap) - case swapAndFee(DustAfterSwapAndFee) } struct CannotReceiveDueExistense { @@ -62,8 +56,8 @@ struct SwapModel { } struct InvalidQuoteDueRateChange { - let oldQuote: AssetConversion.Quote - let newQuote: AssetConversion.Quote + let oldQuote: AssetExchangeQuote + let newQuote: AssetExchangeQuote } enum InvalidQuoteReason { @@ -71,7 +65,7 @@ struct SwapModel { case noLiqudity } - typealias QuoteValidateClosure = (Result) -> Void + typealias QuoteValidateClosure = (Result) -> Void let payChainAsset: ChainAsset let receiveChainAsset: ChainAsset @@ -85,9 +79,9 @@ struct SwapModel { let receiveAssetExistense: AssetBalanceExistence? let feeAssetExistense: AssetBalanceExistence? let utilityAssetExistense: AssetBalanceExistence? - let feeModel: AssetConversion.FeeModel? + let feeModel: AssetExchangeFee? let quoteArgs: AssetConversion.QuoteArgs - let quote: AssetConversion.Quote? + let quote: AssetExchangeQuote? let slippage: BigRational let accountInfo: AccountInfo? @@ -100,120 +94,188 @@ struct SwapModel { } var payAssetTotalBalanceAfterSwap: BigUInt { - let balance = payAssetBalance?.freeInPlank ?? 0 - let fee = isFeeInPayToken ? (feeModel?.totalFee.targetAmount ?? 0) : 0 + let balance = payAssetBalance?.balanceCountingEd ?? 0 + let fee = feeModel?.totalFeeInAssetIn(payChainAsset) ?? 0 let spendingAmount = spendingAmountInPlank ?? 0 let totalSpending = spendingAmount + fee - return balance > totalSpending ? balance - totalSpending : 0 + return balance.subtractOrZero(totalSpending) } var isFeeInPayToken: Bool { payChainAsset.chainAssetId == feeChainAsset.chainAssetId } - func checkBalanceSufficiency() -> InsufficientBalanceReason? { + func checkEnoughBalanceToSpend() -> InsufficientBalanceReason? { let balance = payAssetBalance?.transferable ?? 0 - let fee = isFeeInPayToken ? (feeModel?.totalFee.targetAmount ?? 0) : 0 let swapAmount = spendingAmountInPlank ?? 0 - let totalSpending = swapAmount + fee + guard swapAmount > balance else { + return nil + } + + let model = InsufficientDueBalance( + available: balance.decimal(precision: payChainAsset.asset.precision) + ) + + return .amountToHigh(model) + } - let isViolatingConsumers = !notViolatingConsumers + var notViolatingConsumers: Bool { + guard nativeTokenProviderWillBeKilled else { + return true + } + + return !(accountInfo?.hasConsumers ?? false) + } - guard balance < totalSpending || isViolatingConsumers else { + func checkNotViolatingConsumers() -> InsufficientBalanceReason? { + guard let utilityChainAsset else { return nil } - if balance < swapAmount { - return .amountToHigh(.init(available: balance.decimal(precision: payChainAsset.asset.precision))) - } else if isViolatingConsumers { - let minBalance = utilityAssetExistense?.minBalance ?? 0 - let precision = (utilityChainAsset ?? feeChainAsset).asset.precision - let fee = feeModel?.totalFee.targetAmount ?? 0 - return .violatingConsumers( - .init( - minBalance: minBalance.decimal(precision: precision), - fee: fee.decimal(precision: precision) - ) - ) - } else if payChainAsset.isUtilityAsset { - let available = balance > fee ? balance - fee : 0 - - return .feeInNativeAsset( - .init( - available: available.decimal(precision: payChainAsset.asset.precision), - fee: fee.decimal(precision: feeChainAsset.asset.precision) - ) + guard !notViolatingConsumers else { + return nil + } + + let minBalance = utilityAssetExistense?.minBalance ?? 0 + let feeInNativeToken = feeModel?.originFeeInAsset(utilityChainAsset) ?? 0 + + let assetDisplayInfo = utilityChainAsset.assetDisplayInfo + + return .violatingConsumers( + .init( + minBalance: minBalance.decimal(assetInfo: assetDisplayInfo), + fee: feeInNativeToken.decimal(assetInfo: assetDisplayInfo) ) - } else { - let available = balance > fee ? balance - fee : 0 - - if - isFeeInPayToken, - let addition = feeModel?.networkNativeFeeAddition, - let utilityAsset = feeChainAsset.chain.utilityAsset() { - return .feeInPayAsset( - .init( - available: available.decimal(precision: payChainAsset.asset.precision), - feeInPayAsset: fee.decimal(precision: feeChainAsset.asset.precision), - minBalanceInPayAsset: addition.targetAmount.decimal(precision: payChainAsset.asset.precision), - minBalanceInNativeAsset: addition.nativeAmount.decimal(precision: utilityAsset.precision) - ) - ) - } else { - return .feeInNativeAsset( - .init( - available: available.decimal(precision: payChainAsset.asset.precision), - fee: fee.decimal(precision: feeChainAsset.asset.precision) - ) - ) - } + ) + } + + func checkEnoughBalanceToSpendAndPayFee() -> InsufficientBalanceReason? { + let balance = payAssetBalance?.transferable ?? 0 + let fee = feeModel?.totalFeeInAssetIn(payChainAsset) ?? 0 + let swapAmount = spendingAmountInPlank ?? 0 + + guard balance < swapAmount + fee else { + return nil } + + let model = InsufficientDuePayAssetFee( + available: balance.decimal(assetInfo: payChainAsset.assetDisplayInfo), + feeInPayAsset: fee.decimal(precision: payChainAsset.asset.precision) + ) + + return .feeInPayAsset(model) } - var accountWillBeKilled: Bool { - let balance: BigUInt + func checkEnoughBalanceToPayFeeInNativeBalance() -> InsufficientBalanceReason? { + guard let utilityChainAsset else { + return nil + } - if payChainAsset.isUtilityAsset { - balance = payAssetTotalBalanceAfterSwap - } else if feeChainAsset.isUtilityAsset { - let total = feeAssetBalance?.freeInPlank ?? 0 - let fee = feeModel?.totalFee.targetAmount ?? 0 - balance = total > fee ? total - fee : 0 - } else { - // if fee is paid in non native token then we will have at least ed + let balance = utilityAssetBalance?.transferable ?? 0 + let fee = feeModel?.originFeeInAsset(utilityChainAsset) ?? 0 + + guard balance < fee else { + return nil + } + + let model = InsufficientDueNativeFee( + available: balance.decimal(assetInfo: utilityChainAsset.assetDisplayInfo), + fee: fee.decimal(assetInfo: utilityChainAsset.assetDisplayInfo) + ) + + return .feeInNativeAsset(model) + } + + func checkEnoughBalanceToPayDeliveryFee() -> InsufficientBalanceReason? { + guard + let utilityChainAsset, + let feeModel, + feeModel.hasOriginPostSubmissionByAccount, + nativeTokenProviderWillBeKilled else { + return nil + } + + let minBalance = utilityAssetExistense?.minBalance.decimal( + assetInfo: utilityChainAsset.assetDisplayInfo + ) ?? 0 + + let model = InsufficientDueDeliveryFee(minBalance: minBalance) + + return .deliveryFee(model) + } + + func checkBalanceSufficiency() -> InsufficientBalanceReason? { + if let insufficient = checkEnoughBalanceToSpend() { + return insufficient + } + + if let insufficient = checkEnoughBalanceToSpendAndPayFee() { + return insufficient + } + + if let insufficient = checkEnoughBalanceToPayFeeInNativeBalance() { + return insufficient + } + + if let insufficient = checkEnoughBalanceToPayDeliveryFee() { + return insufficient + } + + if let insufficient = checkNotViolatingConsumers() { + return insufficient + } + + return nil + } + + var nativeTokenProviderWillBeKilled: Bool { + guard let utilityChainAsset else { return false } let minBalance = utilityAssetExistense?.minBalance ?? 0 - return balance < minBalance - } + if payChainAsset.isUtilityAsset { + return payAssetTotalBalanceAfterSwap < minBalance + } - var notViolatingConsumers: Bool { - guard accountWillBeKilled else { - return true + let feeInNativeAsset = feeModel?.originFeeInAsset(utilityChainAsset) ?? 0 + + guard feeInNativeAsset > 0 else { + return false } - return !(accountInfo?.hasConsumers ?? false) + let totalInNativeAsset = utilityAssetBalance?.balanceCountingEd ?? 0 + + return totalInNativeAsset.subtractOrZero(feeInNativeAsset) < minBalance } - func checkCanReceive() -> CannotReceiveReason? { - let isSelfSufficient = receiveAssetExistense?.isSelfSufficient ?? false - let amountAfterSwap = (receiveAssetBalance?.freeInPlank ?? 0) + (quote?.amountOut ?? 0) - let feeInReceiveAsset = feeChainAsset.chainAssetId == receiveChainAsset.chainAssetId ? - (feeModel?.totalFee.targetAmount ?? 0) : 0 + func checkReceiveBalanceAboveMin() -> CannotReceiveReason? { + let amountAfterSwap = (receiveAssetBalance?.balanceCountingEd ?? 0) + (quote?.route.amountOut ?? 0) let minBalance = receiveAssetExistense?.minBalance ?? 0 - if amountAfterSwap < minBalance + feeInReceiveAsset { + if amountAfterSwap < minBalance { return .existense( .init(minBalance: minBalance.decimal(precision: receiveChainAsset.asset.precision)) ) - } else if !isSelfSufficient, accountWillBeKilled { + } else { + return nil + } + } + + func checkReceiveBalanceSelfSufOrHasProvider() -> CannotReceiveReason? { + guard payChainAsset.chain.chainId == receiveChainAsset.chain.chainId, let utilityChainAsset else { + return nil + } + + let isSelfSufficient = receiveAssetExistense?.isSelfSufficient ?? false + + if !isSelfSufficient, nativeTokenProviderWillBeKilled { let utilityMinBalance = utilityAssetExistense?.minBalance ?? 0 - let precision = (utilityChainAsset ?? feeChainAsset).asset.precision + let precision = utilityChainAsset.asset.precision return .noProvider( .init(minBalance: utilityMinBalance.decimal(precision: precision)) ) @@ -222,6 +284,18 @@ struct SwapModel { } } + func checkCanReceive() -> CannotReceiveReason? { + if let cannotReceive = checkReceiveBalanceAboveMin() { + return cannotReceive + } + + if let cannotReceive = checkReceiveBalanceSelfSufOrHasProvider() { + return cannotReceive + } + + return nil + } + func checkDustAfterSwap() -> DustReason? { let balance = payAssetTotalBalanceAfterSwap let minBalance = payAssetExistense?.minBalance ?? 0 @@ -232,35 +306,21 @@ struct SwapModel { let remaning = minBalance - balance - if - isFeeInPayToken, !payChainAsset.isUtilityAsset, - let networkFee = feeModel?.networkFee, - let feeAdditions = feeModel?.networkNativeFeeAddition, - let utilityAsset = feeChainAsset.chain.utilityAsset() { - return .swapAndFee( - .init( - dust: remaning.decimal(precision: payChainAsset.asset.precision), - minBalance: minBalance.decimal(precision: payChainAsset.asset.precision), - fee: networkFee.targetAmount.decimal(precision: payChainAsset.asset.precision), - minBalanceInPayAsset: feeAdditions.targetAmount.decimal(precision: payChainAsset.asset.precision), - minBalanceInNativeAsset: feeAdditions.nativeAmount.decimal(precision: utilityAsset.precision) - ) - ) - } else { - return .swap( - .init( - dust: remaning.decimal(precision: payChainAsset.asset.precision), - minBalance: minBalance.decimal(precision: payChainAsset.asset.precision) - ) - ) - } + let assetDisplayInfo = payChainAsset.assetDisplayInfo + + let model = DustAfterSwap( + dust: remaning.decimal(assetInfo: assetDisplayInfo), + minBalance: minBalance.decimal(assetInfo: assetDisplayInfo) + ) + + return .swap(model) } func asyncCheckQuoteValidity( _ newQuoteClosure: @escaping (AssetConversion.QuoteArgs, @escaping QuoteValidateClosure) -> Void, completion: @escaping (InvalidQuoteReason?) -> Void ) { - guard let currenQuote = quote else { + guard let currentQuote = quote else { completion(.noLiqudity) return } @@ -268,12 +328,11 @@ struct SwapModel { newQuoteClosure(quoteArgs) { result in switch result { case let .success(newQuote): - if !currenQuote.matches( - other: newQuote, - slippage: slippage, - direction: quoteArgs.direction + if !currentQuote.route.matches( + otherRoute: newQuote.route, + slippage: slippage ) { - completion(.rateChange(.init(oldQuote: currenQuote, newQuote: newQuote))) + completion(.rateChange(.init(oldQuote: currentQuote, newQuote: newQuote))) } else { completion(nil) } diff --git a/novawallet/Modules/Swaps/View/AssetAmountRouteItemView.swift b/novawallet/Modules/Swaps/View/AssetAmountRouteItemView.swift new file mode 100644 index 0000000000..cbbb549c3a --- /dev/null +++ b/novawallet/Modules/Swaps/View/AssetAmountRouteItemView.swift @@ -0,0 +1,61 @@ +import Foundation +import SoraUI + +final class AssetAmountRouteItemView: AssetAmountView { + typealias ViewModel = AssetAmountRouteItemView.ItemViewModel + typealias Style = AssetAmountRouteItemView.ItemStyle + + private var imageSize: CGFloat = 24 { + didSet { + if imageSize != oldValue { + updateAssetIconLayout() + } + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + updateAssetIconLayout() + } +} + +private extension AssetAmountRouteItemView { + private func updateAssetIconLayout() { + assetIconView.snp.remakeConstraints { make in + make.size.equalTo(imageSize) + } + } +} + +extension AssetAmountRouteItemView { + struct ItemViewModel { + let imageViewModel: ImageViewModelProtocol? + let amount: String + } + + struct ItemStyle { + let imageSize: CGFloat + let amountStyle: UILabel.Style + let spacing: CGFloat + } +} + +extension AssetAmountRouteItemView: RouteItemViewProtocol { + func bind(routeItemViewModel: ViewModel) { + assetIconView.bind( + viewModel: routeItemViewModel.imageViewModel, size: CGSize(width: imageSize, height: imageSize) + ) + + amountLabel.text = routeItemViewModel.amount + } + + func apply(routeItemStyle: Style) { + imageSize = routeItemStyle.imageSize + + assetIconView.backgroundView.cornerRadius = imageSize / 2 + amountLabel.apply(style: routeItemStyle.amountStyle) + + setHorizontalAndSpacing(routeItemStyle.spacing) + } +} diff --git a/novawallet/Modules/Swaps/View/LabelRouteItemView.swift b/novawallet/Modules/Swaps/View/LabelRouteItemView.swift new file mode 100644 index 0000000000..2ee71d7fef --- /dev/null +++ b/novawallet/Modules/Swaps/View/LabelRouteItemView.swift @@ -0,0 +1,13 @@ +import UIKit + +final class LabelRouteItemView: UILabel, RouteItemViewProtocol { + typealias ViewModel = String + + func bind(routeItemViewModel: ViewModel) { + text = routeItemViewModel + } + + func apply(routeItemStyle: Style) { + apply(style: routeItemStyle) + } +} diff --git a/novawallet/Modules/Swaps/View/RouteView.swift b/novawallet/Modules/Swaps/View/RouteView.swift new file mode 100644 index 0000000000..c4ecf6892e --- /dev/null +++ b/novawallet/Modules/Swaps/View/RouteView.swift @@ -0,0 +1,114 @@ +import UIKit + +protocol RouteItemViewProtocol { + associatedtype Style + associatedtype ViewModel + + func bind(routeItemViewModel: ViewModel) + func apply(routeItemStyle: Style) +} + +protocol RouteSeparatorViewProtocol { + associatedtype Style + + func apply(routeSeparatorStyle: Style) +} + +typealias RouteItemView = UIView & RouteItemViewProtocol +typealias RouteSeparatorView = UIView & RouteSeparatorViewProtocol + +final class RouteView: UIView { + private var itemViews: [I] = [] + private var separatorViews: [S] = [] + + var spacing: CGFloat { + get { + stackView.spacing + } + + set { + stackView.spacing = newValue + } + } + + private var stackView: UIStackView = .create { view in + view.axis = .horizontal + view.spacing = 4 + } + + override init(frame: CGRect) { + super.init(frame: frame) + + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func bind(items: [I.ViewModel], itemStyle: I.Style, separatorStyle: S.Style) { + let itemsToRemove = max(itemViews.count - items.count, 0) + let itemsToAdd = max(items.count - itemViews.count, 0) + + if itemsToRemove > 0 { + let removedItems = itemViews.suffix(itemsToRemove) + itemViews = itemViews.dropLast(itemsToRemove) + + removedItems.forEach { $0.removeFromSuperview() } + + let removedSeparators = separatorViews.suffix(itemsToRemove) + separatorViews = separatorViews.dropLast(itemsToRemove) + + removedSeparators.forEach { $0.removeFromSuperview() } + } + + if itemsToAdd > 0 { + let remainedItems: Int + + if itemViews.isEmpty { + let itemView = I() + + stackView.addArrangedSubview(itemView) + itemViews.append(itemView) + + remainedItems = itemsToAdd - 1 + } else { + remainedItems = itemsToAdd + } + + (0 ..< remainedItems).forEach { _ in + let separator = S() + + stackView.addArrangedSubview(separator) + separatorViews.append(separator) + + let itemView = I() + + stackView.addArrangedSubview(itemView) + itemViews.append(itemView) + } + } + + itemViews.forEach { $0.apply(routeItemStyle: itemStyle) } + + zip(itemViews, items).forEach { $0.0.bind(routeItemViewModel: $0.1) } + + separatorViews.forEach { $0.apply(routeSeparatorStyle: separatorStyle) } + } + + func getItems() -> [I] { + itemViews + } + + func getSeparators() -> [S] { + separatorViews + } + + private func setupLayout() { + addSubview(stackView) + stackView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } +} diff --git a/novawallet/Modules/Swaps/View/SwapRouteView.swift b/novawallet/Modules/Swaps/View/SwapRouteView.swift new file mode 100644 index 0000000000..3d432b07c0 --- /dev/null +++ b/novawallet/Modules/Swaps/View/SwapRouteView.swift @@ -0,0 +1,68 @@ +import UIKit + +final class SwapRouteItemView: LoadableIconDetailsView { + typealias ViewModel = SwapRouteItemView.ItemViewModel + typealias Style = SwapRouteItemView.ItemStyle +} + +extension SwapRouteItemView: RouteItemViewProtocol { + struct ItemViewModel { + let title: String? + let icon: ImageViewModelProtocol + + var hasTitle: Bool { + if let title, !title.isEmpty { + return true + } else { + return false + } + } + } + + struct ItemStyle { + let iconSize: CGFloat + let labelStyle: UILabel.Style? + let spacing: CGFloat + + init( + iconSize: CGFloat, + labelStyle: UILabel.Style? = nil, + spacing: CGFloat = 0 + ) { + self.iconSize = iconSize + self.labelStyle = labelStyle + self.spacing = spacing + } + } + + func apply(routeItemStyle: Style) { + iconWidth = routeItemStyle.iconSize + stackView.spacing = routeItemStyle.spacing + mode = .iconDetails + + if let labelStyle = routeItemStyle.labelStyle { + detailsLabel.apply(style: labelStyle) + } + } + + func bind(routeItemViewModel: ViewModel) { + bind( + viewModel: StackCellViewModel( + details: routeItemViewModel.title ?? "", + imageViewModel: routeItemViewModel.icon + ) + ) + + detailsLabel.isHidden = !routeItemViewModel.hasTitle + } +} + +final class SwapRouteSeparatorView: UIImageView, RouteSeparatorViewProtocol { + typealias Style = UIImage? + + func apply(routeSeparatorStyle style: Style) { + image = style + } +} + +typealias SwapRouteView = RouteView diff --git a/novawallet/Modules/TransactionHistory/TransactionHistoryViewFactory.swift b/novawallet/Modules/TransactionHistory/TransactionHistoryViewFactory.swift index b6bc06042f..7dec76af0a 100644 --- a/novawallet/Modules/TransactionHistory/TransactionHistoryViewFactory.swift +++ b/novawallet/Modules/TransactionHistory/TransactionHistoryViewFactory.swift @@ -6,7 +6,8 @@ import Operation_iOS struct TransactionHistoryViewFactory { static func createView( chainAsset: ChainAsset, - operationState: AssetOperationState + operationState: AssetOperationState, + swapState: SwapTokensFlowStateProtocol ) -> TransactionHistoryViewProtocol? { guard let selectedMetaAccount = SelectedWalletSettings.shared.value, @@ -24,7 +25,8 @@ struct TransactionHistoryViewFactory { let wireframe = TransactionHistoryWireframe( chainAsset: chainAsset, - operationState: operationState + operationState: operationState, + swapState: swapState ) let balanceViewModelFactory = BalanceViewModelFactory( diff --git a/novawallet/Modules/TransactionHistory/TransactionHistoryWireframe.swift b/novawallet/Modules/TransactionHistory/TransactionHistoryWireframe.swift index d5b42bdb65..2f066f52dd 100644 --- a/novawallet/Modules/TransactionHistory/TransactionHistoryWireframe.swift +++ b/novawallet/Modules/TransactionHistory/TransactionHistoryWireframe.swift @@ -3,13 +3,16 @@ import UIKit final class TransactionHistoryWireframe: TransactionHistoryWireframeProtocol { let chainAsset: ChainAsset let operationState: AssetOperationState + let swapState: SwapTokensFlowStateProtocol init( chainAsset: ChainAsset, - operationState: AssetOperationState + operationState: AssetOperationState, + swapState: SwapTokensFlowStateProtocol ) { self.chainAsset = chainAsset self.operationState = operationState + self.swapState = swapState } func showFilter( @@ -34,7 +37,8 @@ final class TransactionHistoryWireframe: TransactionHistoryWireframeProtocol { guard let operationDetailsView = OperationDetailsViewFactory.createView( for: operation, chainAsset: chainAsset, - operationState: operationState + operationState: operationState, + swapState: swapState ) else { return } diff --git a/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferInteractor.swift b/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferInteractor.swift index c80042958f..e6cbab93c0 100644 --- a/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferInteractor.swift +++ b/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferInteractor.swift @@ -558,7 +558,7 @@ extension OnChainTransferInteractor { switch result { case let .success(available): self?.presenter?.didReceiveCustomAssetFeeAvailable(available) - case let .failure(error) where error is AssetConversionAggregationFactoryError: + case let .failure(error) where error is AssetFeePaymentError: self?.presenter?.didReceiveCustomAssetFeeAvailable(false) case let .failure(error): self?.presenter?.didReceiveError(error) diff --git a/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferPresenter.swift b/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferPresenter.swift index d22952792b..5072e6bb99 100644 --- a/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferPresenter.swift +++ b/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferPresenter.swift @@ -55,7 +55,7 @@ class OnChainTransferPresenter { } var feeAssetChangeAvailable: Bool { - chainAsset.chain.hasCustomTransferFees + chainAsset.chain.hasCustomFees && sendingAssetFeeAvailable ?? false && !isUtilityTransfer } diff --git a/novawallet/Modules/Transfer/Operation/AssetTransferAggregationFactory.swift b/novawallet/Modules/Transfer/Operation/AssetTransferAggregationFactory.swift index 7faa501e69..57d4110da8 100644 --- a/novawallet/Modules/Transfer/Operation/AssetTransferAggregationFactory.swift +++ b/novawallet/Modules/Transfer/Operation/AssetTransferAggregationFactory.swift @@ -46,6 +46,10 @@ extension AssetCanPayFeeWrapperFactoryProtocol { protocol AssetTransferAggregationFactoryProtocol: AssetCanPayFeeWrapperFactoryProtocol {} +enum AssetFeePaymentError: Error { + case unavailableProvider(ChainModel) +} + final class AssetTransferAggregationFactory: AssetTransferAggregationFactoryProtocol { let operationQueue: OperationQueue let chainRegistry: ChainRegistryProtocol @@ -59,13 +63,13 @@ final class AssetTransferAggregationFactory: AssetTransferAggregationFactoryProt } func createCanPayFeeWrapper(in chainAsset: ChainAsset) -> CompoundOperationWrapper { - if chainAsset.chain.hasAssetHubTransferFees { + if chainAsset.chain.hasAssetHubFees { return createAssetHubCanPayFee(for: chainAsset) - } else if chainAsset.chain.hasHydrationTransferFees { + } else if chainAsset.chain.hasHydrationFees { return createHydraCanPayFee(for: chainAsset) } else { return CompoundOperationWrapper.createWithError( - AssetConversionAggregationFactoryError.unavailableProvider(chainAsset.chain) + AssetFeePaymentError.unavailableProvider(chainAsset.chain) ) } } diff --git a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmCrossChainViewFactory.swift b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmCrossChainViewFactory.swift index e7e2d6ded2..c144c7470d 100644 --- a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmCrossChainViewFactory.swift +++ b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmCrossChainViewFactory.swift @@ -109,7 +109,6 @@ struct TransferConfirmCrossChainViewFactory { let storageFacade = SubstrateDataStorageFacade.shared let operationQueue = OperationManagerFacade.sharedDefaultQueue let chainRegistry = ChainRegistryFacade.sharedRegistry - let logger = Logger.shared let eventCenter = EventCenter.shared let repositoryFactory = SubstrateRepositoryFactory(storageFacade: storageFacade) @@ -117,16 +116,9 @@ struct TransferConfirmCrossChainViewFactory { let walletRemoteSubscriptionService = WalletServiceFacade.sharedSubstrateRemoteSubscriptionService let walletRemoteSubscriptionWrapper = WalletRemoteSubscriptionWrapper( - remoteSubscriptionService: walletRemoteSubscriptionService, - chainRegistry: chainRegistry, - repositoryFactory: repositoryFactory, - eventCenter: eventCenter, - operationQueue: operationQueue, - logger: logger + remoteSubscriptionService: walletRemoteSubscriptionService ) - let senderResolutionFacade = ExtrinsicSenderResolutionFacade(userStorageFacade: UserDataStorageFacade.shared) - let metadataHashOperationFactory = MetadataHashOperationFactory( metadataRepositoryFactory: RuntimeMetadataRepositoryFactory( storageFacade: SubstrateDataStorageFacade.shared @@ -137,8 +129,9 @@ struct TransferConfirmCrossChainViewFactory { let extrinsicService = XcmTransferService( wallet: wallet, chainRegistry: chainRegistry, - senderResolutionFacade: senderResolutionFacade, metadataHashOperationFactory: metadataHashOperationFactory, + userStorageFacade: UserDataStorageFacade.shared, + substrateStorageFacade: SubstrateDataStorageFacade.shared, operationQueue: operationQueue ) diff --git a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift index 38efcbc129..a89fd96bd5 100644 --- a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift +++ b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift @@ -209,12 +209,7 @@ struct TransferConfirmOnChainViewFactory { let walletRemoteSubscriptionService = WalletServiceFacade.sharedSubstrateRemoteSubscriptionService let walletRemoteSubscriptionWrapper = WalletRemoteSubscriptionWrapper( - remoteSubscriptionService: walletRemoteSubscriptionService, - chainRegistry: chainRegistry, - repositoryFactory: repositoryFactory, - eventCenter: EventCenter.shared, - operationQueue: operationQueue, - logger: Logger.shared + remoteSubscriptionService: walletRemoteSubscriptionService ) let extrinsicService = ExtrinsicServiceFactory( diff --git a/novawallet/Modules/Transfer/TransferSetup/CrossChain/TransferSetupPresenterFactory+CrossChain.swift b/novawallet/Modules/Transfer/TransferSetup/CrossChain/TransferSetupPresenterFactory+CrossChain.swift index 566d44079b..051d4a55dd 100644 --- a/novawallet/Modules/Transfer/TransferSetup/CrossChain/TransferSetupPresenterFactory+CrossChain.swift +++ b/novawallet/Modules/Transfer/TransferSetup/CrossChain/TransferSetupPresenterFactory+CrossChain.swift @@ -106,21 +106,12 @@ extension TransferSetupPresenterFactory { let operationQueue = OperationManagerFacade.sharedDefaultQueue - let repositoryFactory = SubstrateRepositoryFactory(storageFacade: storageFacade) - let walletRemoteSubscriptionService = WalletServiceFacade.sharedSubstrateRemoteSubscriptionService let walletRemoteSubscriptionWrapper = WalletRemoteSubscriptionWrapper( - remoteSubscriptionService: walletRemoteSubscriptionService, - chainRegistry: chainRegistry, - repositoryFactory: repositoryFactory, - eventCenter: eventCenter, - operationQueue: operationQueue, - logger: logger + remoteSubscriptionService: walletRemoteSubscriptionService ) - let senderResolutionFacade = ExtrinsicSenderResolutionFacade(userStorageFacade: UserDataStorageFacade.shared) - let metadataHashOperationFactory = MetadataHashOperationFactory( metadataRepositoryFactory: RuntimeMetadataRepositoryFactory( storageFacade: SubstrateDataStorageFacade.shared @@ -131,8 +122,9 @@ extension TransferSetupPresenterFactory { let extrinsicService = XcmTransferService( wallet: wallet, chainRegistry: chainRegistry, - senderResolutionFacade: senderResolutionFacade, metadataHashOperationFactory: metadataHashOperationFactory, + userStorageFacade: UserDataStorageFacade.shared, + substrateStorageFacade: SubstrateDataStorageFacade.shared, operationQueue: operationQueue ) diff --git a/novawallet/Modules/Transfer/TransferSetup/OnChain/TransferSetupPresenterFactory+OnChain.swift b/novawallet/Modules/Transfer/TransferSetup/OnChain/TransferSetupPresenterFactory+OnChain.swift index 9b936e0534..701ed88a85 100644 --- a/novawallet/Modules/Transfer/TransferSetup/OnChain/TransferSetupPresenterFactory+OnChain.swift +++ b/novawallet/Modules/Transfer/TransferSetup/OnChain/TransferSetupPresenterFactory+OnChain.swift @@ -120,17 +120,10 @@ extension TransferSetupPresenterFactory { let operationQueue = OperationManagerFacade.sharedDefaultQueue - let repositoryFactory = SubstrateRepositoryFactory(storageFacade: storageFacade) - let walletRemoteSubscriptionService = WalletServiceFacade.sharedSubstrateRemoteSubscriptionService let walletRemoteSubscriptionWrapper = WalletRemoteSubscriptionWrapper( - remoteSubscriptionService: walletRemoteSubscriptionService, - chainRegistry: chainRegistry, - repositoryFactory: repositoryFactory, - eventCenter: EventCenter.shared, - operationQueue: operationQueue, - logger: Logger.shared + remoteSubscriptionService: walletRemoteSubscriptionService ) let extrinsicService = ExtrinsicServiceFactory( diff --git a/novawallet/Modules/Vote/Governance/Operation/Fetch/Gov1OperationFactory.swift b/novawallet/Modules/Vote/Governance/Operation/Fetch/Gov1OperationFactory.swift index 1b03f5c0c7..45f8a7ba7c 100644 --- a/novawallet/Modules/Vote/Governance/Operation/Fetch/Gov1OperationFactory.swift +++ b/novawallet/Modules/Vote/Governance/Operation/Fetch/Gov1OperationFactory.swift @@ -216,7 +216,7 @@ final class Gov1OperationFactory { requestFactory.queryItem( engine: connection, factory: { try codingFactoryOperation.extractNoCancellableResultData() }, - storagePath: .blockNumber, + storagePath: SystemPallet.blockNumberPath, at: blockHash ) diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index 5803d9d535..2b2d86c683 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1397,7 +1397,6 @@ "swaps.setup.deposit.button.title" = "Get %@"; "swaps.setup.network.fee.token.title" = "Token for paying network fee"; "swaps.setup.network.fee.token.hint" = "Network fee is added on top of entered amount"; -"swaps.pay.asset.fee.ed.message" = "To pay network fee with %@, Nova will automatically swap %@ for %@ to maintain your account\'s minimum %@ balance."; "swaps.setup.slippage.error.amount.bounds" = "Enter a value between %@ and %@"; "swaps.setup.slippage.warning.low.amount" = "Transaction might be reverted because of low slippage tolerance."; "swaps.setup.slippage.warning.high.amount" = "Transaction might be frontrun because of high slippage."; @@ -1772,6 +1771,23 @@ "common.address.coppied" = "Address copied"; "common.networks" = "Networks"; "assets.search.token.hint" = "Search by token"; +"swaps.details.exec.time" = "Execution time"; +"swaps.details.route" = "Route"; +"swaps.details.total.fee" = "Total fee"; +"swaps.execution.dont.close.app" = "Do not close the app!"; +"sec.time.units" = "sec"; +"swaps.execution.transfer.details" = "Transferring %@ to %@"; +"swaps.execution.swap.details" = "Swapping %@ to %@ on %@"; +"common.of" = "%@ of %@"; +"swaps.execution.swap.failure" = "Failed on operation #%@ (%@)"; +"swaps.label.crosschain" = "Cross-chain transfer"; +"swaps.label.swap" = "Swap"; +"swaps.label.transfer" = "Transfer"; +"swap.intermediate.too.low.amount.to.stay.abow.ed.message" = "During swap execution intermediate receive amount is %@ which is less than minimum balance of %@. Try specifying larger swap amount."; +"common.fee.amount.prefixed" = "Fee: %@"; +"swap.route.details.subtitle" = "The way that your token will take through different networks to get the desired token."; +"common.not.enough.to.pay.fee.message" = "You don\'t have enough balance to pay network fee of %@. Current balance is %@"; +"swap.delivery.fee.error.message" = "Due to crosschain restrictions you should have at least %@ after operation"; "common.pay.anywhere" = "Pay anywhere"; "common.nova.card" = "Nova Card"; "common.estimated.timer" = "Estimated ~%@"; diff --git a/novawallet/en.lproj/Localizable.stringsdict b/novawallet/en.lproj/Localizable.stringsdict index b3b3c06532..c041418ef2 100644 --- a/novawallet/en.lproj/Localizable.stringsdict +++ b/novawallet/en.lproj/Localizable.stringsdict @@ -1,198 +1,230 @@ - - common.days.format - - NSStringLocalizedFormatKey - %#@format@ - format - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - li - one - %li day - other - %li days - - - common.hours.format - - NSStringLocalizedFormatKey - %#@format@ - format - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - li - one - %li hour - other - %li hours - - - common.days.left.format - - NSStringLocalizedFormatKey - %#@format@ - format - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - li - one - %li day left - other - %li days left - - - staking.analytics.validators.eras.counter - - NSStringLocalizedFormatKey - %#@format@ - format - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - li - one - %li era - other - %li eras - - - common.minutes.format - - NSStringLocalizedFormatKey - %#@format@ - format - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - li - one - %li minute - other - %li minutes - - - common.every.days.format - - NSStringLocalizedFormatKey - %#@format@ - format - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - li - one - everyday - other - every %li days - - - common.in.tracks - - NSStringLocalizedFormatKey - %#@format@ - format - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - li - one - 1 track - other - %li tracks - - - common.networks.title - - NSStringLocalizedFormatKey - %#@format@ - format - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - li - one - Network - other - Networks - - - common.unsupported.count - - NSStringLocalizedFormatKey - %#@format@ - format - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - li - one - 1 unsupported - other - %li unsupported - - - missing.accounts.warning.format - - NSStringLocalizedFormatKey - %#@format@ - format - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - li - one - %2$@ account is missing. Add account to the wallet in Settings - other - %2$@ accounts are missing. Add accounts to the wallet in Settings - - - dapps.unsupported.networks.format - - NSStringLocalizedFormatKey - %#@format@ - format - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - li - one - 1 unsupported network hidden - other - %li unsupported networks hidden - - - notifications.wallet.list.selection.hint - - NSStringLocalizedFormatKey - %#@format@ - format - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - li - one - Select at least %li wallet - other - Select at least %li wallets - - - - \ No newline at end of file + + common.days.format + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + %li day + other + %li days + + + common.hours.format + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + %li hour + other + %li hours + + + common.days.left.format + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + %li day left + other + %li days left + + + staking.analytics.validators.eras.counter + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + %li era + other + %li eras + + + common.minutes.format + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + %li minute + other + %li minutes + + + common.seconds.format + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + %li second + other + %li seconds + + + common.every.days.format + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + everyday + other + every %li days + + + common.in.tracks + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + 1 track + other + %li tracks + + + common.networks.title + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + Network + other + Networks + + + common.unsupported.count + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + 1 unsupported + other + %li unsupported + + + missing.accounts.warning.format + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + %2$@ account is missing. Add account to the wallet in Settings + other + %2$@ accounts are missing. Add accounts to the wallet in Settings + + + dapps.unsupported.networks.format + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + 1 unsupported network hidden + other + %li unsupported networks hidden + + + notifications.wallet.list.selection.hint + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + Select at least %li wallet + other + Select at least %li wallets + + + common.operations + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + %li operation + other + %li operations + + + + diff --git a/novawallet/es.lproj/Localizable.strings b/novawallet/es.lproj/Localizable.strings index a5f2e05191..47efcafc39 100644 --- a/novawallet/es.lproj/Localizable.strings +++ b/novawallet/es.lproj/Localizable.strings @@ -1396,7 +1396,6 @@ "swaps.setup.deposit.button.title" = "Obtener %@"; "swaps.setup.network.fee.token.title" = "Token para pagar la tasa de red"; "swaps.setup.network.fee.token.hint" = "La tasa de red se agrega al monto ingresado"; -"swaps.pay.asset.fee.ed.message" = "Para pagar la comisión de red con %@, Nova intercambiará automáticamente %@ por %@ para mantener el balance mínimo de %@ en tu cuenta."; "swaps.setup.slippage.error.amount.bounds" = "Ingrese un valor entre %@ y %@"; "swaps.setup.slippage.warning.low.amount" = "La transacción podría ser revertida debido a la baja tolerancia al deslizamiento."; "swaps.setup.slippage.warning.high.amount" = "La transacción podría ser adelantada debido al alto deslizamiento."; @@ -1768,4 +1767,21 @@ "wallet.receive.details.format" = "Enviar solo el token %@ y tokens en la red %@ a esta dirección, o podrías perder tus fondos"; "common.address.coppied" = "Dirección copiada"; "common.networks" = "Redes"; -"assets.search.token.hint" = "Buscar por token"; \ No newline at end of file +"assets.search.token.hint" = "Buscar por token"; +"sec.time.units" = "seg"; +"swaps.execution.dont.close.app" = "¡No cierres la aplicación!"; +"swaps.details.exec.time" = "Tiempo de ejecución"; +"swaps.details.total.fee" = "Tarifa total"; +"swaps.label.transfer" = "Transferencia"; +"swaps.label.swap" = "Intercambio"; +"swap.route.details.subtitle" = "La manera en que tu token pasará a través de diferentes redes para obtener el token deseado."; +"swaps.details.route" = "Ruta"; +"swaps.execution.transfer.details" = "Transfiriendo %@ a %@"; +"swaps.execution.swap.details" = "Intercambiando %@ por %@ en %@"; +"common.of" = "%@ de %@"; +"swaps.execution.swap.failure" = "Falló en la operación #%@ (%@)"; +"swaps.label.crosschain" = "Transferencia entre cadenas"; +"swap.intermediate.too.low.amount.to.stay.abow.ed.message" = "Durante la ejecución del swap, la cantidad recibida intermedia es %@, lo cual es menos que el saldo mínimo de %@. Intenta especificar una cantidad de swap mayor."; +"common.fee.amount.prefixed" = "Tarifa: %@"; +"common.not.enough.to.pay.fee.message" = "No tienes suficiente saldo para pagar la tarifa de red de %@. El saldo actual es %@"; +"swap.delivery.fee.error.message" = "Debido a restricciones de crosschain deberías tener al menos %@ después de la operación"; \ No newline at end of file diff --git a/novawallet/es.lproj/Localizable.stringsdict b/novawallet/es.lproj/Localizable.stringsdict index 4c28591658..57b44b579b 100644 --- a/novawallet/es.lproj/Localizable.stringsdict +++ b/novawallet/es.lproj/Localizable.stringsdict @@ -194,5 +194,37 @@ Selecciona al menos %li carteras + common.seconds.format + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + %li segundo + other + %li segundos + + + common.operations + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + %li operación + other + %li operaciones + + \ No newline at end of file diff --git a/novawallet/fr.lproj/Localizable.strings b/novawallet/fr.lproj/Localizable.strings index 0714c9fe04..f81c5846d6 100644 --- a/novawallet/fr.lproj/Localizable.strings +++ b/novawallet/fr.lproj/Localizable.strings @@ -1396,7 +1396,6 @@ "swaps.setup.deposit.button.title" = "Obtenez %@"; "swaps.setup.network.fee.token.title" = "Token pour le paiement des frais de réseau"; "swaps.setup.network.fee.token.hint" = "Les frais de réseau sont ajoutés en plus du montant saisi"; -"swaps.pay.asset.fee.ed.message" = "Pour payer les frais de réseau avec %@, Nova échangera automatiquement %@ contre %@ pour maintenir le solde minimum de %@ sur votre compte."; "swaps.setup.slippage.error.amount.bounds" = "Entrez une valeur comprise entre %@ et %@"; "swaps.setup.slippage.warning.low.amount" = "La transaction pourrait être annulée en raison d'une faible tolérance au slippage."; "swaps.setup.slippage.warning.high.amount" = "La transaction pourrait être anticipée en raison d'un slippage élevé."; @@ -1768,4 +1767,21 @@ "wallet.receive.details.format" = "Envoyez uniquement le token %@ et les tokens dans le réseau %@ à cette adresse, ou vous pourriez perdre vos fonds"; "common.address.coppied" = "Adresse copiée"; "common.networks" = "Réseaux"; -"assets.search.token.hint" = "Rechercher par token"; \ No newline at end of file +"assets.search.token.hint" = "Rechercher par token"; +"sec.time.units" = "sec"; +"swaps.execution.dont.close.app" = "Ne fermez pas l'application!"; +"swaps.details.exec.time" = "Temps d'exécution"; +"swaps.details.total.fee" = "Frais totaux"; +"swaps.label.transfer" = "Transfert"; +"swaps.label.swap" = "Échange"; +"swap.route.details.subtitle" = "La voie que prendra votre jeton à travers différents réseaux pour obtenir le jeton souhaité."; +"swaps.details.route" = "Itinéraire"; +"swaps.execution.transfer.details" = "Transfert de %@ vers %@"; +"swaps.execution.swap.details" = "Échanger %@ contre %@ sur %@"; +"common.of" = "%@ de %@"; +"swaps.execution.swap.failure" = "Échec de l'opération n°%@ (%@)"; +"swaps.label.crosschain" = "Transfert inter-chaînes"; +"swap.intermediate.too.low.amount.to.stay.abow.ed.message" = "Lors de l'exécution de l'échange, le montant intermédiaire reçu est %@, ce qui est inférieur au solde minimum de %@. Essayez de spécifier un montant d'échange plus important."; +"common.fee.amount.prefixed" = "Frais : %@"; +"common.not.enough.to.pay.fee.message" = "Vous n'avez pas assez de solde pour payer les frais de réseau de %@. Le solde actuel est %@"; +"swap.delivery.fee.error.message" = "En raison des restrictions crosschain, vous devez avoir au moins %@ après l'opération"; \ No newline at end of file diff --git a/novawallet/fr.lproj/Localizable.stringsdict b/novawallet/fr.lproj/Localizable.stringsdict index 75b7fd37c4..ba2fee3095 100644 --- a/novawallet/fr.lproj/Localizable.stringsdict +++ b/novawallet/fr.lproj/Localizable.stringsdict @@ -194,5 +194,37 @@ Sélectionnez au moins %li portefeuilles + common.seconds.format + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + %li seconde + other + %li secondes + + + common.operations + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + %li opération + other + %li opérations + + \ No newline at end of file diff --git a/novawallet/hu.lproj/Localizable.strings b/novawallet/hu.lproj/Localizable.strings index b4641f94d6..711d8c66e0 100644 --- a/novawallet/hu.lproj/Localizable.strings +++ b/novawallet/hu.lproj/Localizable.strings @@ -1396,7 +1396,6 @@ "swaps.setup.deposit.button.title" = "%@ beszerzése"; "swaps.setup.network.fee.token.title" = "Token a hálózati díj fizetéséhez"; "swaps.setup.network.fee.token.hint" = "A hálózati díj hozzáadódik a megadott összeghez"; -"swaps.pay.asset.fee.ed.message" = "A %@ tokennel történő hálózati díj fizetéséhez, Nova automatikus %@ és %@ konverziót fog végrehajtani, ahhoz, hogy fenntartsa a számla minimális egyenlegét, ami %@."; "swaps.setup.slippage.error.amount.bounds" = "Az érték %@ és %@ között kell legyen"; "swaps.setup.slippage.warning.low.amount" = "A tranzakció az alacsony csúszás miatt lehet, hogy vissza lesz fordítva."; "swaps.setup.slippage.warning.high.amount" = "A tranzakció a magas csúszás miatt lehet, hogy hamarabb for lefutni."; @@ -1769,3 +1768,20 @@ "common.address.coppied" = "Cím másolva"; "common.networks" = "Hálózatok"; "assets.search.token.hint" = "Keresés token alapján"; +"sec.time.units" = "mp"; +"swaps.execution.dont.close.app" = "Ne zárja be az alkalmazást!"; +"swaps.details.exec.time" = "Végrehajtási idő"; +"swaps.details.total.fee" = "Teljes díj"; +"swaps.label.transfer" = "Átutalás"; +"swaps.label.swap" = "Csere"; +"swap.route.details.subtitle" = "Az út, amelyen a tokenje különböző hálózatokon megy át, hogy megkapja a kívánt tokent."; +"swaps.details.route" = "Útvonal"; +"swaps.execution.transfer.details" = "Átvitel: %@ a(z) %@-ra"; +"swaps.execution.swap.details" = "Csere: %@-ról %@-ra a(z) %@-on"; +"common.of" = "%@ a(z) %@ közül"; +"swaps.execution.swap.failure" = "Sikertelen művelet: #%@ (%@)"; +"swaps.label.crosschain" = "Láncok közötti átutalás"; +"swap.intermediate.too.low.amount.to.stay.abow.ed.message" = "A csere végrehajtása során a köztes kapott összeg %@, ami kevesebb, mint a minimális egyenleg %@. Próbáljon meg nagyobb csereösszeget megadni."; +"common.fee.amount.prefixed" = "Díj: %@"; +"common.not.enough.to.pay.fee.message" = "Nincs elegendő egyenlege a hálózati díj kifizetéséhez, amely %@. Az aktuális egyenleg: %@"; +"swap.delivery.fee.error.message" = "A láncközi korlátozások miatt legalább %@-nak kell maradnia a művelet után"; \ No newline at end of file diff --git a/novawallet/hu.lproj/Localizable.stringsdict b/novawallet/hu.lproj/Localizable.stringsdict index 6d20e46f53..a9da8a20ef 100644 --- a/novawallet/hu.lproj/Localizable.stringsdict +++ b/novawallet/hu.lproj/Localizable.stringsdict @@ -194,5 +194,37 @@ Legalább %li tárcát kell választani + common.seconds.format + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + %li másodperc + other + %li másodpercek + + + common.operations + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + %li művelet + other + %li műveletek + + \ No newline at end of file diff --git a/novawallet/id.lproj/Localizable.strings b/novawallet/id.lproj/Localizable.strings index ab4e0d1075..1c0b237098 100644 --- a/novawallet/id.lproj/Localizable.strings +++ b/novawallet/id.lproj/Localizable.strings @@ -1396,7 +1396,6 @@ "swaps.setup.deposit.button.title" = "Dapatkan %@"; "swaps.setup.network.fee.token.title" = "Token untuk membayar biaya jaringan"; "swaps.setup.network.fee.token.hint" = "Biaya jaringan ditambahkan di atas jumlah yang dimasukkan"; -"swaps.pay.asset.fee.ed.message" = "Untuk membayar biaya jaringan dengan %@, Nova secara otomatis akan menukar %@ dengan %@ untuk mempertahankan saldo minimum %@ akun Anda."; "swaps.setup.slippage.error.amount.bounds" = "Masukkan nilai antara %@ dan %@"; "swaps.setup.slippage.warning.low.amount" = "Transaksi mungkin dibalikkan karena toleransi slippage yang rendah."; "swaps.setup.slippage.warning.high.amount" = "Transaksi mungkin diambil oleh pihak lain karena slippage yang tinggi."; @@ -1768,4 +1767,21 @@ "wallet.receive.details.format" = "Hanya kirim token %@ dan token di jaringan %@ ke alamat ini, atau Anda bisa kehilangan dana Anda"; "common.address.coppied" = "Alamat disalin"; "common.networks" = "Jaringan"; -"assets.search.token.hint" = "Cari berdasarkan token"; \ No newline at end of file +"assets.search.token.hint" = "Cari berdasarkan token"; +"sec.time.units" = "detik"; +"swaps.execution.dont.close.app" = "Jangan tutup aplikasinya!"; +"swaps.details.exec.time" = "Waktu eksekusi"; +"swaps.details.total.fee" = "Biaya total"; +"swaps.label.transfer" = "Transfer"; +"swaps.label.swap" = "Tukar"; +"swap.route.details.subtitle" = "Jalur yang akan diambil token Anda melalui berbagai jaringan untuk mendapatkan token yang diinginkan."; +"swaps.details.route" = "Rute"; +"swaps.execution.transfer.details" = "Mentransfer %@ ke %@"; +"swaps.execution.swap.details" = "Menukar %@ ke %@ di %@"; +"common.of" = "%@ dari %@"; +"swaps.execution.swap.failure" = "Gagal pada operasi #%@ (%@)"; +"swaps.label.crosschain" = "Transfer lintas rantai"; +"swap.intermediate.too.low.amount.to.stay.abow.ed.message" = "Selama eksekusi swap, jumlah terima sementara adalah %@ yang kurang dari saldo minimum %@. Coba tentukan jumlah swap yang lebih besar."; +"common.fee.amount.prefixed" = "Biaya: %@"; +"common.not.enough.to.pay.fee.message" = "Kamu tidak memiliki saldo yang cukup untuk membayar biaya jaringan sebesar %@. Saldo saat ini adalah %@"; +"swap.delivery.fee.error.message" = "Karena pembatasan antar-chain, kamu harus memiliki setidaknya %@ setelah operasi"; \ No newline at end of file diff --git a/novawallet/id.lproj/Localizable.stringsdict b/novawallet/id.lproj/Localizable.stringsdict index 63a87b16de..6f11920496 100644 --- a/novawallet/id.lproj/Localizable.stringsdict +++ b/novawallet/id.lproj/Localizable.stringsdict @@ -170,5 +170,33 @@ Pilih setidaknya %li dompet + common.seconds.format + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + other + %li detik + + + common.operations + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + other + %li operasi + + \ No newline at end of file diff --git a/novawallet/it.lproj/Localizable.strings b/novawallet/it.lproj/Localizable.strings index 84413cbd5b..52d0181606 100644 --- a/novawallet/it.lproj/Localizable.strings +++ b/novawallet/it.lproj/Localizable.strings @@ -1396,7 +1396,6 @@ "swaps.setup.deposit.button.title" = "Ottieni %@"; "swaps.setup.network.fee.token.title" = "Token per pagare la commissione di rete"; "swaps.setup.network.fee.token.hint" = "La commissione di rete viene aggiunta alla somma inserita"; -"swaps.pay.asset.fee.ed.message" = "Per pagare la tariffa di rete con %@, Nova effettuerà automaticamente lo scambio di %@ per %@ per mantenere il saldo minimo del tuo account di %@."; "swaps.setup.slippage.error.amount.bounds" = "Inserisci un valore compreso tra %@ e %@"; "swaps.setup.slippage.warning.low.amount" = "La transazione potrebbe essere annullata a causa di una bassa tolleranza per lo scivolamento."; "swaps.setup.slippage.warning.high.amount" = "La transazione potrebbe essere front-runner a causa di un elevato scivolamento."; @@ -1768,4 +1767,21 @@ "wallet.receive.details.format" = "Invia solo token %@ e token nella rete %@ a questo indirizzo, altrimenti potresti perdere i tuoi fondi"; "common.address.coppied" = "Indirizzo copiato"; "common.networks" = "Reti"; -"assets.search.token.hint" = "Cerca per token"; \ No newline at end of file +"assets.search.token.hint" = "Cerca per token"; +"sec.time.units" = "sec"; +"swaps.execution.dont.close.app" = "Non chiudere l'app!"; +"swaps.details.exec.time" = "Tempo di esecuzione"; +"swaps.details.total.fee" = "Commissione totale"; +"swaps.label.transfer" = "Trasferimento"; +"swaps.label.swap" = "Scambio"; +"swap.route.details.subtitle" = "Il percorso che il tuo token seguirà attraverso diverse reti per ottenere il token desiderato."; +"swaps.details.route" = "Percorso"; +"swaps.execution.transfer.details" = "Trasferimento di %@ a %@"; +"swaps.execution.swap.details" = "Scambiando %@ con %@ su %@"; +"common.of" = "%@ di %@"; +"swaps.execution.swap.failure" = "Operazione fallita #%@ (%@)"; +"swaps.label.crosschain" = "Trasferimento tra catene"; +"swap.intermediate.too.low.amount.to.stay.abow.ed.message" = "Durante l'esecuzione dello swap, l'importo intermedio ricevuto è %@ che è inferiore al saldo minimo di %@. Prova a specificare un importo di swap maggiore."; +"common.fee.amount.prefixed" = "Tassa: %@"; +"common.not.enough.to.pay.fee.message" = "Non hai abbastanza saldo per pagare la tassa di rete di %@. Il saldo attuale è %@"; +"swap.delivery.fee.error.message" = "A causa delle restrizioni crosschain dovresti avere almeno %@ dopo l'operazione"; \ No newline at end of file diff --git a/novawallet/it.lproj/Localizable.stringsdict b/novawallet/it.lproj/Localizable.stringsdict index 36a311274d..e3a23b31a2 100644 --- a/novawallet/it.lproj/Localizable.stringsdict +++ b/novawallet/it.lproj/Localizable.stringsdict @@ -194,5 +194,37 @@ Seleziona almeno %li portafogli + common.seconds.format + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + %li secondo + other + %li secondi + + + common.operations + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + %li operazione + other + %li operazioni + + \ No newline at end of file diff --git a/novawallet/ja.lproj/Localizable.strings b/novawallet/ja.lproj/Localizable.strings index 6d4729fdc3..42a8e6e67e 100644 --- a/novawallet/ja.lproj/Localizable.strings +++ b/novawallet/ja.lproj/Localizable.strings @@ -1396,7 +1396,6 @@ "swaps.setup.deposit.button.title" = "%@を取得"; "swaps.setup.network.fee.token.title" = "ネットワーク手数料を支払うトークン"; "swaps.setup.network.fee.token.hint" = "入力された金額の上にネットワーク手数料が追加されます"; -"swaps.pay.asset.fee.ed.message" = "ネットワーク手数料を %@ で支払うため、Novaは %@ を %@ に自動的にスワップして、アカウントの最低 %@ 残高を維持します。"; "swaps.setup.slippage.error.amount.bounds" = "%@から%@の範囲内の値を入力してください"; "swaps.setup.slippage.warning.low.amount" = "低スリッページ許容のため、トランザクションがリバートされる可能性があります。"; "swaps.setup.slippage.warning.high.amount" = "高いスリッページのため、トランザクションがフロントランされる可能性があります。"; @@ -1768,4 +1767,21 @@ "wallet.receive.details.format" = "送信するのは%@トークンと%@ネットワークのトークンのみ可能です、それ以外の場合、資金を失う可能性があります"; "common.address.coppied" = "住所がコピーされました"; "common.networks" = "ネットワーク"; -"assets.search.token.hint" = "トークンで検索"; \ No newline at end of file +"assets.search.token.hint" = "トークンで検索"; +"sec.time.units" = "秒"; +"swaps.execution.dont.close.app" = "アプリを閉じないでください!"; +"swaps.details.exec.time" = "実行時間"; +"swaps.details.total.fee" = "合計手数料"; +"swaps.label.transfer" = "転送"; +"swaps.label.swap" = "スワップ"; +"swap.route.details.subtitle" = "異なるネットワークを通じて希望するトークンを得るためにトークンが通る経路です。"; +"swaps.details.route" = "ルート"; +"swaps.execution.transfer.details" = "%@を%@へ転送中"; +"swaps.execution.swap.details" = "Swapping %@ to %@ on %@"; +"common.of" = "%@ の %@"; +"swaps.execution.swap.failure" = "操作#%@ (%@) 上で失敗しました"; +"swaps.label.crosschain" = "クロスチェーントランスファー"; +"swap.intermediate.too.low.amount.to.stay.abow.ed.message" = "スワップ実行中に中間の受取額が%@で、最小残高%@を下回っています。より大きなスワップ額を指定してみてください。"; +"common.fee.amount.prefixed" = "手数料: %@"; +"common.not.enough.to.pay.fee.message" = "ネットワーク手数料%@を支払うための残高が不足しています。現在の残高は%@"; +"swap.delivery.fee.error.message" = "クロスチェーンの制限により、操作後に少なくとも%@を保持している必要があります"; \ No newline at end of file diff --git a/novawallet/ja.lproj/Localizable.stringsdict b/novawallet/ja.lproj/Localizable.stringsdict index 1fff105730..d91d0c4e9c 100644 --- a/novawallet/ja.lproj/Localizable.stringsdict +++ b/novawallet/ja.lproj/Localizable.stringsdict @@ -170,5 +170,33 @@ %liウォレットを少なくとも選択してください + common.seconds.format + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + other + %li秒 + + + common.operations + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + other + %li操作 + + \ No newline at end of file diff --git a/novawallet/ko.lproj/Localizable.strings b/novawallet/ko.lproj/Localizable.strings index f3e3db2731..b0291e000f 100644 --- a/novawallet/ko.lproj/Localizable.strings +++ b/novawallet/ko.lproj/Localizable.strings @@ -1396,7 +1396,6 @@ "swaps.setup.deposit.button.title" = "%@ 받기"; "swaps.setup.network.fee.token.title" = "네트워크 수수료를 지불할 토큰"; "swaps.setup.network.fee.token.hint" = "입력한 금액 외에 네트워크 수수료가 추가됩니다"; -"swaps.pay.asset.fee.ed.message" = "%@로 네트워크 수수료를 지불하기 위해, Nova는 계정의 최소 %@ 잔액을 유지하기 위해 자동으로 %@를 %@로 교환합니다."; "swaps.setup.slippage.error.amount.bounds" = "%@와 %@ 사이의 값을 입력하세요"; "swaps.setup.slippage.warning.low.amount" = "낮은 슬리피지 허용량으로 인해 거래가 되돌릴 수 있습니다."; "swaps.setup.slippage.warning.high.amount" = "높은 슬리피지로 인해 거래가 프론트런 당할 수 있습니다."; @@ -1768,4 +1767,21 @@ "wallet.receive.details.format" = "이 주소로 %@ 토큰과 %@ 네트워크에서의 토큰만 보내세요, 그렇지 않으면 자금을 잃을 수 있습니다."; "common.address.coppied" = "주소가 복사되었습니다"; "common.networks" = "네트워크"; -"assets.search.token.hint" = "토큰으로 검색"; \ No newline at end of file +"assets.search.token.hint" = "토큰으로 검색"; +"sec.time.units" = "초"; +"swaps.execution.dont.close.app" = "앱을 닫지 마세요!"; +"swaps.details.exec.time" = "실행 시간"; +"swaps.details.total.fee" = "총 수수료"; +"swaps.label.transfer" = "전송"; +"swaps.label.swap" = "스왑"; +"swap.route.details.subtitle" = "당신의 토큰이 원하는 토큰을 얻기 위해 다른 네트워크를 통해 이동하는 방식입니다."; +"swaps.details.route" = "경로"; +"swaps.execution.transfer.details" = "%@로 옮기는 중"; +"swaps.execution.swap.details" = "%@를 %@로 %@에서 스왑하는 중"; +"common.of" = "%@의 %@"; +"swaps.execution.swap.failure" = "작업 #%@ (%@)에서 실패"; +"swaps.label.crosschain" = "크로스체인 전송"; +"swap.intermediate.too.low.amount.to.stay.abow.ed.message" = "스왑 실행 중 중간 수신 금액이 %@로 최소 잔액 %@보다 적습니다. 더 큰 스왑 금액을 지정해 보세요."; +"common.fee.amount.prefixed" = "수수료: %@"; +"common.not.enough.to.pay.fee.message" = "네트워크 수수료 %@를 지불할 충분한 잔액이 없습니다. 현재 잔액은 %@입니다"; +"swap.delivery.fee.error.message" = "크로스체인 제한으로 인해 작업 후 최소 %@ 이상의 잔액이 있어야 합니다"; \ No newline at end of file diff --git a/novawallet/ko.lproj/Localizable.stringsdict b/novawallet/ko.lproj/Localizable.stringsdict index 9f9c588293..e6e14133ff 100644 --- a/novawallet/ko.lproj/Localizable.stringsdict +++ b/novawallet/ko.lproj/Localizable.stringsdict @@ -170,5 +170,33 @@ 최소 %li 개의 지갑을 선택하세요 + common.seconds.format + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + other + %li초 + + + common.operations + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + other + %li 작업 + + \ No newline at end of file diff --git a/novawallet/pl.lproj/Localizable.strings b/novawallet/pl.lproj/Localizable.strings index ad89cb12f5..8d5cf12dff 100644 --- a/novawallet/pl.lproj/Localizable.strings +++ b/novawallet/pl.lproj/Localizable.strings @@ -1396,7 +1396,6 @@ "swaps.setup.deposit.button.title" = "Odbierz %@"; "swaps.setup.network.fee.token.title" = "Token do opłacenia opłaty sieciowej"; "swaps.setup.network.fee.token.hint" = "Opłata sieciowa jest dodawana do wprowadzonej kwoty"; -"swaps.pay.asset.fee.ed.message" = "Aby zapłacić opłatę sieciową za pomocą %@, Nova automatycznie zamieni %@ na %@, aby utrzymać minimalne saldo %@ na twoim koncie."; "swaps.setup.slippage.error.amount.bounds" = "Wprowadź wartość pomiędzy %@ a %@"; "swaps.setup.slippage.warning.low.amount" = "Transakcja może zostać anulowana z powodu niskiej tolerancji na poślizg."; "swaps.setup.slippage.warning.high.amount" = "Transakcja może zostać przechwycona z powodu wysokiego poślizgu."; @@ -1768,4 +1767,21 @@ "wallet.receive.details.format" = "Wysyłaj tylko tokeny %@ i tokeny w sieci %@ na ten adres, w przeciwnym razie możesz stracić swoje środki"; "common.address.coppied" = "Adres skopiowany"; "common.networks" = "Sieci"; -"assets.search.token.hint" = "Wyszukaj według tokena"; \ No newline at end of file +"assets.search.token.hint" = "Wyszukaj według tokena"; +"sec.time.units" = "sek"; +"swaps.execution.dont.close.app" = "Nie zamykaj aplikacji!"; +"swaps.details.exec.time" = "Czas wykonania"; +"swaps.details.total.fee" = "Łączna opłata"; +"swaps.label.transfer" = "Transfer"; +"swaps.label.swap" = "Swap"; +"swap.route.details.subtitle" = "Droga, którą twoje tokeny przejdą przez różne sieci, aby uzyskać żądany token."; +"swaps.details.route" = "Trasa"; +"swaps.execution.transfer.details" = "Przesyłanie %@ do %@"; +"swaps.execution.swap.details" = "Wymiana %@ na %@ na %@"; +"common.of" = "%@ z %@"; +"swaps.execution.swap.failure" = "Niepowodzenie operacji #%@ (%@)"; +"swaps.label.crosschain" = "Transfer międzyłańcuchowy"; +"swap.intermediate.too.low.amount.to.stay.abow.ed.message" = "Podczas wykonywania swapu pośrednia kwota otrzymana wynosi %@, co jest mniej niż minimalny stan konta wynoszący %@. Spróbuj określić większą kwotę swapu."; +"common.fee.amount.prefixed" = "Opłata: %@"; +"common.not.enough.to.pay.fee.message" = "Nie masz wystarczającej ilości środków, aby opłacić opłatę sieciową w wysokości %@. Obecny stan konta to %@"; +"swap.delivery.fee.error.message" = "Ze względu na ograniczenia międzyłańcuchowe powinieneś mieć co najmniej %@ po operacji"; \ No newline at end of file diff --git a/novawallet/pl.lproj/Localizable.stringsdict b/novawallet/pl.lproj/Localizable.stringsdict index 547d3f23ed..4acb41e715 100644 --- a/novawallet/pl.lproj/Localizable.stringsdict +++ b/novawallet/pl.lproj/Localizable.stringsdict @@ -242,5 +242,45 @@ Wybierz co najmniej %li portfeli + common.seconds.format + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + %li sekunda + few + %li sekunda + many + %li sekunda + other + %li sekund + + + common.operations + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + %li operacja + few + %li operacja + many + %li operacja + other + %li operacji + + \ No newline at end of file diff --git a/novawallet/pt-PT.lproj/Localizable.strings b/novawallet/pt-PT.lproj/Localizable.strings index fadb5a1770..3d24bc3e82 100644 --- a/novawallet/pt-PT.lproj/Localizable.strings +++ b/novawallet/pt-PT.lproj/Localizable.strings @@ -1396,7 +1396,6 @@ "swaps.setup.deposit.button.title" = "Obter %@"; "swaps.setup.network.fee.token.title" = "Token para pagar a taxa de rede"; "swaps.setup.network.fee.token.hint" = "A taxa de rede é adicionada em cima do valor inserido"; -"swaps.pay.asset.fee.ed.message" = "Para pagar a taxa de rede com %@, a Nova fará automaticamente a troca de %@ por %@ para manter o saldo mínimo de %@ na sua conta."; "swaps.setup.slippage.error.amount.bounds" = "Insira um valor entre %@ e %@"; "swaps.setup.slippage.warning.low.amount" = "A transação pode ser revertida por causa da baixa tolerância ao deslizamento."; "swaps.setup.slippage.warning.high.amount" = "A transação pode ser antecipada por causa do alto deslizamento."; @@ -1768,4 +1767,21 @@ "wallet.receive.details.format" = "Envie somente o token %@ e tokens na rede %@ para este endereço, ou você pode perder seus fundos"; "common.address.coppied" = "Endereço copiado"; "common.networks" = "Redes"; -"assets.search.token.hint" = "Pesquisar por token"; \ No newline at end of file +"assets.search.token.hint" = "Pesquisar por token"; +"sec.time.units" = "seg"; +"swaps.execution.dont.close.app" = "Não feche o aplicativo!"; +"swaps.details.exec.time" = "Tempo de execução"; +"swaps.details.total.fee" = "Taxa total"; +"swaps.label.transfer" = "Transferência"; +"swaps.label.swap" = "Troca"; +"swap.route.details.subtitle" = "O caminho que seu token percorrerá através de diferentes redes para obter o token desejado."; +"swaps.details.route" = "Rota"; +"swaps.execution.transfer.details" = "Transferindo %@ para %@"; +"swaps.execution.swap.details" = "Trocando %@ para %@ em %@"; +"common.of" = "%@ de %@"; +"swaps.execution.swap.failure" = "Falha na operação #%@ (%@)"; +"swaps.label.crosschain" = "Transferência entre cadeias"; +"swap.intermediate.too.low.amount.to.stay.abow.ed.message" = "Durante a execução da troca, o valor intermediário recebido é %@, o que é menor que o saldo mínimo de %@. Tente especificar um valor maior para a troca."; +"common.fee.amount.prefixed" = "Taxa: %@"; +"common.not.enough.to.pay.fee.message" = "Você não tem saldo suficiente para pagar a taxa de rede de %@. O saldo atual é %@"; +"swap.delivery.fee.error.message" = "Devido a restrições de crosschain, você deve ter pelo menos %@ após a operação"; \ No newline at end of file diff --git a/novawallet/pt-PT.lproj/Localizable.stringsdict b/novawallet/pt-PT.lproj/Localizable.stringsdict index 96f75641da..acaf06ca4b 100644 --- a/novawallet/pt-PT.lproj/Localizable.stringsdict +++ b/novawallet/pt-PT.lproj/Localizable.stringsdict @@ -194,5 +194,37 @@ Selecione pelo menos %li carteiras + common.seconds.format + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + %li segundo + other + %li segundos + + + common.operations + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + %li operação + other + %li operações + + \ No newline at end of file diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index c3c369318e..02bda13ec0 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1396,7 +1396,6 @@ "swaps.setup.deposit.button.title" = "Получить %@"; "swaps.setup.network.fee.token.title" = "Токен для оплаты комиссии сети"; "swaps.setup.network.fee.token.hint" = "Комиссия сети добавится к введенной сумме"; -"swaps.pay.asset.fee.ed.message" = "Чтобы оплатить комиссию сети с помощью %@, Nova автоматически обменяет %@ на %@, чтобы поддерживать минимальный %@ баланс вашей учетной записи."; "swaps.setup.slippage.error.amount.bounds" = "Введите значение между %@ и %@"; "swaps.setup.slippage.warning.low.amount" = "Транзакция может быть отменена из-за низкой устойчивости к проскальзыванию."; "swaps.setup.slippage.warning.high.amount" = "Транзакция может подвергнуться фронтрану из-за высокого проскальзывания"; @@ -1775,6 +1774,23 @@ "common.address.coppied" = "Адрес скопирован"; "common.networks" = "Сети"; "assets.search.token.hint" = "Поиск по токену"; +"sec.time.units" = "сек"; +"swaps.execution.dont.close.app" = "Не закрывайте приложение!"; +"swaps.details.exec.time" = "Время выполнения"; +"swaps.details.total.fee" = "Общая комиссия"; +"swaps.label.transfer" = "Перевод"; +"swaps.label.swap" = "Обмен"; +"swap.route.details.subtitle" = "Путь, по которому ваш токен пройдет через разные сети, чтобы получить желаемый токен."; +"swaps.details.route" = "Маршрут"; +"swaps.execution.transfer.details" = "Перевод %@ на %@"; +"swaps.execution.swap.details" = "Обмен %@ на %@ на %@"; +"common.of" = "%@ из %@"; +"swaps.execution.swap.failure" = "Ошибка в операции #%@ (%@)"; +"swaps.label.crosschain" = "Межсетевой перевод"; +"swap.intermediate.too.low.amount.to.stay.abow.ed.message" = "Во время выполнения обмена промежуточная полученная сумма составляет %@, что меньше минимального баланса в %@. Попробуйте указать большую сумму обмена."; +"common.fee.amount.prefixed" = "Комиссия: %@"; +"common.not.enough.to.pay.fee.message" = "У вас недостаточно средств для оплаты сетевой комиссии в размере %@. Текущий баланс: %@"; +"swap.delivery.fee.error.message" = "Из-за ограничений межсетевого взаимодействия у вас должно остаться как минимум %@ после операции"; "common.pay.anywhere" = "Платите везде"; "common.nova.card" = "Карта Nova"; "common.estimated.timer" = "Ожидаемое ~%@"; diff --git a/novawallet/ru.lproj/Localizable.stringsdict b/novawallet/ru.lproj/Localizable.stringsdict index ddb709a410..b141173f00 100644 --- a/novawallet/ru.lproj/Localizable.stringsdict +++ b/novawallet/ru.lproj/Localizable.stringsdict @@ -242,5 +242,45 @@ Выберите хотя бы %li кошельков + common.seconds.format + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + %li секунда + few + %li секунды + many + %li секунд + other + %li секунд + + + common.operations + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + %li операция + few + %li операция + many + %li операция + other + %li операций + + \ No newline at end of file diff --git a/novawallet/tr.lproj/Localizable.strings b/novawallet/tr.lproj/Localizable.strings index f26f2ece4d..ceff557839 100644 --- a/novawallet/tr.lproj/Localizable.strings +++ b/novawallet/tr.lproj/Localizable.strings @@ -1396,7 +1396,6 @@ "swaps.setup.deposit.button.title" = "%@ alın"; "swaps.setup.network.fee.token.title" = "Ağ ücreti ödemek için token"; "swaps.setup.network.fee.token.hint" = "Ağ ücreti girilen miktarın üstüne eklenir"; -"swaps.pay.asset.fee.ed.message" = "%@ ile ağ ücreti ödemek için, Nova otomatik olarak hesabınızın minimum %@ bakiyesini korumak için %@'i %@'ye dönüştürecektir."; "swaps.setup.slippage.error.amount.bounds" = "%@ ile %@ arasında bir değer girin"; "swaps.setup.slippage.warning.low.amount" = "Düşük kayma toleransı nedeniyle işlem geri alınabilir."; "swaps.setup.slippage.warning.high.amount" = "Yüksek kayma nedeniyle işlem ön alıma maruz kalabilir."; @@ -1768,4 +1767,21 @@ "wallet.receive.details.format" = "Bu adrese yalnızca %@ token ve %@ ağındaki tokenleri gönderin, aksi takdirde paranızı kaybedebilirsiniz"; "common.address.coppied" = "Adres kopyalandı"; "common.networks" = "Ağlar"; -"assets.search.token.hint" = "Tokene göre ara"; \ No newline at end of file +"assets.search.token.hint" = "Tokene göre ara"; +"sec.time.units" = "sn"; +"swaps.execution.dont.close.app" = "Uygulamayı kapatmayın!"; +"swaps.details.exec.time" = "Gerçekleşme süresi"; +"swaps.details.total.fee" = "Toplam Ücret"; +"swaps.label.transfer" = "Transfer"; +"swaps.label.swap" = "Takas"; +"swap.route.details.subtitle" = "Token'ınızın farklı ağlardan geçerek istenen token'a ulaşmak için alacağı yol."; +"swaps.details.route" = "Rota"; +"swaps.execution.transfer.details" = "%@'yı %@'ye aktarıyor"; +"swaps.execution.swap.details" = "%@'yı %@'ye %@ üzerinde takas ediyor"; +"common.of" = "%@ / %@"; +"swaps.execution.swap.failure" = "#%@ (%@) işleminde başarısız oldu"; +"swaps.label.crosschain" = "Çapraz zincir transferi"; +"swap.intermediate.too.low.amount.to.stay.abow.ed.message" = "Takas işlemi sırasında alınan ara miktar %@ olup, minimum bakiye olan %@'dan daha azdır. Daha büyük bir takas miktarı belirtmeyi deneyin."; +"common.fee.amount.prefixed" = "Ücret: %@"; +"common.not.enough.to.pay.fee.message" = "Ağ ücreti olan %@'yi ödemek için yeterli bakiyeniz yok. Mevcut bakiye %@"; +"swap.delivery.fee.error.message" = "Çapraz zincir kısıtlamaları nedeniyle işlemden sonra en az %@'a sahip olmalısınız"; \ No newline at end of file diff --git a/novawallet/tr.lproj/Localizable.stringsdict b/novawallet/tr.lproj/Localizable.stringsdict index d89dc28ee7..8896ddd169 100644 --- a/novawallet/tr.lproj/Localizable.stringsdict +++ b/novawallet/tr.lproj/Localizable.stringsdict @@ -194,5 +194,37 @@ En az %li cüzdan seçin + common.seconds.format + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + %li saniye + other + %li saniye + + + common.operations + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + one + %li işlem + other + %li işlem + + \ No newline at end of file diff --git a/novawallet/vi.lproj/Localizable.strings b/novawallet/vi.lproj/Localizable.strings index 23761475c0..14ccf0033a 100644 --- a/novawallet/vi.lproj/Localizable.strings +++ b/novawallet/vi.lproj/Localizable.strings @@ -1396,7 +1396,6 @@ "swaps.setup.deposit.button.title" = "Nhận %@"; "swaps.setup.network.fee.token.title" = "Token để trả phí mạng"; "swaps.setup.network.fee.token.hint" = "Phí mạng được thêm vào trên số tiền đã nhập"; -"swaps.pay.asset.fee.ed.message" = "Để thanh toán phí mạng bằng %@, Nova sẽ tự động hoán đổi %@ thành %@ để duy trì số dư tối thiểu %@ trong tài khoản của bạn."; "swaps.setup.slippage.error.amount.bounds" = "Nhập một giá trị giữa %@ và %@"; "swaps.setup.slippage.warning.low.amount" = "Giao dịch có thể bị hoàn lại do mức chấp nhận độ trượt giá thấp."; "swaps.setup.slippage.warning.high.amount" = "Giao dịch có thể bị frontrun do độ trượt giá cao."; @@ -1768,4 +1767,21 @@ "wallet.receive.details.format" = "Chỉ gửi token %@ và các token trong mạng %@ tới địa chỉ này, nếu không bạn có thể mất tiền"; "common.address.coppied" = "Địa chỉ đã được sao chép"; "common.networks" = "Mạng"; -"assets.search.token.hint" = "Tìm kiếm theo token"; \ No newline at end of file +"assets.search.token.hint" = "Tìm kiếm theo token"; +"sec.time.units" = "giây"; +"swaps.execution.dont.close.app" = "Không đóng ứng dụng!"; +"swaps.details.exec.time" = "Thời gian thực hiện"; +"swaps.details.total.fee" = "Tổng phí"; +"swaps.label.transfer" = "Chuyển"; +"swaps.label.swap" = "Hoán đổi"; +"swap.route.details.subtitle" = "Cách mà token của bạn sẽ đi qua các mạng lưới khác nhau để có được token mong muốn."; +"swaps.details.route" = "Lộ trình"; +"swaps.execution.transfer.details" = "Chuyển %@ tới %@"; +"swaps.execution.swap.details" = "Đang hoán đổi từ %@ sang %@ trên %@"; +"common.of" = "%@ của %@"; +"swaps.execution.swap.failure" = "Thất bại ở thao tác #%@ (%@)"; +"swaps.label.crosschain" = "Chuyển liên chuỗi"; +"swap.intermediate.too.low.amount.to.stay.abow.ed.message" = "Trong quá trình thực hiện giao dịch, số tiền nhận trung gian là %@, thấp hơn số dư tối thiểu là %@. Thử chỉ định số lượng swap lớn hơn."; +"common.fee.amount.prefixed" = "Phí: %@"; +"common.not.enough.to.pay.fee.message" = "Bạn không có đủ số dư để trả phí mạng %@. Số dư hiện tại là %@"; +"swap.delivery.fee.error.message" = "Do những hạn chế crosschain, bạn cần có ít nhất %@ sau khi thực hiện"; \ No newline at end of file diff --git a/novawallet/vi.lproj/Localizable.stringsdict b/novawallet/vi.lproj/Localizable.stringsdict index 91282b7738..967f2060b3 100644 --- a/novawallet/vi.lproj/Localizable.stringsdict +++ b/novawallet/vi.lproj/Localizable.stringsdict @@ -170,5 +170,33 @@ Chọn ít nhất %li ví + common.seconds.format + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + other + %li giây + + + common.operations + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + other + %li hoạt động + + \ No newline at end of file diff --git a/novawallet/zh-Hans.lproj/Localizable.strings b/novawallet/zh-Hans.lproj/Localizable.strings index e4d7f7a539..04ac6d3b58 100644 --- a/novawallet/zh-Hans.lproj/Localizable.strings +++ b/novawallet/zh-Hans.lproj/Localizable.strings @@ -1396,7 +1396,6 @@ "swaps.setup.deposit.button.title" = "获取%@"; "swaps.setup.network.fee.token.title" = "用于支付网络费用的代币"; "swaps.setup.network.fee.token.hint" = "网络费用将添加到输入的金额之上"; -"swaps.pay.asset.fee.ed.message" = "为了支付网络费用,Nova将自动将%@兑换为%@,以保持您账户的最低%@余额。"; "swaps.setup.slippage.error.amount.bounds" = "请输入介于%@和%@之间的值"; "swaps.setup.slippage.warning.low.amount" = "因低滑点容忍度可能会被回退。"; "swaps.setup.slippage.warning.high.amount" = "因高滑点而可能遭受前置交易。"; @@ -1768,4 +1767,21 @@ "wallet.receive.details.format" = "仅将%@代币和%@网络中的代币发送到此地址,否则您可能会失去资金"; "common.address.coppied" = "地址已复制"; "common.networks" = "网络"; -"assets.search.token.hint" = "按代币搜索"; \ No newline at end of file +"assets.search.token.hint" = "按代币搜索"; +"sec.time.units" = "秒"; +"swaps.execution.dont.close.app" = "不要关闭应用程序!"; +"swaps.details.exec.time" = "执行时间"; +"swaps.details.total.fee" = "总费用"; +"swaps.label.transfer" = "转移"; +"swaps.label.swap" = "交换"; +"swap.route.details.subtitle" = "你的代币通过不同网络到达所需代币的路径。"; +"swaps.details.route" = "路线"; +"swaps.execution.transfer.details" = "将 %@ 转移到 %@"; +"swaps.execution.swap.details" = "在 %@ 上将 %@ 兑换为 %@"; +"common.of" = "%@ 的 %@"; +"swaps.execution.swap.failure" = "在第 %@ 次操作(%@)中失败"; +"swaps.label.crosschain" = "跨链转账"; +"swap.intermediate.too.low.amount.to.stay.abow.ed.message" = "在交换执行过程中,中间接收金额为 %@,低于最小余额 %@。请尝试指定更大的交换金额。"; +"common.fee.amount.prefixed" = "费用:%@"; +"common.not.enough.to.pay.fee.message" = "您的余额不足以支付 %@ 的网络费用。当前余额为 %@"; +"swap.delivery.fee.error.message" = "由于跨链限制,操作后您至少应有 %@"; \ No newline at end of file diff --git a/novawallet/zh-Hans.lproj/Localizable.stringsdict b/novawallet/zh-Hans.lproj/Localizable.stringsdict index 08cc35435c..5b50f8e01a 100644 --- a/novawallet/zh-Hans.lproj/Localizable.stringsdict +++ b/novawallet/zh-Hans.lproj/Localizable.stringsdict @@ -170,5 +170,33 @@ 请至少选择%li个钱包 + common.seconds.format + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + other + %li 秒 + + + common.operations + + NSStringLocalizedFormatKey + %#@format@ + format + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + li + other + %li 操作 + + \ No newline at end of file diff --git a/novawalletIntegrationTests/AssetHubSwapTests.swift b/novawalletIntegrationTests/AssetHubSwapTests.swift index 668309224f..0053065ee2 100644 --- a/novawalletIntegrationTests/AssetHubSwapTests.swift +++ b/novawalletIntegrationTests/AssetHubSwapTests.swift @@ -95,13 +95,12 @@ final class AssetHubSwapTests: XCTestCase { amountOut: quote.amountOut, receiver: AccountId.zeroAccountId(of: 32), direction: .sell, - slippage: .percent(of: 1), - context: nil + slippage: .percent(of: 1) ) let fee = try fetchFee(for: callArgs, feeAssetId: .init(chainId: KnowChainId.westmint, assetId: 0)) - Logger.shared.info("Max fee: \(String(fee.totalFee.targetAmount))") + Logger.shared.info("Max fee: \(String(fee.amount))") } func testFeeForWestmintSiriSellInSiriToken() throws { @@ -122,13 +121,12 @@ final class AssetHubSwapTests: XCTestCase { amountOut: quote.amountOut, receiver: AccountId.zeroAccountId(of: 32), direction: .sell, - slippage: .percent(of: 1), - context: nil + slippage: .percent(of: 1) ) let fee = try fetchFee(for: callArgs, feeAssetId: .init(chainId: KnowChainId.westmint, assetId: 1)) - Logger.shared.info("Max fee: \(String(fee.totalFee.targetAmount))") + Logger.shared.info("Max fee: \(String(fee.amount))") } private func performAvailableDirectionsFetch( @@ -212,59 +210,55 @@ final class AssetHubSwapTests: XCTestCase { return try quoteWrapper.targetOperation.extractNoCancellableResultData() } - private func fetchFee(for args: AssetConversion.CallArgs, feeAssetId: ChainAssetId) throws -> AssetConversion.FeeModel { - let storageFacade = SubstrateStorageTestFacade() - let chainRegistry = ChainRegistryFacade.setupForIntegrationTest(with: storageFacade) + private func fetchFee(for args: AssetConversion.CallArgs, feeAssetId: ChainAssetId) throws -> ExtrinsicFeeProtocol { + let substrateStorageFacade = SubstrateStorageTestFacade() + let userStorageFacade = UserDataStorageTestFacade() + let chainRegistry = ChainRegistryFacade.setupForIntegrationTest(with: substrateStorageFacade) let chainId = args.assetIn.chainId + let wallet = AccountGenerator.generateMetaAccount(generatingChainAccounts: 1) + guard let chain = chainRegistry.getChain(for: chainId), - let asset = chain.asset(for: feeAssetId.assetId) else { + let asset = chain.asset(for: feeAssetId.assetId), + let chainAccount = wallet.fetch(for: chain.accountRequest()), + let connection = chainRegistry.getConnection(for: chainId), + let runtimeProvider = chainRegistry.getRuntimeProvider(for: chainId) else { throw CommonError.dataCorruption } let feeAsset = ChainAsset(chain: chain, asset: asset) - let wallet = AccountGenerator.generateMetaAccount(generatingChainAccounts: 1) - let operationQueue = OperationQueue() - let generalLocalSubscriptionFactory = GeneralStorageSubscriptionFactory( - chainRegistry: chainRegistry, - storageFacade: storageFacade, - operationManager: OperationManager(operationQueue: operationQueue), - logger: Logger.shared - ) - - let feeService = try AssetConversionFlowFacade( - wallet: wallet, - chainRegistry: chainRegistry, - userStorageFacade: UserDataStorageTestFacade(), - substrateStorageFacade: storageFacade, - generalSubscriptonFactory: generalLocalSubscriptionFactory, - operationQueue: operationQueue - ).createFeeService(for: chain) + let extrinsicOperationFactory = ExtrinsicServiceFactory( + runtimeRegistry: runtimeProvider, + engine: connection, + operationQueue: operationQueue, + userStorageFacade: userStorageFacade, + substrateStorageFacade: substrateStorageFacade + ).createOperationFactory(account: chainAccount, chain: chain) - var feeResult: AssetConversion.FeeResult? - - let expectation = XCTestExpectation() - - feeService.calculate(in: feeAsset, callArgs: args, runCompletionIn: .main) { result in - feeResult = result - - expectation.fulfill() - } + let codingFactoryOperation = runtimeProvider.fetchCoderFactoryOperation() + + let feeWrapper = extrinsicOperationFactory.estimateFeeOperation({ builder in + let codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + + return try AssetHubExtrinsicConverter.addingOperation( + to: builder, + chain: chain, + args: args, + codingFactory: codingFactory + ) + }, payingIn: feeAsset.chainAssetId) + + feeWrapper.addDependency(operations: [codingFactoryOperation]) + + let totalWrapper = feeWrapper.insertingHead(operations: [codingFactoryOperation]) - wait(for: [expectation], timeout: 600) + operationQueue.addOperations(totalWrapper.allOperations, waitUntilFinished: true) - switch feeResult { - case let .success(fee): - return fee - case let .failure(error): - throw error - case .none: - throw CommonError.undefined - } + return try feeWrapper.targetOperation.extractNoCancellableResultData() } } diff --git a/novawalletIntegrationTests/AssetsExchange/AssetsExchangeGraphDescription.swift b/novawalletIntegrationTests/AssetsExchange/AssetsExchangeGraphDescription.swift new file mode 100644 index 0000000000..2e48305aed --- /dev/null +++ b/novawalletIntegrationTests/AssetsExchange/AssetsExchangeGraphDescription.swift @@ -0,0 +1,30 @@ +import Foundation +@testable import novawallet + +enum AssetsExchangeGraphDescription { + static func getDescriptionForNode(_ node: ChainAssetId, chainRegistry: ChainRegistryProtocol) -> String { + guard + let chain = chainRegistry.getChain(for: node.chainId), + let asset = chain.assets.first(where: { $0.assetId == node.assetId }) + else { return "" } + + return "\(asset.symbol): \(chain.name)" + } + + static func getDescriptionForPath( + edges: [AnyAssetExchangeEdge], + chainRegistry: ChainRegistryProtocol + ) -> String { + guard let firstEdge = edges.first else { + return "" + } + + let otherNodes = edges.suffix(edges.count - 1).map { $0.destination } + + let pathNodes = [firstEdge.origin, firstEdge.destination] + otherNodes + + return pathNodes + .map { Self.getDescriptionForNode($0, chainRegistry: chainRegistry) } + .joined(separator: " -> ") + } +} diff --git a/novawalletIntegrationTests/AssetsExchange/AssetsExchangeTests.swift b/novawalletIntegrationTests/AssetsExchange/AssetsExchangeTests.swift new file mode 100644 index 0000000000..d213e4ff76 --- /dev/null +++ b/novawalletIntegrationTests/AssetsExchange/AssetsExchangeTests.swift @@ -0,0 +1,320 @@ +import XCTest +@testable import novawallet +import SoraKeystore + +final class AssetsExchangeTests: XCTestCase { + func testFindPath() { + let params = buildCommonParams() + + guard + let dotPolkadot = params.chainRegistry.getChain(for: KnowChainId.polkadot)?.utilityChainAsset(), + let usdtAssetHubId = params.chainRegistry.getChain(for: KnowChainId.statemint)?.chainAssetForSymbol("USDT")?.chainAssetId else { + XCTFail("No chain or asset") + return + } + + guard + let amountIn = Decimal(1000).toSubstrateAmount( + precision: dotPolkadot.assetDisplayInfo.assetPrecision + ) else { + XCTFail("Can't convert amount") + return + } + + do { + let quoteArgs = AssetConversion.QuoteArgs( + assetIn: dotPolkadot.chainAssetId, + assetOut: usdtAssetHubId, + amount: amountIn, + direction: .sell + ) + + guard let route = try findRoute(quoteArgs: quoteArgs, params: params) else { + XCTFail("Route not found") + return + } + + let routeDescription = AssetsExchangeGraphDescription.getDescriptionForPath( + edges: route.items.map({ $0.edge }), + chainRegistry: params.chainRegistry + ) + + params.logger.info("Route: \(routeDescription)") + params.logger.info("Quote: \(String(route.quote))") + } catch { + XCTFail("Quote error: \(error)") + } + } + + func testFindAvailablePairs() { + let params = buildCommonParams() + + guard + let polkadotUtilityAsset = params.chainRegistry.getChain(for: KnowChainId.polkadot)?.utilityChainAssetId(), + let hydraUtilityAsset = params.chainRegistry.getChain(for: KnowChainId.hydra)?.utilityChainAssetId(), + let assetHubUtilityAsset = params.chainRegistry.getChain(for: KnowChainId.statemint)?.utilityChainAssetId() else { + XCTFail("No chain or asset") + return + } + + let graph = createGraph(for: params) + + measure { + guard let reachability = graph?.fetchReachability() else { + XCTFail("No graph") + return + } + + let hasDirections = !reachability.getAllAssetIn().isEmpty && + !reachability.getAllAssetOut().isEmpty && + !reachability.getAssetsIn(for: assetHubUtilityAsset).isEmpty && + !reachability.getAssetsOut(for: polkadotUtilityAsset).isEmpty && + !reachability.getAssetsIn(for: hydraUtilityAsset).isEmpty + + XCTAssert(hasDirections, "Some directions were not found") + } + } + + func testNoRoute() { + let params = buildCommonParams() + + guard + let polkadotUtilityAsset = params.chainRegistry.getChain(for: KnowChainId.polkadot)?.utilityChainAssetId(), + let kusamaUtilityAsset = params.chainRegistry.getChain(for: KnowChainId.kusama)?.utilityChainAssetId() else { + XCTFail("No chain or asset") + return + } + + guard let graph = createGraph(for: params) else { + XCTFail("No graph") + return + } + + measure { + let route = graph.fetchPaths(from: kusamaUtilityAsset, to: polkadotUtilityAsset, maxTopPaths: 1) + + XCTAssert(route.isEmpty, "Unexpected route found") + } + } + + func testMeasureRouteSearch() { + let params = buildCommonParams() + + guard + let polkadotUtilityAsset = params.chainRegistry.getChain(for: KnowChainId.polkadot)?.utilityChainAssetId(), + let ibtcInterlayAsset = params.chainRegistry.getChain( + for: "bf88efe70e9e0e916416e8bed61f2b45717f517d7f3523e33c7b001e5ffcbc72" + )?.chainAssetForSymbol("iBTC")?.chainAssetId else { + XCTFail("No chain or asset") + return + } + + guard let graph = createGraph(for: params) else { + XCTFail("No graph") + return + } + + measure { + let route = graph.fetchPaths(from: polkadotUtilityAsset, to: ibtcInterlayAsset, maxTopPaths: 1) + XCTAssert(!route.isEmpty, "No routes founds") + } + } + + func testCalculateFee() { + let params = buildCommonParams() + + guard + let dotPolkadot = params.chainRegistry.getChain(for: KnowChainId.polkadot)?.utilityChainAsset(), + let usdtAssetHubId = params.chainRegistry.getChain(for: KnowChainId.statemint)?.chainAssetForSymbol("USDT")?.chainAssetId else { + XCTFail("No chain or asset") + return + } + + guard + let amountIn = Decimal(1000).toSubstrateAmount( + precision: dotPolkadot.assetDisplayInfo.assetPrecision + ) else { + XCTFail("Can't convert amount") + return + } + + do { + let quoteArgs = AssetConversion.QuoteArgs( + assetIn: dotPolkadot.chainAssetId, + assetOut: usdtAssetHubId, + amount: amountIn, + direction: .sell + ) + + guard let factory = createExchangeFactory(for: params) else { + XCTFail("Service not found") + return + } + + let routeWrapper = factory.createQuoteWrapper(args: quoteArgs) + + params.operationQueue.addOperations(routeWrapper.allOperations, waitUntilFinished: true) + + let quote = try routeWrapper.targetOperation.extractNoCancellableResultData() + + let feeWrapper = factory.createFeeWrapper( + for: .init( + route: quote.route, + slippage: BigRational.percent(of: 5), + feeAssetId: dotPolkadot.chainAssetId + ) + ) + + params.operationQueue.addOperations(feeWrapper.allOperations, waitUntilFinished: true) + + let feeResult = try feeWrapper.targetOperation.extractNoCancellableResultData() + + params.logger.info("Fees: \(feeResult.operationFees)") + + } catch { + XCTFail("Fee error: \(error)") + } + } + + private func findRoute( + quoteArgs: AssetConversion.QuoteArgs, + params: AssetExchangeGraphProvidingParams + ) throws -> AssetExchangeRoute? { + guard let factory = createExchangeFactory(for: params) else { + return nil + } + + let routeWrapper = factory.createQuoteWrapper(args: quoteArgs) + + params.operationQueue.addOperations(routeWrapper.allOperations, waitUntilFinished: true) + + return try routeWrapper.targetOperation.extractNoCancellableResultData().route + } + + private func createExchangeFactory(for params: AssetExchangeGraphProvidingParams) -> AssetsExchangeOperationFactoryProtocol? { + guard let graph = createGraph(for: params) else { + return nil + } + + return AssetsExchangeOperationFactory( + graph: graph, + pathCostEstimator: MockAssetsExchangePathCostEstimator(), + operationQueue: params.operationQueue, + logger: params.logger + ) + } + + private func createGraph( + for params: AssetExchangeGraphProvidingParams + ) -> AssetsExchangeGraphProtocol? { + let exchangeStateRegistrar = AssetsExchangeStateMediator() + + let feeSupportProvider = AssetsExchangeFeeSupportProvider( + feeSupportFetchersProvider: AssetExchangeFeeSupportFetchersProvider( + chainRegistry: params.chainRegistry, + operationQueue: params.operationQueue, + logger: params.logger + ), + operationQueue: params.operationQueue, + logger: params.logger + ) + + let pathCostEstimator = MockAssetsExchangePathCostEstimator() + + let graphProvider = AssetsExchangeGraphProvider( + selectedWallet: params.wallet, + chainRegistry: params.chainRegistry, + supportedExchangeProviders: [ + CrosschainAssetsExchangeProvider( + wallet: params.wallet, + syncService: XcmTransfersSyncService( + remoteUrl: ApplicationConfig.shared.xcmTransfersURL, + operationQueue: params.operationQueue + ), + chainRegistry: params.chainRegistry, + pathCostEstimator: pathCostEstimator, + signingWrapperFactory: SigningWrapperFactory(), + userStorageFacade: params.userDataStorageFacade, + substrateStorageFacade: params.substrateStorageFacade, + operationQueue: params.operationQueue, + logger: params.logger + ), + + AssetsHubExchangeProvider( + wallet: params.wallet, + chainRegistry: params.chainRegistry, + pathCostEstimator: pathCostEstimator, + signingWrapperFactory: SigningWrapperFactory(), + userStorageFacade: params.userDataStorageFacade, + substrateStorageFacade: params.substrateStorageFacade, + exchangeStateRegistrar: exchangeStateRegistrar, + operationQueue: params.operationQueue, + logger: params.logger + ), + + AssetsHydraExchangeProvider( + selectedWallet: params.wallet, + chainRegistry: params.chainRegistry, + pathCostEstimator: pathCostEstimator, + userStorageFacade: params.userDataStorageFacade, + substrateStorageFacade: params.substrateStorageFacade, + exchangeStateRegistrar: exchangeStateRegistrar, + operationQueue: params.operationQueue, + logger: params.logger + ) + ], + feeSupportProvider: feeSupportProvider, + suffiencyProvider: AssetExchangeSufficiencyProvider(), + operationQueue: params.operationQueue, + logger: params.logger + ) + + graphProvider.setup() + feeSupportProvider.setup() + + var actualGraph: AssetsExchangeGraphProtocol? + + graphProvider.subscribeGraph( + self, + notifyingIn: .global() + ) { graph in + actualGraph = graph + } + + let expectation = XCTestExpectation() + + DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(10)) { + expectation.fulfill() + } + + wait(for: [expectation], timeout: 60) + + graphProvider.throttle() + feeSupportProvider.throttle() + + return actualGraph + } + + private func buildCommonParams() -> AssetExchangeGraphProvidingParams { + let substrateStorageFacade = SubstrateStorageTestFacade() + let userDataStorageFacade = UserDataStorageTestFacade() + let chainRegistry = ChainRegistryFacade.setupForIntegrationTest(with: substrateStorageFacade) + + let logger = Logger.shared + let operationQueue = OperationQueue() + + let wallet = AccountGenerator.generateMetaAccount(type: .watchOnly) + + return .init( + wallet: wallet, + substrateStorageFacade: substrateStorageFacade, + userDataStorageFacade: userDataStorageFacade, + chainRegistry: chainRegistry, + config: ApplicationConfig.shared, + operationQueue: operationQueue, + keychain: InMemoryKeychain(), + settingsManager: InMemorySettingsManager(), + logger: logger + ) + } +} diff --git a/novawalletIntegrationTests/AssetsExchange/MockAssetExchangePathCostEstimator.swift b/novawalletIntegrationTests/AssetsExchange/MockAssetExchangePathCostEstimator.swift new file mode 100644 index 0000000000..adafa1850a --- /dev/null +++ b/novawalletIntegrationTests/AssetsExchange/MockAssetExchangePathCostEstimator.swift @@ -0,0 +1,13 @@ +import Foundation +@testable import novawallet +import Operation_iOS + +final class MockAssetsExchangePathCostEstimator {} + +extension MockAssetsExchangePathCostEstimator: AssetsExchangePathCostEstimating { + func costEstimationWrapper( + for path: AssetExchangeGraphPath + ) -> CompoundOperationWrapper { + CompoundOperationWrapper.createWithResult(.zero) + } +} diff --git a/novawalletIntegrationTests/AutocompounDelegateStakeTests.swift b/novawalletIntegrationTests/AutocompounDelegateStakeTests.swift index a4eeda9b15..0b57a124c8 100644 --- a/novawalletIntegrationTests/AutocompounDelegateStakeTests.swift +++ b/novawalletIntegrationTests/AutocompounDelegateStakeTests.swift @@ -194,17 +194,9 @@ class AutocompounDelegateStakeTests: XCTestCase { let signedExtensionFactory = ExtrinsicSignedExtensionFacade().createFactory(for: chainId) - let feeEstimatingWrapperFactory = ExtrinsicFeeEstimatingWrapperFactory( + let extrinsicFeeHost = ExtrinsicFeeEstimatorHost( account: account, chain: chain, - runtimeService: runtimeProvider, - connection: connection, - operationQueue: operationQueue - ) - - let feeEstimationRegistry = ExtrinsicFeeEstimationRegistry( - chain: chain, - estimatingWrapperFactory: feeEstimatingWrapperFactory, connection: connection, runtimeProvider: runtimeProvider, userStorageFacade: UserDataStorageTestFacade(), @@ -212,6 +204,17 @@ class AutocompounDelegateStakeTests: XCTestCase { operationQueue: operationQueue ) + let feeEstimationRegistry = ExtrinsicFeeEstimationRegistry( + chain: chain, + estimatingWrapperFactory: ExtrinsicFeeEstimatingWrapperFactory( + host: extrinsicFeeHost, + customFeeEstimatorFactory: AssetConversionFeeEstimatingFactory(host: extrinsicFeeHost) + ), + feeInstallingWrapperFactory: ExtrinsicFeeInstallingWrapperFactory( + customFeeInstallerFactory: AssetConversionFeeInstallingFactory(host: extrinsicFeeHost) + ) + ) + extrinsicService = ExtrinsicService( chain: chain, runtimeRegistry: runtimeProvider, @@ -292,17 +295,9 @@ class AutocompounDelegateStakeTests: XCTestCase { let operationQueue = OperationQueue() - let feeEstimatingWrapperFactory = ExtrinsicFeeEstimatingWrapperFactory( + let extrinsicFeeHost = ExtrinsicFeeEstimatorHost( account: account, chain: chain, - runtimeService: runtimeService, - connection: connection, - operationQueue: operationQueue - ) - - let feeEstimationRegistry = ExtrinsicFeeEstimationRegistry( - chain: chain, - estimatingWrapperFactory: feeEstimatingWrapperFactory, connection: connection, runtimeProvider: runtimeService, userStorageFacade: UserDataStorageTestFacade(), @@ -310,6 +305,17 @@ class AutocompounDelegateStakeTests: XCTestCase { operationQueue: operationQueue ) + let feeEstimationRegistry = ExtrinsicFeeEstimationRegistry( + chain: chain, + estimatingWrapperFactory: ExtrinsicFeeEstimatingWrapperFactory( + host: extrinsicFeeHost, + customFeeEstimatorFactory: AssetConversionFeeEstimatingFactory(host: extrinsicFeeHost) + ), + feeInstallingWrapperFactory: ExtrinsicFeeInstallingWrapperFactory( + customFeeInstallerFactory: AssetConversionFeeInstallingFactory(host: extrinsicFeeHost) + ) + ) + extrinsicService = ExtrinsicService( chain: chain, runtimeRegistry: runtimeService, diff --git a/novawalletIntegrationTests/ChainRegistryIntegrationTests.swift b/novawalletIntegrationTests/ChainRegistryIntegrationTests.swift index c8f8823319..d8becee9d2 100644 --- a/novawalletIntegrationTests/ChainRegistryIntegrationTests.swift +++ b/novawalletIntegrationTests/ChainRegistryIntegrationTests.swift @@ -77,7 +77,7 @@ class ChainRegistryIntegrationTests: XCTestCase { [accountId] }, factory: { try factoryOperation.extractNoCancellableResultData() - }, storagePath: .account + }, storagePath: SystemPallet.accountPath ) queryWrapper.addDependency(operations: [factoryOperation]) diff --git a/novawalletIntegrationTests/ExtrinsicServiceTests.swift b/novawalletIntegrationTests/ExtrinsicServiceTests.swift index 3d7f8679e5..148c725512 100644 --- a/novawalletIntegrationTests/ExtrinsicServiceTests.swift +++ b/novawalletIntegrationTests/ExtrinsicServiceTests.swift @@ -66,17 +66,9 @@ class ExtrinsicServiceTests: XCTestCase { operationQueue: operationQueue ) - let feeEstimatingWrapperFactory = ExtrinsicFeeEstimatingWrapperFactory( + let extrinsicFeeHost = ExtrinsicFeeEstimatorHost( account: account, chain: chain, - runtimeService: runtimeService, - connection: connection, - operationQueue: operationQueue - ) - - let feeEstimationRegistry = ExtrinsicFeeEstimationRegistry( - chain: chain, - estimatingWrapperFactory: feeEstimatingWrapperFactory, connection: connection, runtimeProvider: runtimeService, userStorageFacade: UserDataStorageTestFacade(), @@ -84,6 +76,17 @@ class ExtrinsicServiceTests: XCTestCase { operationQueue: operationQueue ) + let feeEstimationRegistry = ExtrinsicFeeEstimationRegistry( + chain: chain, + estimatingWrapperFactory: ExtrinsicFeeEstimatingWrapperFactory( + host: extrinsicFeeHost, + customFeeEstimatorFactory: AssetConversionFeeEstimatingFactory(host: extrinsicFeeHost) + ), + feeInstallingWrapperFactory: ExtrinsicFeeInstallingWrapperFactory( + customFeeInstallerFactory: AssetConversionFeeInstallingFactory(host: extrinsicFeeHost) + ) + ) + let extrinsicService = ExtrinsicService( chain: chain, runtimeRegistry: runtimeService, @@ -146,17 +149,9 @@ class ExtrinsicServiceTests: XCTestCase { operationQueue: operationQueue ) - let feeEstimatingWrapperFactory = ExtrinsicFeeEstimatingWrapperFactory( + let extrinsicFeeHost = ExtrinsicFeeEstimatorHost( account: account, chain: chain, - runtimeService: runtimeService, - connection: connection, - operationQueue: operationQueue - ) - - let feeEstimationRegistry = ExtrinsicFeeEstimationRegistry( - chain: chain, - estimatingWrapperFactory: feeEstimatingWrapperFactory, connection: connection, runtimeProvider: runtimeService, userStorageFacade: UserDataStorageTestFacade(), @@ -164,6 +159,17 @@ class ExtrinsicServiceTests: XCTestCase { operationQueue: operationQueue ) + let feeEstimationRegistry = ExtrinsicFeeEstimationRegistry( + chain: chain, + estimatingWrapperFactory: ExtrinsicFeeEstimatingWrapperFactory( + host: extrinsicFeeHost, + customFeeEstimatorFactory: AssetConversionFeeEstimatingFactory(host: extrinsicFeeHost) + ), + feeInstallingWrapperFactory: ExtrinsicFeeInstallingWrapperFactory( + customFeeInstallerFactory: AssetConversionFeeInstallingFactory(host: extrinsicFeeHost) + ) + ) + let extrinsicService = ExtrinsicService( chain: chain, runtimeRegistry: runtimeService, diff --git a/novawalletIntegrationTests/HydraDx/HydraOmnipoolSwapTests.swift b/novawalletIntegrationTests/HydraDx/HydraOmnipoolSwapTests.swift index 3c206b64e9..4ddf250c3c 100644 --- a/novawalletIntegrationTests/HydraDx/HydraOmnipoolSwapTests.swift +++ b/novawalletIntegrationTests/HydraDx/HydraOmnipoolSwapTests.swift @@ -141,6 +141,7 @@ final class HydraOmnipoolSwapTests: XCTestCase { chain: chain, connection: connection, runtimeProvider: runtimeService, + notificationsRegistrar: nil, operationQueue: operationQueue ) diff --git a/novawalletIntegrationTests/HydraDx/HydraStableswapTests.swift b/novawalletIntegrationTests/HydraDx/HydraStableswapTests.swift index 8fdde996c6..721d0c322d 100644 --- a/novawalletIntegrationTests/HydraDx/HydraStableswapTests.swift +++ b/novawalletIntegrationTests/HydraDx/HydraStableswapTests.swift @@ -155,6 +155,7 @@ final class HydraStableswapTests: XCTestCase { chain: chain, connection: connection, runtimeProvider: runtimeService, + notificationsRegistrar: nil, operationQueue: operationQueue ) diff --git a/novawalletIntegrationTests/HydraDx/HydraSwapsFeeTests.swift b/novawalletIntegrationTests/HydraDx/HydraSwapsFeeTests.swift deleted file mode 100644 index 60bd2f3f56..0000000000 --- a/novawalletIntegrationTests/HydraDx/HydraSwapsFeeTests.swift +++ /dev/null @@ -1,134 +0,0 @@ -import XCTest -@testable import novawallet -import Operation_iOS - -final class HydraSwapsFeeTests: XCTestCase { - - func testNativeFee() { - do { - let address = "7HoFY1kmdfge15uRWtU6T5XZKsbxd97E3Ek1fi2xHbyqT2JD" - - let accountId = try address.toAccountId() - - let fee = try fetchSwapFee( - for: address, - callArgs: .init( - assetIn: .init(chainId: KnowChainId.hydra, assetId: 1), - amountIn: 10_000_000_000, - assetOut: .init(chainId: KnowChainId.hydra, assetId: 0), - amountOut: 199_000_000_000_000, - receiver: accountId, - direction: .sell, - slippage: SlippageConfig.defaultConfig.defaultSlippage, - context: nil - ), - feeAssetId: .init(chainId: KnowChainId.hydra, assetId: 0) - ) - - Logger.shared.info("Fee: \(fee)") - } catch { - XCTFail("Unexpected error: \(error)") - } - } - - func testFeeInDot() { - do { - let address = "7HoFY1kmdfge15uRWtU6T5XZKsbxd97E3Ek1fi2xHbyqT2JD" - - let accountId = try address.toAccountId() - - let fee = try fetchSwapFee( - for: address, - callArgs: .init( - assetIn: .init(chainId: KnowChainId.hydra, assetId: 1), - amountIn: 10_000_000_000, - assetOut: .init(chainId: KnowChainId.hydra, assetId: 0), - amountOut: 199_000_000_000_000, - receiver: accountId, - direction: .sell, - slippage: SlippageConfig.defaultConfig.defaultSlippage, - context: nil - ), - feeAssetId: .init(chainId: KnowChainId.hydra, assetId: 1) - ) - - Logger.shared.info("Fee: \(fee)") - } catch { - XCTFail("Unexpected error: \(error)") - } - } - - func fetchSwapFee( - for address: AccountAddress, - callArgs: AssetConversion.CallArgs, - feeAssetId: ChainAssetId - ) throws -> AssetConversion.FeeModel { - let substrateStorageFacade = SubstrateStorageTestFacade() - let chainRegistry = ChainRegistryFacade.setupForIntegrationTest(with: substrateStorageFacade) - let chainId = feeAssetId.chainId - - let operationQueue = OperationQueue() - - let accountId = try address.toAccountId() - - let wallet = AccountGenerator.createWatchOnly(for: accountId) - - let userFacade = UserDataStorageTestFacade() - - let saveOperation = AccountRepositoryFactory(storageFacade: userFacade).createMetaAccountRepository( - for: nil, - sortDescriptors: [] - ).saveOperation({ - [wallet] - }, { [] }) - - operationQueue.addOperations([saveOperation], waitUntilFinished: true) - - guard - let chain = chainRegistry.getChain(for: chainId), - let feeAsset = chain.asset(for: feeAssetId.assetId) else { - throw ChainRegistryError.noChain(chainId) - } - - let generalSubscriptionFactory = GeneralStorageSubscriptionFactory( - chainRegistry: chainRegistry, - storageFacade: SubstrateStorageTestFacade(), - operationManager: OperationManager(operationQueue: operationQueue), - logger: Logger.shared - ) - - let feeService = try AssetConversionFlowFacade( - wallet: wallet, - chainRegistry: chainRegistry, - userStorageFacade: userFacade, - substrateStorageFacade: substrateStorageFacade, - generalSubscriptonFactory: generalSubscriptionFactory, - operationQueue: operationQueue - ).createFeeService(for: chain) - - var feeResult: AssetConversion.FeeResult? - - let expectation = XCTestExpectation() - - feeService.calculate( - in: ChainAsset(chain: chain, asset: feeAsset), - callArgs: callArgs, - runCompletionIn: .main - ) { result in - feeResult = result - - expectation.fulfill() - } - - wait(for: [expectation], timeout: 600) - - switch feeResult { - case let .success(fee): - return fee - case let .failure(error): - throw error - case .none: - throw CommonError.undefined - } - } -} diff --git a/novawalletIntegrationTests/WalletRemoteQueryFactoryTests.swift b/novawalletIntegrationTests/WalletRemoteQueryFactoryTests.swift index d7256a90c2..cdc6da549a 100644 --- a/novawalletIntegrationTests/WalletRemoteQueryFactoryTests.swift +++ b/novawalletIntegrationTests/WalletRemoteQueryFactoryTests.swift @@ -152,7 +152,6 @@ final class WalletRemoteQueryFactoryTests: XCTestCase { let operationFactory = WalletRemoteQueryWrapperFactory( requestFactory: requestFactory, - assetInfoOperationFactory: AssetStorageInfoOperationFactory(), runtimeProvider: runtimeProvider, connection: connection, operationQueue: operationQueue diff --git a/novawalletIntegrationTests/XcmTransfersFeeTests.swift b/novawalletIntegrationTests/XcmTransfersFeeTests.swift index ccc45ff957..9f816ebe62 100644 --- a/novawalletIntegrationTests/XcmTransfersFeeTests.swift +++ b/novawalletIntegrationTests/XcmTransfersFeeTests.swift @@ -336,8 +336,9 @@ class XcmTransfersFeeTests: XCTestCase { let service = XcmTransferService( wallet: wallet, chainRegistry: chainRegistry, - senderResolutionFacade: ExtrinsicSenderResolutionFacadeStub(), metadataHashOperationFactory: metadataHashFactory, + userStorageFacade: UserDataStorageTestFacade(), + substrateStorageFacade: substrateStorageFacade, operationQueue: operationQueue ) @@ -402,8 +403,9 @@ class XcmTransfersFeeTests: XCTestCase { let service = XcmTransferService( wallet: wallet, chainRegistry: chainRegistry, - senderResolutionFacade: ExtrinsicSenderResolutionFacadeStub(), metadataHashOperationFactory: metadataHashFactory, + userStorageFacade: UserDataStorageTestFacade(), + substrateStorageFacade: substrateStorageFacade, operationQueue: OperationQueue() ) diff --git a/novawalletTests/Common/Services/ExtrinsicProcessingTests.swift b/novawalletTests/Common/Services/ExtrinsicProcessingTests.swift index 808cbb523f..5f82c94ced 100644 --- a/novawalletTests/Common/Services/ExtrinsicProcessingTests.swift +++ b/novawalletTests/Common/Services/ExtrinsicProcessingTests.swift @@ -22,7 +22,7 @@ class ExtrinsicProcessingTests: XCTestCase { let processor = ExtrinsicProcessor(accountId: senderAccountId, chain: chain) let eventRecordsData = try Data(hexString: eventRecordsHex) - let typeName = coderFactory.metadata.getStorageMetadata(for: .events)!.type.typeName + let typeName = coderFactory.metadata.getStorageMetadata(for: SystemPallet.eventsPath)!.type.typeName let decoder = try coderFactory.createDecoder(from: eventRecordsData) let eventRecords: [EventRecord] = try decoder.read(of: typeName) diff --git a/novawalletTests/Mocks/DataProviders/CrowdloanLocalSubscriptionFactoryStub.swift b/novawalletTests/Mocks/DataProviders/CrowdloanLocalSubscriptionFactoryStub.swift index 3bb6a10a3a..73f4823695 100644 --- a/novawalletTests/Mocks/DataProviders/CrowdloanLocalSubscriptionFactoryStub.swift +++ b/novawalletTests/Mocks/DataProviders/CrowdloanLocalSubscriptionFactoryStub.swift @@ -18,7 +18,7 @@ final class CrowdloanLocalSubscriptionFactoryStub: CrowdloanLocalSubscriptionFac let localIdentifierFactory = LocalStorageKeyFactory() let blockNumberModel: DecodedBlockNumber = try { - let localKey = try localIdentifierFactory.createFromStoragePath(.blockNumber, chainId: chainId) + let localKey = try localIdentifierFactory.createFromStoragePath(SystemPallet.blockNumberPath, chainId: chainId) if let blockNumber = blockNumber { return DecodedBlockNumber(identifier: localKey, item: StringScaleMapper(value: blockNumber)) } else { diff --git a/novawalletTests/Mocks/DataProviders/ExtrinsicServiceFactoryStub.swift b/novawalletTests/Mocks/DataProviders/ExtrinsicServiceFactoryStub.swift index 561b1c86d3..80f0318f5b 100644 --- a/novawalletTests/Mocks/DataProviders/ExtrinsicServiceFactoryStub.swift +++ b/novawalletTests/Mocks/DataProviders/ExtrinsicServiceFactoryStub.swift @@ -21,6 +21,15 @@ final class ExtrinsicServiceFactoryStub: ExtrinsicServiceFactoryProtocol { ) -> ExtrinsicServiceProtocol { extrinsicService } + + func createService( + account: ChainAccountResponse, + chain: ChainModel, + extensions: [ExtrinsicSignedExtending], + customFeeEstimatingFactory: ExtrinsicCustomFeeEstimatingFactoryProtocol + ) -> ExtrinsicServiceProtocol { + extrinsicService + } func createOperationFactory( account: ChainAccountResponse, @@ -29,4 +38,13 @@ final class ExtrinsicServiceFactoryStub: ExtrinsicServiceFactoryProtocol { ) -> ExtrinsicOperationFactoryProtocol { extrinsicOperationFactory } + + func createOperationFactory( + account: ChainAccountResponse, + chain: ChainModel, + extensions: [ExtrinsicSignedExtending], + customFeeEstimatingFactory: ExtrinsicCustomFeeEstimatingFactoryProtocol + ) -> ExtrinsicOperationFactoryProtocol { + extrinsicOperationFactory + } } diff --git a/novawalletTests/Mocks/DataProviders/ExtrinsicServiceStub.swift b/novawalletTests/Mocks/DataProviders/ExtrinsicServiceStub.swift index 5a85392326..c3dc660de7 100644 --- a/novawalletTests/Mocks/DataProviders/ExtrinsicServiceStub.swift +++ b/novawalletTests/Mocks/DataProviders/ExtrinsicServiceStub.swift @@ -112,7 +112,7 @@ final class ExtrinsicServiceStub: ExtrinsicServiceProtocol { switch txHash { case let .success(value): - notificationClosure(.success(.inBlock(value))) + notificationClosure(.success(.init(extrinsicHash: value, extrinsicStatus: .inBlock(value)))) case let .failure(error): notificationClosure(.failure(error)) } diff --git a/novawalletTests/Mocks/ViewModel/StubBalanceViewModelFactory.swift b/novawalletTests/Mocks/ViewModel/StubBalanceViewModelFactory.swift index 6d08a915d4..e1b9f2294f 100644 --- a/novawalletTests/Mocks/ViewModel/StubBalanceViewModelFactory.swift +++ b/novawalletTests/Mocks/ViewModel/StubBalanceViewModelFactory.swift @@ -6,7 +6,7 @@ import SoraFoundation struct StubBalanceViewModelFactory: BalanceViewModelFactoryProtocol { func priceFromFiatAmount( _ decimalValue: Decimal, - priceData: PriceData + currencyId: Int? ) -> LocalizableResource { LocalizableResource { _ in "$100" diff --git a/novawalletTests/Modules/AssetsManage/AssetsManageTests.swift b/novawalletTests/Modules/AssetsManage/AssetsManageTests.swift index ac3ec6fdae..a23ca82ba0 100644 --- a/novawalletTests/Modules/AssetsManage/AssetsManageTests.swift +++ b/novawalletTests/Modules/AssetsManage/AssetsManageTests.swift @@ -21,7 +21,6 @@ class AssetsManageTests: XCTestCase { let repository: CoreDataRepository = storageFacade.createRepository( mapper: AnyCoreDataMapper(mapper) ) - let dataOperationFactory = MockDataOperationFactoryProtocol() let operationQueue = OperationQueue() let eventCenter = MockEventCenterProtocol() diff --git a/novawalletTests/Modules/DApps/DAppOperationConfirm/DAppOperationConfirmTests.swift b/novawalletTests/Modules/DApps/DAppOperationConfirm/DAppOperationConfirmTests.swift index 5fc4d79ba5..4cca1c9a2e 100644 --- a/novawalletTests/Modules/DApps/DAppOperationConfirm/DAppOperationConfirmTests.swift +++ b/novawalletTests/Modules/DApps/DAppOperationConfirm/DAppOperationConfirmTests.swift @@ -113,18 +113,11 @@ class DAppOperationConfirmTests: XCTestCase { let operationQueue = OperationQueue() - let feeEstimatingWrapperFactory = ExtrinsicFeeEstimatingWrapperFactory( - account: wallet.fetch(for: chain.accountRequest())!, - chain: chain, - runtimeService: runtimeProvider, - connection: connection, - operationQueue: operationQueue - ) - let storageFacade = SubstrateStorageTestFacade() - let feeEstimationRegistry = ExtrinsicFeeEstimationRegistry( + + let extrinsicFeeHost = ExtrinsicFeeEstimatorHost( + account: wallet.fetch(for: chain.accountRequest())!, chain: chain, - estimatingWrapperFactory: feeEstimatingWrapperFactory, connection: connection, runtimeProvider: runtimeProvider, userStorageFacade: UserDataStorageTestFacade(), @@ -132,6 +125,17 @@ class DAppOperationConfirmTests: XCTestCase { operationQueue: operationQueue ) + let feeEstimationRegistry = ExtrinsicFeeEstimationRegistry( + chain: chain, + estimatingWrapperFactory: ExtrinsicFeeEstimatingWrapperFactory( + host: extrinsicFeeHost, + customFeeEstimatorFactory: AssetConversionFeeEstimatingFactory(host: extrinsicFeeHost) + ), + feeInstallingWrapperFactory: ExtrinsicFeeInstallingWrapperFactory( + customFeeInstallerFactory: AssetConversionFeeInstallingFactory(host: extrinsicFeeHost) + ) + ) + let metadataHashFactory = MetadataHashOperationFactory( metadataRepositoryFactory: RuntimeMetadataRepositoryFactory( storageFacade: storageFacade diff --git a/novawalletTests/Modules/DApps/DAppTxDetails/DAppTxDetailsTests.swift b/novawalletTests/Modules/DApps/DAppTxDetails/DAppTxDetailsTests.swift deleted file mode 100644 index cde39842bd..0000000000 --- a/novawalletTests/Modules/DApps/DAppTxDetails/DAppTxDetailsTests.swift +++ /dev/null @@ -1,16 +0,0 @@ -import XCTest - -class DAppTxDetailsTests: 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() { - // TODO: Add tests - } -} diff --git a/novawalletTests/Modules/Governance/GovernanceUnlocksTestBuilding.swift b/novawalletTests/Modules/Governance/GovernanceUnlocksTestBuilding.swift index 2bdb20c4b5..8ed3b91aaf 100644 --- a/novawalletTests/Modules/Governance/GovernanceUnlocksTestBuilding.swift +++ b/novawalletTests/Modules/Governance/GovernanceUnlocksTestBuilding.swift @@ -75,7 +75,6 @@ enum GovernanceUnlocksTestBuilding { let initReferendums = [ReferendumIdLocal: GovUnlockReferendumProtocol]() let referendums = tracksVoting.votes.tracksByReferendums().keys.reduce(into: initReferendums) { (accum, referendumId) in - let deposit = Referenda.Deposit(who: AccountId.zeroAccountId(of: 32), amount: BigUInt(0)) let referendum = prepareReferendumInfo( for: referendumId, given: referendumsDef, diff --git a/novawalletTests/Modules/Governance/VotingCurveTests.swift b/novawalletTests/Modules/Governance/VotingCurveTests.swift index 1b95e03dc3..24139ae781 100644 --- a/novawalletTests/Modules/Governance/VotingCurveTests.swift +++ b/novawalletTests/Modules/Governance/VotingCurveTests.swift @@ -101,7 +101,7 @@ class VotingCurveTests: XCTestCase { stubs.forEach { expectedX, y in let resultX = function.delay(for: y) - XCTAssertTrue(resultX == expectedX, "Expected \(expectedX) for input \(y) but got: \(resultX)") + XCTAssertTrue(resultX == expectedX, "Expected \(expectedX) for input \(y) but got: \(String(describing: resultX))") } } @@ -117,7 +117,7 @@ class VotingCurveTests: XCTestCase { stubs.forEach { x, expectedY in let resultY = function.calculateThreshold(for: x) - XCTAssertTrue(resultY == expectedY, "Expected \(expectedY) for input \(x) but got: \(resultY)") + XCTAssertTrue(resultY == expectedY, "Expected \(expectedY) for input \(x) but got: \(String(describing: resultY))") } } } diff --git a/novawalletTests/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestinationSetupTests.swift b/novawalletTests/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestinationSetupTests.swift index 8ebaedb396..f26f3ed7f6 100644 --- a/novawalletTests/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestinationSetupTests.swift +++ b/novawalletTests/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestinationSetupTests.swift @@ -8,7 +8,11 @@ import SoraFoundation import BigInt class StakingRewardDestinationSetupTests: XCTestCase { - + struct PresenterSetupResult { + let presenter: StakingRewardDestSetupPresenter + let outputProxy: StakingRewardDestSetupInteractorOutputProtocol + } + func testRewardDestinationSetupSuccess() throws { // given @@ -19,7 +23,7 @@ class StakingRewardDestinationSetupTests: XCTestCase { // when - let presenter = try setupPresenter(for: view, wireframe: wireframe, newPayout: newPayoutAccount) + let presenterResult = try setupPresenter(for: view, wireframe: wireframe, newPayout: newPayoutAccount) let changesApplied = XCTestExpectation() @@ -66,12 +70,12 @@ class StakingRewardDestinationSetupTests: XCTestCase { } } - presenter.selectPayoutDestination() - presenter.selectPayoutAccount() + presenterResult.presenter.selectPayoutDestination() + presenterResult.presenter.selectPayoutAccount() wait(for: [changesApplied, payoutSelectionsExpectation], timeout: 10.0) - presenter.proceed() + presenterResult.presenter.proceed() // then @@ -82,7 +86,7 @@ class StakingRewardDestinationSetupTests: XCTestCase { for view: MockStakingRewardDestSetupViewProtocol, wireframe: MockStakingRewardDestSetupWireframeProtocol, newPayout: MetaAccountModel? - ) throws -> StakingRewardDestSetupPresenter { + ) throws -> PresenterSetupResult { // given let chain = ChainModelGenerator.generateChain( @@ -201,14 +205,17 @@ class StakingRewardDestinationSetupTests: XCTestCase { assetInfo: assetInfo ) + let mockedPresenter = MockStakingRewardDestSetupInteractorOutputProtocol() + presenter.view = view - interactor.presenter = presenter + interactor.presenter = mockedPresenter validationFactory.view = view // when let feeExpectation = XCTestExpectation() let rewardDestinationExpectation = XCTestExpectation() + let balanceExpectation = XCTestExpectation() stub(view) { stub in when(stub).didReceiveFee(viewModel: any()).then { feeViewModel in @@ -223,14 +230,68 @@ class StakingRewardDestinationSetupTests: XCTestCase { } } } + + stub(mockedPresenter) { stub in + when(stub).didReceiveFee(result: any()).then { fee in + presenter.didReceiveFee(result: fee) + } + + when(stub).didReceivePriceData(result: any()).then { result in + presenter.didReceivePriceData(result: result) + } + + when(stub).didReceiveStashItem(result: any()).then { result in + presenter.didReceiveStashItem(result: result) + } + + when(stub).didReceiveStakingLedger(result: any()).then { result in + presenter.didReceiveStakingLedger(result: result) + } + + when(stub).didReceiveController(result: any()).then { result in + presenter.didReceiveController(result: result) + } + + when(stub).didReceiveStash(result: any()).then { result in + presenter.didReceiveStash(result: result) + } + + when(stub).didReceiveRewardDestinationAccount(result: any()).then { result in + presenter.didReceiveRewardDestinationAccount(result: result) + } + + when(stub).didReceiveRewardDestinationAddress(result: any()).then { result in + presenter.didReceiveRewardDestinationAddress(result: result) + } + + when(stub).didReceiveCalculator(result: any()).then { result in + presenter.didReceiveCalculator(result: result) + } + + when(stub).didReceiveAccounts(result: any()).then { result in + presenter.didReceiveAccounts(result: result) + } + + when(stub).didReceiveNomination(result: any()).then { result in + presenter.didReceiveNomination(result: result) + } + + when(stub).didReceiveAccountBalance(result: any()).then { result in + presenter.didReceiveAccountBalance(result: result) + + if case .success = result { + balanceExpectation.fulfill() + } + } + } presenter.setup() // then - wait(for: [feeExpectation, rewardDestinationExpectation], timeout: 10) + wait(for: [feeExpectation, rewardDestinationExpectation, balanceExpectation], timeout: 20) - return presenter + return PresenterSetupResult(presenter: presenter, outputProxy: mockedPresenter) } } diff --git a/novawalletTests/Modules/Swaps/SwapsValidationTests.swift b/novawalletTests/Modules/Swaps/SwapsValidationTests.swift deleted file mode 100644 index f530e42c93..0000000000 --- a/novawalletTests/Modules/Swaps/SwapsValidationTests.swift +++ /dev/null @@ -1,105 +0,0 @@ -import XCTest -@testable import novawallet -import SoraKeystore -import Cuckoo -import BigInt - -final class SwapsValidationTests: XCTestCase { - private func amountInPlank(_ amount: Decimal, _ chainAsset: ChainAsset) -> BigUInt { - amount.toSubstrateAmount(precision: chainAsset.assetDisplayInfo.assetPrecision) ?? 0 - } - - func testCalculatedFeeWithoutED() throws { - let chain = ChainModelGenerator.generateChain(generatingAssets: 3, addressPrefix: 42) - let utilityChainAsset = ChainAsset(chain: chain, asset: chain.assets.first(where: { $0.assetId == 0 })!) - let payChainAsset = ChainAsset(chain: chain, asset: chain.assets.first(where: { $0.assetId == 2 })!) - let feeChainAsset = payChainAsset - let accountId = try WestendStub.address.toAccountId() - - let freeBalance = amountInPlank(50, payChainAsset) - let payAssetBalance = AssetBalance( - chainAssetId: payChainAsset.chainAssetId, - accountId: accountId, - freeInPlank: freeBalance, - reservedInPlank: 0, - frozenInPlank: 0, - edCountMode: .basedOnFree, - transferrableMode: .fungibleTrait, - blocked: false - ) - - let existentialDeposit = amountInPlank(1, utilityChainAsset) - let fee = amountInPlank(0.1, payChainAsset) - let existentialDepositInFeeToken = amountInPlank(0.01, payChainAsset) - - let swapMax = SwapMaxModel( - payChainAsset: payChainAsset, - feeChainAsset: feeChainAsset, - balance: payAssetBalance, - feeModel: .init( - totalFee: .init( - targetAmount: fee + existentialDepositInFeeToken, - nativeAmount: (fee + existentialDeposit) / 100 - ), - networkFee: .init( - targetAmount: fee, - nativeAmount: fee / 100 - ), - networkFeePayer: nil - ), - payAssetExistense: nil, - receiveAssetExistense: nil, - accountInfo: nil - ) - - let result = swapMax.calculate() - - XCTAssertEqual(result, 49.89) - - } - - func testCalculatedFeeWithED() throws { - let chain = ChainModelGenerator.generateChain(generatingAssets: 3, addressPrefix: 42) - let utilityChainAsset = ChainAsset(chain: chain, asset: chain.assets.first(where: { $0.assetId == 0 })!) - let payChainAsset = ChainAsset(chain: chain, asset: chain.assets.first(where: { $0.assetId == 2 })!) - let feeChainAsset = utilityChainAsset - let accountId = try WestendStub.address.toAccountId() - - let freeBalance = amountInPlank(50, payChainAsset) - let payAssetBalance = AssetBalance(chainAssetId: payChainAsset.chainAssetId, - accountId: accountId, - freeInPlank: freeBalance, - reservedInPlank: 0, - frozenInPlank: 0, - edCountMode: .basedOnFree, - transferrableMode: .fungibleTrait, - blocked: false) - - let fee = amountInPlank(0.1, payChainAsset) - - let params = SwapMaxModel( - payChainAsset: payChainAsset, - feeChainAsset: feeChainAsset, - balance: payAssetBalance, - feeModel: .init( - totalFee: .init( - targetAmount: fee, - nativeAmount: fee - ), - networkFee: .init( - targetAmount: fee, - nativeAmount: fee - ), - networkFeePayer: nil - ), - payAssetExistense: nil, - receiveAssetExistense: nil, - accountInfo: nil - ) - - let result = params.calculate() - - XCTAssertEqual(result, 50) - - } -} diff --git a/novawalletTests/Modules/WalletList/WalletListTests.swift b/novawalletTests/Modules/WalletList/WalletListTests.swift deleted file mode 100644 index a4f0a25c34..0000000000 --- a/novawalletTests/Modules/WalletList/WalletListTests.swift +++ /dev/null @@ -1,16 +0,0 @@ -import XCTest - -class WalletListTests: 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() { - // TODO: Add tests - } -}