diff --git a/.github/workflows/deploy_appstore.yml b/.github/workflows/deploy_appstore.yml index f493f5864e..73e68eacfc 100644 --- a/.github/workflows/deploy_appstore.yml +++ b/.github/workflows/deploy_appstore.yml @@ -8,21 +8,22 @@ on: jobs: build_and_deploy: name: Build and Deploy to TestFlight - runs-on: macos-13 + runs-on: macos-14 steps: - - name: Setup Xcode to 15.0 + - name: Setup Xcode to 15.3 uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: '15.0' + xcode-version: '15.3' - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup Ruby v3 uses: ruby/setup-ruby@v1 with: ruby-version: 3.0.2 + bundler-cache: true - name: Setup Rust and Cargo uses: actions-rs/toolchain@v1 @@ -36,10 +37,11 @@ jobs: run: rustup target add aarch64-apple-ios x86_64-apple-ios - name: Run Fastlane (build, upload to TestFlight) - uses: maierj/fastlane-action@v2.0.1 + uses: maierj/fastlane-action@v3.1.0 with: lane: release env: + FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 120 BUILD_NUMBER: ${{ github.run_number }} APP_ID: ${{ secrets.APP_ID }} GIT_AUTHORIZATION: ${{ secrets.GIT_AUTHORIZATION }} @@ -66,3 +68,5 @@ jobs: XCCONFIG_PROD_TRONGRID_API_KEY: ${{ secrets.XCCONFIG_PROD_TRONGRID_API_KEY }} XCCONFIG_PROD_UNSTOPPABLE_DOMAINS_API_KEY: ${{ secrets.XCCONFIG_PROD_UNSTOPPABLE_DOMAINS_API_KEY }} XCCONFIG_PROD_ONE_INCH_API_KEY: ${{ secrets.XCCONFIG_PROD_ONE_INCH_API_KEY }} + XCCONFIG_PROD_ONE_INCH_COMMISSION: ${{ secrets.XCCONFIG_PROD_ONE_INCH_COMMISSION }} + XCCONFIG_PROD_ONE_INCH_COMMISSION_ADDRESS: ${{ secrets.XCCONFIG_PROD_ONE_INCH_COMMISSION_ADDRESS }} diff --git a/.github/workflows/deploy_dev.yml b/.github/workflows/deploy_dev.yml index bf3bc278c3..9ceb6d75d1 100644 --- a/.github/workflows/deploy_dev.yml +++ b/.github/workflows/deploy_dev.yml @@ -8,21 +8,22 @@ on: jobs: build_and_deploy: name: Build and Deploy to AppCenter - runs-on: macos-13 + runs-on: macos-14 steps: - - name: Setup Xcode to 15.0 + - name: Setup Xcode to 15.3 uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: '15.0' + xcode-version: '15.3' - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup Ruby v3 uses: ruby/setup-ruby@v1 with: ruby-version: 3.0.2 + bundler-cache: true - name: Setup Rust and Cargo uses: actions-rs/toolchain@v1 @@ -36,10 +37,11 @@ jobs: run: rustup target add aarch64-apple-ios x86_64-apple-ios - name: Run Fastlane (build, upload to AppCenter) - uses: maierj/fastlane-action@v2.0.1 + uses: maierj/fastlane-action@v3.1.0 with: lane: dev env: + FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 120 BUILD_NUMBER: ${{ github.run_number }} APP_ID: ${{ secrets.APP_ID }} GIT_AUTHORIZATION: ${{ secrets.GIT_AUTHORIZATION }} @@ -67,3 +69,5 @@ jobs: XCCONFIG_DEV_TRONGRID_API_KEY: ${{ secrets.XCCONFIG_DEV_TRONGRID_API_KEY }} XCCONFIG_DEV_UNSTOPPABLE_DOMAINS_API_KEY: ${{ secrets.XCCONFIG_DEV_UNSTOPPABLE_DOMAINS_API_KEY }} XCCONFIG_DEV_ONE_INCH_API_KEY: ${{ secrets.XCCONFIG_DEV_ONE_INCH_API_KEY }} + XCCONFIG_DEV_ONE_INCH_COMMISSION: ${{ secrets.XCCONFIG_DEV_ONE_INCH_COMMISSION }} + XCCONFIG_DEV_ONE_INCH_COMMISSION_ADDRESS: ${{ secrets.XCCONFIG_DEV_ONE_INCH_COMMISSION_ADDRESS }} diff --git a/Gemfile.lock b/Gemfile.lock index 430cf25f48..5e904ac35b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,29 +1,32 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.6) + CFPropertyList (3.0.7) + base64 + nkf rexml - addressable (2.8.5) + addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) - artifactory (3.0.15) + artifactory (3.0.17) atomos (0.1.3) - aws-eventstream (1.2.0) - aws-partitions (1.854.0) - aws-sdk-core (3.187.1) - aws-eventstream (~> 1, >= 1.0.2) + aws-eventstream (1.3.0) + aws-partitions (1.929.0) + aws-sdk-core (3.196.1) + aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.5) + aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.72.0) - aws-sdk-core (~> 3, >= 3.184.0) + aws-sdk-kms (1.81.0) + aws-sdk-core (~> 3, >= 3.193.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.137.0) - aws-sdk-core (~> 3, >= 3.181.0) + aws-sdk-s3 (1.151.0) + aws-sdk-core (~> 3, >= 3.194.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.6) - aws-sigv4 (1.6.1) + aws-sigv4 (~> 1.8) + aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) + base64 (0.2.0) claide (1.1.0) colored (1.2) colored2 (3.1.2) @@ -32,10 +35,10 @@ GEM declarative (0.0.20) digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) - domain_name (0.6.20231109) + domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.104.0) + excon (0.110.0) faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -64,15 +67,15 @@ GEM faraday-retry (1.0.3) faraday_middleware (1.2.0) faraday (~> 1.0) - fastimage (2.2.7) - fastlane (2.217.0) + fastimage (2.3.1) + fastlane (2.220.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) aws-sdk-s3 (~> 1.0) babosa (>= 1.0.3, < 2.0.0) bundler (>= 1.12.0, < 3.0.0) - colored + colored (~> 1.2) commander (~> 4.6) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) @@ -84,6 +87,7 @@ GEM gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) google-cloud-storage (~> 1.31) highline (~> 2.0) http-cookie (~> 1.0.5) @@ -92,10 +96,10 @@ GEM mini_magick (>= 4.9.4, < 5.0.0) multipart-post (>= 2.0.0, < 3.0.0) naturally (~> 2.2) - optparse (~> 0.1.1) + optparse (>= 0.1.1, < 1.0.0) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.3) + security (= 0.1.5) simctl (~> 1.6.3) terminal-notifier (>= 2.0.0, < 3.0.0) terminal-table (~> 3) @@ -104,13 +108,13 @@ GEM word_wrap (~> 1.0.0) xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) - xcpretty-travis-formatter (>= 0.0.3) - fastlane-plugin-appcenter (2.1.1) - fastlane-plugin-xcconfig (2.0.0) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-plugin-appcenter (2.1.2) + fastlane-plugin-xcconfig (2.1.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.53.0) + google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.2) + google-apis-core (0.11.3) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -118,24 +122,23 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.a) rexml - webrick google-apis-iamcredentials_v1 (0.17.0) google-apis-core (>= 0.11.0, < 2.a) google-apis-playcustomapp_v1 (0.13.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.29.0) + google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.6.0) - google-cloud-env (~> 1.0) + google-cloud-core (1.7.0) + google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.3.1) - google-cloud-storage (1.45.0) + google-cloud-errors (1.4.0) + google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.29.0) + google-apis-storage_v1 (~> 0.31.0) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) @@ -150,19 +153,21 @@ GEM domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.6.2) - json (2.6.3) - jwt (2.7.1) + json (2.7.2) + jwt (2.8.1) + base64 mini_magick (4.12.0) mini_mime (1.1.5) multi_json (1.15.0) - multipart-post (2.3.0) + multipart-post (2.4.1) nanaimo (0.3.0) naturally (2.2.1) - optparse (0.1.1) + nkf (0.2.0) + optparse (0.5.0) os (1.1.4) - plist (3.7.0) - public_suffix (5.0.4) - rake (13.1.0) + plist (3.7.1) + public_suffix (5.0.5) + rake (13.2.1) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) @@ -172,8 +177,8 @@ GEM rouge (2.0.7) ruby2_keywords (0.0.5) rubyzip (2.3.2) - security (0.1.3) - signet (0.18.0) + security (0.1.5) + signet (0.19.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) @@ -186,17 +191,16 @@ GEM unicode-display_width (>= 1.1.1, < 3) trailblazer-option (0.1.2) tty-cursor (0.7.1) - tty-screen (0.8.1) + tty-screen (0.8.2) tty-spinner (0.9.3) tty-cursor (~> 0.7) uber (0.1.0) unicode-display_width (2.5.0) - webrick (1.8.1) word_wrap (1.0.0) xcode-install (2.8.1) claide (>= 0.9.1) fastlane (>= 2.1.0, < 3.0.0) - xcodeproj (1.23.0) + xcodeproj (1.24.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index 10e803b942..aa791e9cba 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -19,7 +19,6 @@ 11B3501D2E8C6FB3B3B767FC /* StatStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D6FC62F4797DEE1C419 /* StatStorage.swift */; }; 11B3501EA3658BFDC67DEB1F /* ThemeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E7E7A5DBB09A2A5197D /* ThemeView.swift */; }; 11B3502C799E4ED762F95252 /* AddEvmSyncSourceModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35504934CE3C31D523F82 /* AddEvmSyncSourceModule.swift */; }; - 11B3502DA4A2B869638AF4D8 /* MarketListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355BEB95969D89B3F8876 /* MarketListViewModel.swift */; }; 11B3502E567C29F297090D9B /* SyncErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3516207E568E7D54428CA /* SyncErrorView.swift */; }; 11B3502E7AA00ACFE8EE8CD9 /* FormTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35392439DBC8C06DCCB60 /* FormTextView.swift */; }; 11B35030E4C9A4157CE5FF43 /* WidgetProd.entitlements in Resources */ = {isa = PBXBuildFile; fileRef = 11B35DDE879F1628BB2CE523 /* WidgetProd.entitlements */; }; @@ -29,9 +28,7 @@ 11B350354CCA9BDDC05A9CBA /* WalletConnectSessionStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355C1E3C922BAE804AAF9 /* WalletConnectSessionStorage.swift */; }; 11B350388CD7F33B10BD3F4B /* AdapterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3583932F270503C1DF3F0 /* AdapterFactory.swift */; }; 11B3503BF015EA47E1061122 /* AccountRecordStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358556C8FC5368E14D81E /* AccountRecordStorage.swift */; }; - 11B3503C1335F0860B6DF7B8 /* CoinTreasuriesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3580ECB328146E94D4359 /* CoinTreasuriesService.swift */; }; 11B3503F5875A29E6949B13C /* EvmPrivateKeyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D67F24A3128864A8700 /* EvmPrivateKeyService.swift */; }; - 11B3504029EB87A32DB63666 /* MarketTopService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35770F0C72E1CD3F99985 /* MarketTopService.swift */; }; 11B35040917F10257C949596 /* NftCollectionRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350E6966203DF2855369A /* NftCollectionRecord.swift */; }; 11B35041FF23044B3CCDDA55 /* CoinInvestment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35264F7CE80AD5D9A540A /* CoinInvestment.swift */; }; 11B3504619330D3DB0E0ECD1 /* PublicKeysModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35111F25CE7D0C8E0B29B /* PublicKeysModule.swift */; }; @@ -42,7 +39,6 @@ 11B35051B85C51018A1C7A3A /* KeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BFA410F267D3A9B8E43 /* KeychainStorage.swift */; }; 11B35052C5059CDD8E4BA940 /* BackupMnemonicWordCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35736BA15E54066036D54 /* BackupMnemonicWordCell.swift */; }; 11B35056B69A06C8CFF3CBB6 /* BackupModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358967A086CFE9DBB152B /* BackupModule.swift */; }; - 11B3505A12674A5FF36D3CC8 /* MarketAdvancedSearchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C19608F6A314CF1F0C5 /* MarketAdvancedSearchService.swift */; }; 11B3505A7E4B89E9A23C19F5 /* NftMetadataSyncRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B58E21336A3DF5A9B45 /* NftMetadataSyncRecord.swift */; }; 11B3505B911DD28AC464A694 /* BackupManagerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A999428368A8FA264CA4 /* BackupManagerViewModel.swift */; }; 11B35063C586D8E1365E9DFF /* SendEvmConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B354950B1534AD045FDA3A /* SendEvmConfirmationViewController.swift */; }; @@ -51,7 +47,6 @@ 11B350667CC409D86A3C4C6A /* CexAmountInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35071F0BD63CCE6417ADC /* CexAmountInputViewModel.swift */; }; 11B35068706B5C13888F7E22 /* EvmSyncSourceRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B56F5C8138085588EE5 /* EvmSyncSourceRecord.swift */; }; 11B35068E05BC58C6C9A93D7 /* AccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35872950C107E4810AB6B /* AccountManager.swift */; }; - 11B3506ECD6D4D8D0C7717B6 /* MarketAdvancedSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356BEB2B4DFC3E9C950C5 /* MarketAdvancedSearchViewModel.swift */; }; 11B35071705455BC25C214D2 /* TonAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F9B75F6663FAFCA3177 /* TonAdapter.swift */; }; 11B3507578AF3163AAC8C494 /* EditPasscodeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3529CF33E51DA1C872106 /* EditPasscodeModule.swift */; }; 11B35076A96AD17809CE1F62 /* ClickableRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D179817528224E926D1 /* ClickableRow.swift */; }; @@ -87,13 +82,12 @@ 11B350B3D287EE732007892B /* WalletCoinPriceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3508A42F804E2010C7BB3 /* WalletCoinPriceService.swift */; }; 11B350B7D3DDBEE4F5B6E914 /* SendEvmTransactionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35785C46145F04D21302C /* SendEvmTransactionViewModel.swift */; }; 11B350BFC559991F9BA7A63F /* CexAssetRecordStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35AD211091A7C8619CEA2 /* CexAssetRecordStorage.swift */; }; - 11B350C02EDA928A80F3AD83 /* MultiSwapConfirmationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3573427BEEC59D4B978FA /* MultiSwapConfirmationViewModel.swift */; }; 11B350C1B04946C9AA8B3430 /* ListSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35AA43C4832521D428799 /* ListSection.swift */; }; 11B350C214D423CE2DCD6853 /* CexAssetRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35195509787CD52A6873A /* CexAssetRecord.swift */; }; 11B350C265D84964DCB0B317 /* CoinAuditsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35792F63B15682C00A3D9 /* CoinAuditsModule.swift */; }; 11B350CA618DD7BBA452FC33 /* AppearanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353955E2C534319A5EBE9 /* AppearanceViewModel.swift */; }; 11B350CB4E7C006C26AE5FB3 /* EnabledWalletStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35763ED14419B9EE4C6F9 /* EnabledWalletStorage.swift */; }; - 11B350CE805774C98948353C /* SendConfirmField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359C9E2EFD391FB848618 /* SendConfirmField.swift */; }; + 11B350CE805774C98948353C /* SendField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359C9E2EFD391FB848618 /* SendField.swift */; }; 11B350D00FA0A18EF540C945 /* BottomSingleSelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EF3688D60C8E6823267 /* BottomSingleSelectorViewController.swift */; }; 11B350D6CBB602F510882F1E /* WalletConnectRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CD5EBBB403D46BDEF0B /* WalletConnectRequest.swift */; }; 11B350D931616C0C296B6082 /* DuressModeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F5B696CF0677865FA2C /* DuressModeViewModel.swift */; }; @@ -108,13 +102,11 @@ 11B350ECEE8748562D27249F /* CexWithdrawNetworkSelectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351C522855F29C6B038D3 /* CexWithdrawNetworkSelectViewController.swift */; }; 11B350F12C3CA54080C16031 /* ManageWalletsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C4D645B4468F84EADB7 /* ManageWalletsViewController.swift */; }; 11B350F24818BABB6DB6512A /* SingleCoinPriceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CFAA2B156E439EB18B3 /* SingleCoinPriceView.swift */; }; - 11B350F36947CF278CDB436B /* MarketListMarketFieldDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353B060BDF272932D3522 /* MarketListMarketFieldDecorator.swift */; }; 11B350F58D6907C9A9A79F6B /* NftViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35419084A6CB11230E3C6 /* NftViewController.swift */; }; 11B350F8D2D09F1BD5ECF003 /* SendEvmViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F6B511DA5E0C60ED156 /* SendEvmViewModel.swift */; }; 11B350F9484020EFF74EFF1F /* MnemonicInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DB358405198CF67F11D /* MnemonicInputCell.swift */; }; 11B350F9E67A095998D9462C /* BalanceConversionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351609863530CA8259648 /* BalanceConversionManager.swift */; }; 11B350FEA2D819F569B0308E /* CexCoinSelectService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35FB89677FD9DA58D2DED /* CexCoinSelectService.swift */; }; - 11B350FF14C22F4CE7FB153F /* MarketAdvancedSearchModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DCB7125B0046592414B /* MarketAdvancedSearchModule.swift */; }; 11B3510BC26128273E969C7F /* TermsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B462980B0617E11FB05 /* TermsModule.swift */; }; 11B3511037E26318792E7DF3 /* WalletCoinPriceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3508A42F804E2010C7BB3 /* WalletCoinPriceService.swift */; }; 11B3511098C99D7B7D5A492A /* CexWithdrawNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3572F134D41A670EE9244 /* CexWithdrawNetwork.swift */; }; @@ -138,7 +130,6 @@ 11B3513AC9A4C945E6432475 /* EvmAddressViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35962622F74F89FD32D2B /* EvmAddressViewModel.swift */; }; 11B3513C8E7B2C3651F00F8F /* AccountStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E87C15A3AE82F471007 /* AccountStorage.swift */; }; 11B35146CA9BE897C858AB73 /* BinanceChainKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3525F8436F286491A241F /* BinanceChainKit.swift */; }; - 11B35147B6C9B62FC86A5BB9 /* CoinTreasuriesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351E253E310F1738EBE13 /* CoinTreasuriesViewController.swift */; }; 11B3514C6EC2B0BB2C6788BF /* PrivateKeysViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350D8D6E2FAD43FFCA8BB /* PrivateKeysViewController.swift */; }; 11B35150D2CC1B062E098C52 /* CexCoinSelectService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35FB89677FD9DA58D2DED /* CexCoinSelectService.swift */; }; 11B351581C801BF3A8415DBE /* TonAddressParserItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C6DF4DEE25B8B4B2E28 /* TonAddressParserItem.swift */; }; @@ -153,7 +144,6 @@ 11B3516FFFEA842D66C51988 /* AmountTypeSwitchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359E546B8F1E572E695F4 /* AmountTypeSwitchService.swift */; }; 11B351767B71D6C42A23C518 /* NftCollectionRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350E6966203DF2855369A /* NftCollectionRecord.swift */; }; 11B35177B650540BCEA880B3 /* UnlinkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351A464627DCBABD1AC17 /* UnlinkService.swift */; }; - 11B35179EE2C14CF52443183 /* SendAmountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352436A876FC59DF41C78 /* SendAmountView.swift */; }; 11B3517A67227ECCDE817036 /* CoinMajorHoldersModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35995DE2AAD2186441E38 /* CoinMajorHoldersModule.swift */; }; 11B3517B16E90C016A588C7C /* CexWithdrawConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3517F84E9913C9030E749 /* CexWithdrawConfirmViewController.swift */; }; 11B3517B2CB1A05BA5FEDB1F /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359697FC3E92D4111ED5D /* String.swift */; }; @@ -182,7 +172,6 @@ 11B351B95F191EEA750D9955 /* BalancePrimaryValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B354C4EB27186435736BBA /* BalancePrimaryValue.swift */; }; 11B351BB835C2EE7144D8D7E /* ActivateSubscriptionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3508AB65CCBDC18FEF2A6 /* ActivateSubscriptionViewController.swift */; }; 11B351C81028727569851233 /* EvmAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352D314A298B6B832F309 /* EvmAdapter.swift */; }; - 11B351D378E1EF2A72467B88 /* MultiSwapConfirmationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3573427BEEC59D4B978FA /* MultiSwapConfirmationViewModel.swift */; }; 11B351D834D8858391B32866 /* HighlightedDescriptionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CEE91732D3F18290263 /* HighlightedDescriptionCell.swift */; }; 11B351D9E1CAF8AA5BCE39F5 /* RecoveryPhraseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BCBAD15E32459826712 /* RecoveryPhraseViewModel.swift */; }; 11B351DB86D936CC17C4A635 /* PrivateKeysModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D55BE7717A87DA6FC43 /* PrivateKeysModule.swift */; }; @@ -193,11 +182,9 @@ 11B351E4BD2180A5D6D59F23 /* PoolGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357F4747A6B256C31EC7C /* PoolGroup.swift */; }; 11B351E6C8EF6B22C5F8B98D /* NftCollectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3529499CD211CC5A21CA2 /* NftCollectionService.swift */; }; 11B351EE1B16B2A26B5D6A40 /* CoinInvestorsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EC8B737D03ECC70CA80 /* CoinInvestorsService.swift */; }; - 11B351EEC95E342C2B4F5BC4 /* MarketHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3543F4D196A47EFE3E6F7 /* MarketHeaderCell.swift */; }; 11B351F04B82B33855E2CEBB /* EvmNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352EB0986D26399B7F89B /* EvmNetworkService.swift */; }; 11B351F1347A73080BB2795F /* TokenTransactionsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DB5445B83B51C69D7AE /* TokenTransactionsService.swift */; }; 11B351F2BE118946AD633035 /* BlockchainTokensService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35219C4AB26DC0D104E30 /* BlockchainTokensService.swift */; }; - 11B351F81CCF4675CBDAC9B0 /* DropdownSortHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F9BA41AC15436A4B977 /* DropdownSortHeaderView.swift */; }; 11B351F991634E3E6A0846EF /* NftHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35ECC6866F29A33129F06 /* NftHeaderView.swift */; }; 11B351FB99274553725754E4 /* GuidesModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352CFEDEBF0A01CC7073D /* GuidesModule.swift */; }; 11B351FC393EDD17C3487796 /* SelectorModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353C09FE554834C760777 /* SelectorModule.swift */; }; @@ -215,7 +202,6 @@ 11B3520F75BD8D46B1F44B3F /* CexAmountInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35071F0BD63CCE6417ADC /* CexAmountInputViewModel.swift */; }; 11B3520F7C6CEAF1710AAD45 /* MarkdownTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359DAB464176D8EBFC8A0 /* MarkdownTextView.swift */; }; 11B35210D072735192AD9BC8 /* NftAssetBriefMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359E32AEEE37347E255C4 /* NftAssetBriefMetadata.swift */; }; - 11B3521427196CEE9057D6A0 /* MarketAdvancedSearchResultModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3598FB2653DB1DC1429CA /* MarketAdvancedSearchResultModule.swift */; }; 11B352184FCE4B2B3E68E459 /* CurrentDateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35779E6353B98B298FF29 /* CurrentDateProvider.swift */; }; 11B352210BEEE91481291D4C /* FormCautionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3568F6FAF721301DEC188 /* FormCautionView.swift */; }; 11B3522207EA307D94070776 /* CoinPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3553967AFF40F6A9A611A /* CoinPageView.swift */; }; @@ -242,19 +228,16 @@ 11B35259209EA0C688BB2EC9 /* PasscodeLockState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B6F5261FF3F9ECBC02E /* PasscodeLockState.swift */; }; 11B3525BA3799B70B25CF2FC /* SingleCoinPriceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F0C950836EA4166CEC5 /* SingleCoinPriceProvider.swift */; }; 11B3525DD29BC2286526669F /* PoolGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357F4747A6B256C31EC7C /* PoolGroup.swift */; }; - 11B3525F2CAA4B98F29705A7 /* SendConfirmationNewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BEF6F80BDF166173819 /* SendConfirmationNewViewModel.swift */; }; + 11B3525F2CAA4B98F29705A7 /* SendViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BEF6F80BDF166173819 /* SendViewModel.swift */; }; 11B35262A6D3AB8C41E2E245 /* AccountStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E87C15A3AE82F471007 /* AccountStorage.swift */; }; 11B35262B98EA59CDA12DF97 /* WalletConnectSessionStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355C1E3C922BAE804AAF9 /* WalletConnectSessionStorage.swift */; }; 11B35264EC1BABABCDDD1F67 /* TonIncomingTransactionRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3577A9294A3EE662872D7 /* TonIncomingTransactionRecord.swift */; }; 11B3526AA8E758606BC0CE38 /* CexWithdrawService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3548F0E1223B08D3B7F0C /* CexWithdrawService.swift */; }; - 11B3526D1747C11291F2D998 /* CoinTreasuriesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3580ECB328146E94D4359 /* CoinTreasuriesService.swift */; }; - 11B3527103D25C72BC849651 /* MarketOverviewTopPairsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350465C489A233625E8F2 /* MarketOverviewTopPairsDataSource.swift */; }; - 11B352712EC6F2C7F6965443 /* MarketWatchlistViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3566FE007887C3528583C /* MarketWatchlistViewModel.swift */; }; 11B3527C3BD088DCCA6959C3 /* ModuleUnlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35FF02BBEDAEF446D0610 /* ModuleUnlockView.swift */; }; 11B3527D20636D21F0F45C80 /* CurrentDateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35779E6353B98B298FF29 /* CurrentDateProvider.swift */; }; 11B3527F2E2D46DC307E6D3D /* RestoreSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E343901BA7DE01181CB /* RestoreSettingsViewModel.swift */; }; 11B35281808DE30B8D717B73 /* PublicKeysViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C285327E4099656DBA8 /* PublicKeysViewModel.swift */; }; - 11B35283F8170DB664A77C2B /* SendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353355FF7FBE72BF60981 /* SendView.swift */; }; + 11B35283F8170DB664A77C2B /* PreSendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353355FF7FBE72BF60981 /* PreSendView.swift */; }; 11B352840E06275F96EFFCDB /* BaseCurrencySettingsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B57AFCEAA3AA071F07F /* BaseCurrencySettingsModule.swift */; }; 11B352841C5901ABCC0096C2 /* BlockchainSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3573A58F426A72669A948 /* BlockchainSettingsViewModel.swift */; }; 11B35286305AB7DA6114F1D0 /* SwitchAccountService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359D884F1698E70F2536E /* SwitchAccountService.swift */; }; @@ -268,7 +251,6 @@ 11B3529B3DD134BC0770BD20 /* MarkdownModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35609D3FA1729A7D80153 /* MarkdownModule.swift */; }; 11B3529CFD24A94DC35B476E /* CoinAnalyticsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358DFD25E8DC35F689D5C /* CoinAnalyticsViewModel.swift */; }; 11B3529D506ADAEB715BF0D1 /* RestorePrivateKeyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35625BCC4536F39B151F0 /* RestorePrivateKeyViewModel.swift */; }; - 11B352A7A3457BBAF4BF704F /* MarketOverviewCategoryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A0AF4D03160AF66D1D9 /* MarketOverviewCategoryService.swift */; }; 11B352AA36A25DF590166418 /* NftAddressMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BEEB24CDB82D3F4E7C0 /* NftAddressMetadata.swift */; }; 11B352AB213E0F3C147EAEE9 /* SendEvmTransactionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B31362C98B401A8F9A1 /* SendEvmTransactionViewController.swift */; }; 11B352AE20E447BA1E9E890B /* SwitchAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350BC3E707879846AC0AA /* SwitchAccountViewModel.swift */; }; @@ -284,11 +266,9 @@ 11B352CBDFAABC82F02F66E9 /* MarkdownHeader1Cell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D31D3EC415789CFA160 /* MarkdownHeader1Cell.swift */; }; 11B352D006136B42D2705778 /* BalanceData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BDEB703708795B71C4E /* BalanceData.swift */; }; 11B352D47A0F5A3E4AF8F948 /* BaseUniswapV3MultiSwapProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351FAE6B01F29FC37B3C2 /* BaseUniswapV3MultiSwapProvider.swift */; }; - 11B352D8DDF054073BC79FC2 /* CoinRankModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358A22655004017228F65 /* CoinRankModule.swift */; }; 11B352DF096B6CFD050D500D /* CoinRecord_v19.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B2C6C103AFF4CCC6E91 /* CoinRecord_v19.swift */; }; 11B352E00B6E0DF4F6D42486 /* DashAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358CA18471A93188933B4 /* DashAdapter.swift */; }; 11B352E420B3FBCD85612E63 /* TransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350CCAA0C9F2F5279F680 /* TransactionsViewController.swift */; }; - 11B352E46C24498018071705 /* MarketCategoryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357229D5E717F2051F0AC /* MarketCategoryService.swift */; }; 11B352E503309454C976ED03 /* MarkdownImageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3563ED22080EE222848A5 /* MarkdownImageCell.swift */; }; 11B352E613B0EC5D0DA14570 /* RestoreSelectViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DCDDACF2BB1E0748ABB /* RestoreSelectViewModel.swift */; }; 11B352E8348A715EB537F643 /* TransactionFilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3544AC69419F31F20F34E /* TransactionFilterViewModel.swift */; }; @@ -302,7 +282,6 @@ 11B352FC06650CD111076054 /* TextInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35968F5DE9FDA6EC26FCD /* TextInputCell.swift */; }; 11B352FF1C3FA152E2EEFE67 /* EvmMethodLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E3F01D5A5CFE5A4E94B /* EvmMethodLabel.swift */; }; 11B3530088E70831A648EC63 /* CexDepositNetworkRaw.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3502198C667A95C21DCF3 /* CexDepositNetworkRaw.swift */; }; - 11B3530307FFDC0AF9D3A8F2 /* CoinPriceListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355D1DB2F95F1183FF2F8 /* CoinPriceListView.swift */; }; 11B35307AE70D7996F483DAE /* InputStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353BA87FDCB1BCBA92E61 /* InputStackView.swift */; }; 11B353096900F82EDF084F3B /* SetPasscodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F99E093B7DDB24D39C9 /* SetPasscodeViewModel.swift */; }; 11B35309CE9FBDA200067C4F /* ActiveAccount_v_0_36.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3528DDD55DDA1BAC2BADB /* ActiveAccount_v_0_36.swift */; }; @@ -316,11 +295,9 @@ 11B3531D97E44DA1D8280C35 /* EditDuressPasscodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3594CBF3EA39A848D22EB /* EditDuressPasscodeViewModel.swift */; }; 11B3531F75BB6113B49DC088 /* BarPageControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3512EF5B66B852F5E05FB /* BarPageControl.swift */; }; 11B35321C9FCFD1DFA4401A3 /* SendEvmService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3540BDD94203AFD41C6C7 /* SendEvmService.swift */; }; - 11B353260AE7B998C07955E6 /* MarketCategoryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357229D5E717F2051F0AC /* MarketCategoryService.swift */; }; 11B35328067C30C80DF244DF /* BackupVerifyWordsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3576FCFC9394BA37975FC /* BackupVerifyWordsViewModel.swift */; }; 11B35328EA42C49649B1E3F6 /* SyncMode_v_0_24.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CB98A27269A510F40EE /* SyncMode_v_0_24.swift */; }; 11B3532D03B0893AB8E46CD9 /* BalanceViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3591AD106DAC0D18FEDD7 /* BalanceViewItem.swift */; }; - 11B3532D56D7D1F8286B39AC /* FavoriteCoinRecordStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C7B8BA65E9AA3BB7AFB /* FavoriteCoinRecordStorage.swift */; }; 11B3532F1BE8F0D1A6B43A33 /* EvmSyncSourceStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3564E87C69B2989E6A3D2 /* EvmSyncSourceStorage.swift */; }; 11B35337D37FD03982571DF0 /* ThorChainMultiSwapProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B323F0836767BA0565F /* ThorChainMultiSwapProvider.swift */; }; 11B3533941A80D693369E9C0 /* BrandFooterCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355DF40EB498107EDAA4A /* BrandFooterCell.swift */; }; @@ -354,7 +331,6 @@ 11B3538FA5A4953A7C9AC9E6 /* SingleCoinPriceWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351F5E57874D4517F67B7 /* SingleCoinPriceWidget.swift */; }; 11B353917D2223D2B275429A /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359D48FD6D82735C8969A /* Error.swift */; }; 11B35394C8DD94B9C726B22B /* CexAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359198B26152903D5CA14 /* CexAccount.swift */; }; - 11B353973C1ADD51D174AC74 /* CoinRankService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3513AC6560B9C37C342F3 /* CoinRankService.swift */; }; 11B3539B3634BF7B3B1B9061 /* DescriptionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3515BDAF15B6F7EEAB609 /* DescriptionCell.swift */; }; 11B3539E833ABB2D6F696916 /* BlockchainSettingRecord_v_0_24.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3526A40F07F6C8E77BEF9 /* BlockchainSettingRecord_v_0_24.swift */; }; 11B353A07F9259765D90F3BA /* NftService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355129D9F61172FCAB8C0 /* NftService.swift */; }; @@ -366,20 +342,16 @@ 11B353BAEF83867422611E7B /* TransactionFilterModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BCF17FE0D9238AA4341 /* TransactionFilterModule.swift */; }; 11B353C149EC597A051E8310 /* BinanceWithdrawHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3576C0D8464F74D44EE92 /* BinanceWithdrawHandler.swift */; }; 11B353C7553F40CEEA28678B /* PasscodeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B5570E7513DF2A455BB /* PasscodeManager.swift */; }; - 11B353CB3021FA5266D07607 /* MarketWatchlistToggleService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3562819DF141457837340 /* MarketWatchlistToggleService.swift */; }; 11B353CBE0B92639753A9591 /* FaqRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358D98E1FBA6909D352DA /* FaqRepository.swift */; }; 11B353CE5AFDEB1F3BBF73F9 /* OpenSeaNftProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C1DF4F98D814CFE3951 /* OpenSeaNftProvider.swift */; }; 11B353D3A4F2305366835086 /* NftActivityHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351EC6F1B4D72D52B4D16 /* NftActivityHeaderView.swift */; }; 11B353D4BC292465358979D3 /* ValueLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355166E437B7ADB8B8EBA /* ValueLevel.swift */; }; 11B353DE48A4B088210D927D /* CoinAnalyticsRatingScaleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35FDC67CE58FBE44A4107 /* CoinAnalyticsRatingScaleViewController.swift */; }; - 11B353E15F4A208D393C7262 /* MarketCategoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3584888F2DB8CCFAA90DF /* MarketCategoryViewController.swift */; }; - 11B353E17EA348D431520FAD /* MarketListMarketPairDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3557E5ACDC89EF79C8C0C /* MarketListMarketPairDecorator.swift */; }; 11B353E4793549B6A4F23997 /* CoinOverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3506BFA73130CA9A1FF71 /* CoinOverviewView.swift */; }; 11B353E61A5496074178741C /* SendAvailableBalanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3526E11EC0F9CFCC69D17 /* SendAvailableBalanceViewModel.swift */; }; 11B353E7A2462E19D946E723 /* CellComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355436F62829DBE3C92B4 /* CellComponent.swift */; }; 11B353EAF32244B06E44FAD1 /* PrivateKeysViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A9DB4112F41D7FCAC12 /* PrivateKeysViewModel.swift */; }; 11B353EDF27AB81FCF2A36EE /* GuidesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352E6CB5B964E2A1521CC /* GuidesViewModel.swift */; }; - 11B353F04B500616AC5CB9C5 /* MarketTopPairsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C5CA7497C540FFC5D39 /* MarketTopPairsViewController.swift */; }; 11B353F2788F7E8F42C5E03D /* EvmPrivateKeyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352E62EBBDE01560EB2E4 /* EvmPrivateKeyViewController.swift */; }; 11B353F671EA70F8BE7F02F0 /* NftAssetTitleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353D752983424F341F2FC /* NftAssetTitleCell.swift */; }; 11B353F6BB87F0F1933D63C2 /* AddEvmSyncSourceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F8A77664848396B7567 /* AddEvmSyncSourceService.swift */; }; @@ -397,7 +369,7 @@ 11B3540F182F3EDE74245EC7 /* MainSettingsFooterCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BD9A836C953CCF8D077 /* MainSettingsFooterCell.swift */; }; 11B35411C817EDE73D4E6242 /* CoinPageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BFAAAE3B1357B5CE944 /* CoinPageService.swift */; }; 11B3541A1D615503ECD614D8 /* MultiSwapAllowanceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35914D68AEC1242C482FA /* MultiSwapAllowanceHelper.swift */; }; - 11B3541A3A7BDE85240F71A0 /* SendConfirmationNewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B91B5EAF2E193FDC04E /* SendConfirmationNewView.swift */; }; + 11B3541A3A7BDE85240F71A0 /* SendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B91B5EAF2E193FDC04E /* SendView.swift */; }; 11B3541BC8A0EF1066EBA464 /* NonSpamPoolProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359FE5BB60FB12BB24F3E /* NonSpamPoolProvider.swift */; }; 11B3541D3F7AFB8C651BEE2B /* Guide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353B4C04282FDBB1B6563 /* Guide.swift */; }; 11B3541E8CB5F0F743E9CDF3 /* AppWidgetConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E298D53B8A2C2684119 /* AppWidgetConstants.swift */; }; @@ -408,11 +380,9 @@ 11B35425857F772B06E7805D /* CexWithdrawViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35ABC3E6C990E3BFA0A7B /* CexWithdrawViewController.swift */; }; 11B3542694E183882F9BEBEC /* CoinPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3553967AFF40F6A9A611A /* CoinPageView.swift */; }; 11B3542831EAA647A1D16E8A /* MarkdownVisitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DC72F0D8DBBCCE2F988 /* MarkdownVisitor.swift */; }; - 11B354283B8AC609B65AADDF /* FavoriteCoinRecord_v_0_22.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E255F6CA21FFA9E6B42 /* FavoriteCoinRecord_v_0_22.swift */; }; + 11B354283B8AC609B65AADDF /* FavoriteCoinRecord_v_0_38.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E255F6CA21FFA9E6B42 /* FavoriteCoinRecord_v_0_38.swift */; }; 11B3542A74C72C4CE03C727B /* CoinToggleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A3B8FB90E561DE3F22B /* CoinToggleViewController.swift */; }; - 11B3542DB7195238D606D057 /* MultiSwapConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353F8063B95C6571AA517 /* MultiSwapConfirmationView.swift */; }; 11B35434C09F1E3818DC857B /* ReceiveDerivationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357EEC98939F9C7AA3271 /* ReceiveDerivationViewModel.swift */; }; - 11B3543546FE55AE0AB91FAF /* MarketOverviewTopPairsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352BDC42A2F717AFAE7BD /* MarketOverviewTopPairsService.swift */; }; 11B3543A420A23064B056925 /* ReceiveAddressViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3532A1DC90E3D0E3403F8 /* ReceiveAddressViewModel.swift */; }; 11B3543A7A9EB1E0E0E8753D /* DuressModeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F5B696CF0677865FA2C /* DuressModeViewModel.swift */; }; 11B3543EF331DA9E5E33822A /* SingleCoinPriceEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E514EF2784402C7DC2A /* SingleCoinPriceEntry.swift */; }; @@ -428,7 +398,6 @@ 11B354542A3E929C1A7924FD /* CoinReportsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C7FCEFE15A50EB5C6E0 /* CoinReportsViewController.swift */; }; 11B3545A8A2A23ADB5BA1E0A /* WelcomeTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A05B93CB243B6404C4A /* WelcomeTextView.swift */; }; 11B3545B6D4BA7CC76ACB9D2 /* ThemeList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B354E3FDE8CA30D8983EB4 /* ThemeList.swift */; }; - 11B3545B8A5568792A4C43D8 /* CoinTreasuriesModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F08C14B3F0D978E2E7F /* CoinTreasuriesModule.swift */; }; 11B3545D4765792A91CF5817 /* EvmTransactionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3564FC860C383807BFBE3 /* EvmTransactionService.swift */; }; 11B3546AC03E6B632D155766 /* MarkdownImageTitleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353002DD782C5BEE9BFD4 /* MarkdownImageTitleCell.swift */; }; 11B3546CB3C043D22A5F7A88 /* TransactionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B9BA734A13A0ADC507E /* TransactionsViewModel.swift */; }; @@ -456,7 +425,7 @@ 11B354AC828E53D98222EE71 /* SubscriptionInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351CD91AE01747F66E746 /* SubscriptionInfoViewController.swift */; }; 11B354AD94C483A7D5E3DD1F /* ValueFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EFB45ECC2D403CA6C89 /* ValueFormatter.swift */; }; 11B354B34C4B6471F67F5471 /* InputPrefixWrapperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35743158763BD8E336770 /* InputPrefixWrapperView.swift */; }; - 11B354B484C4785A27216BEC /* ISendConfirmationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355A2FFA369C4E89DFF53 /* ISendConfirmationData.swift */; }; + 11B354B484C4785A27216BEC /* ISendData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355A2FFA369C4E89DFF53 /* ISendData.swift */; }; 11B354B499FB939FE08AB111 /* BlockchainTokensModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BC10B98A0770A2AC342 /* BlockchainTokensModule.swift */; }; 11B354B551E301C652D67319 /* WCSendEthereumTransactionRequestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BD6F543322880433ACD /* WCSendEthereumTransactionRequestViewController.swift */; }; 11B354B5E42290EE934C428E /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359697FC3E92D4111ED5D /* String.swift */; }; @@ -464,8 +433,6 @@ 11B354BC4D954CCDA2E75C68 /* AddEvmSyncSourceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350B29037572DDAAF9E16 /* AddEvmSyncSourceViewModel.swift */; }; 11B354C1218C0776499FAA5E /* Kmm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D813B2B43683404CCD6 /* Kmm.swift */; }; 11B354CAD4BC4FAB3889838D /* EvmSyncSourceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D9C2409FD9060974F67 /* EvmSyncSourceManager.swift */; }; - 11B354CBCCB0FFD2FDBEE757 /* MarketFilteredListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35ADF9BC4D149F86F23E4 /* MarketFilteredListService.swift */; }; - 11B354CC5E68F04E22D633D9 /* TopPlatformHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357E05A8AF5608ECF5D5F /* TopPlatformHeaderCell.swift */; }; 11B354CF393A2EAFDABE1C47 /* WalletTokenListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357D222B4819BE881E182 /* WalletTokenListViewController.swift */; }; 11B354D628AADF3AFD9123E1 /* SingleCoinPriceWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351F5E57874D4517F67B7 /* SingleCoinPriceWidget.swift */; }; 11B354D6DE193776FFACE1B5 /* AddEvmSyncSourceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351F33517C6DDA1E7AF59 /* AddEvmSyncSourceViewController.swift */; }; @@ -477,7 +444,6 @@ 11B354E72D9BDF04E75C8748 /* WalletHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3597A2B0B529BE97F85C8 /* WalletHeaderCell.swift */; }; 11B354E85FD7EE82D34FD1C4 /* BalanceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BB7206DA0EDBB43C814 /* BalanceCell.swift */; }; 11B354EB71E52DF79A003231 /* FaqUrlHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35759E226171A4969E66E /* FaqUrlHelper.swift */; }; - 11B354ECE594CE629BA46BE1 /* MarketCategoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352AC4F5BE70D055293D7 /* MarketCategoryViewModel.swift */; }; 11B354EE16A63DAD95DE3861 /* ManageWalletsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3550ED151B4C6824B9779 /* ManageWalletsViewModel.swift */; }; 11B354EFE4620A8E65D44335 /* WalletViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357426B767AA64ED8E7A2 /* WalletViewModel.swift */; }; 11B354F237E59C24ED8F3759 /* TokenQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353684493AFDF3711DF2B /* TokenQuery.swift */; }; @@ -493,7 +459,7 @@ 11B35504619FDE878D865781 /* MultiSwapMainField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D0D43137223A01FC2DA /* MultiSwapMainField.swift */; }; 11B35504EF11FA59D2A358BE /* SendTonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F5835A746165C520333 /* SendTonViewController.swift */; }; 11B3550548CB49D32EAC1DF5 /* WatchlistWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B0879F715C0777919AA /* WatchlistWidget.swift */; }; - 11B3550846F2DD60D3778FE5 /* SendEvmHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B0F9BC5C4CDBC5B041D /* SendEvmHandler.swift */; }; + 11B3550846F2DD60D3778FE5 /* EvmSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B0F9BC5C4CDBC5B041D /* EvmSendHandler.swift */; }; 11B35508E26446AC692EBAEF /* RecipientAndSlippageMultiSwapSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3599833F166185872A3AC /* RecipientAndSlippageMultiSwapSettingsView.swift */; }; 11B3550A6826CF513B1A77F0 /* TabHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357D7156B86181DD0C6D4 /* TabHeaderView.swift */; }; 11B3550B0E0438427CBE72A3 /* NftCollectionOverviewModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35507299A9DA6CF3C626A /* NftCollectionOverviewModule.swift */; }; @@ -508,7 +474,6 @@ 11B3552937152C5A4169C018 /* PasscodeLockManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3505A43D9C2787B3BD153 /* PasscodeLockManager.swift */; }; 11B35529AB46C98BC35C72E4 /* CoinPriceListProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EE1C1F555F4160AC201 /* CoinPriceListProvider.swift */; }; 11B35530A9FC0972D8716C31 /* ValueFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357A5569EAC7D20CD40B2 /* ValueFormatter.swift */; }; - 11B3553109794AE192BF7591 /* MarketCategoryModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350CAB1C54A2CAA4C76F6 /* MarketCategoryModule.swift */; }; 11B35531B3F80D06EF040301 /* CoinType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357B2D07C69579BAEC997 /* CoinType.swift */; }; 11B355342F86DF79AE7000B9 /* Cex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D6718A1DEB73A0CEC02 /* Cex.swift */; }; 11B35538EF749777CF7B2E8B /* ChartUiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DE812F995B07C8F0B01 /* ChartUiView.swift */; }; @@ -524,7 +489,7 @@ 11B3555B8D452B7F64815FAC /* WalletStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BBEA6AF9464C818389E /* WalletStorage.swift */; }; 11B3555CA9B2F01358E055BE /* UnlinkViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35419B0C846238DDC50F3 /* UnlinkViewModel.swift */; }; 11B3555F968EFA0AF7D1DF46 /* WalletElementServiceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F4B9522FCCD91582AAF /* WalletElementServiceFactory.swift */; }; - 11B35563DA099AE8ABE34F8D /* SendConfirmationNewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BEF6F80BDF166173819 /* SendConfirmationNewViewModel.swift */; }; + 11B35563DA099AE8ABE34F8D /* SendViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BEF6F80BDF166173819 /* SendViewModel.swift */; }; 11B35567A098667C9955F1F9 /* RecoveryPhraseModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3517B0E763E2C217654A7 /* RecoveryPhraseModule.swift */; }; 11B355696714B5570748EF03 /* AccountRecord_v_0_36.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356A734526DECD9606A66 /* AccountRecord_v_0_36.swift */; }; 11B3556B4E9B6E54C93205D6 /* CexCoinSelectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352F071CE0EF1505A8380 /* CexCoinSelectViewController.swift */; }; @@ -534,8 +499,7 @@ 11B355734D16C412220BBEBD /* NftKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35665980CEA4D009A9B77 /* NftKit.swift */; }; 11B35573BC52BFE9E545AA01 /* GuideCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EC5CADBD290DDD3DE1C /* GuideCell.swift */; }; 11B355756A2457C64C969024 /* CoinCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357C17104792A20769560 /* CoinCategory.swift */; }; - 11B35577071CBB59D7692DE4 /* MarketTopPairsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C5CA7497C540FFC5D39 /* MarketTopPairsViewController.swift */; }; - 11B3557AF8D64E9897965526 /* ISendConfirmationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355A2FFA369C4E89DFF53 /* ISendConfirmationData.swift */; }; + 11B3557AF8D64E9897965526 /* ISendData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355A2FFA369C4E89DFF53 /* ISendData.swift */; }; 11B3557CB2595D2884C94498 /* MultiTextMetricsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359824DCDF3B05413CDD2 /* MultiTextMetricsView.swift */; }; 11B3557D484786A0D41E16AF /* AlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357736B8C29DF38F5DCBA /* AlertViewController.swift */; }; 11B3557E460F3BA25ED9F6CC /* AlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F4A3C8D3D2C6579FD94 /* AlertPresenter.swift */; }; @@ -561,7 +525,6 @@ 11B355BDD19498AA9CD250BB /* LanguageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3508A186E4CC100967FD3 /* LanguageSettingsView.swift */; }; 11B355C5BB0C447A0395168C /* GuidesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D9767615D8FBF7A314F /* GuidesManager.swift */; }; 11B355C78B016FC2EDDAECCC /* SendEvmModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3515139F2682C9C733F3D /* SendEvmModule.swift */; }; - 11B355C8567EB1690E3BDA77 /* MarketOverviewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3554159E6E5B7C1E71F04 /* MarketOverviewService.swift */; }; 11B355CA37285348558E98A9 /* SwitchAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352E8A5E0463B30001AFE /* SwitchAccountViewController.swift */; }; 11B355D89EADD907D4FC3273 /* MultiSwapButtonState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35140CD5BF8B1C26A6278 /* MultiSwapButtonState.swift */; }; 11B355DB012D5856C675FE72 /* CreateAccountService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3592A5323E54639864FC7 /* CreateAccountService.swift */; }; @@ -583,7 +546,6 @@ 11B35608F7D19B3E6318CB22 /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352972B14FA6EBEFD6904 /* Text.swift */; }; 11B3560E158C55624C466E27 /* GuidesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E511F9D2B6C65792324 /* GuidesViewController.swift */; }; 11B3560F69D84432665A2BAA /* CoinPageViewModelNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3529DC8E74672659515B8 /* CoinPageViewModelNew.swift */; }; - 11B35611F21D266215BD82A5 /* MarketOverviewTopPairsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350465C489A233625E8F2 /* MarketOverviewTopPairsDataSource.swift */; }; 11B3561679C05C31F16EDC77 /* BaseUnlockViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351F8A0A9EB045377C152 /* BaseUnlockViewModel.swift */; }; 11B3561A469C906B67F24459 /* FeeRateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359BBFCD82C3C6DC06F96 /* FeeRateProvider.swift */; }; 11B3561E7DF566A274210E01 /* EvmSyncSourceRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B56F5C8138085588EE5 /* EvmSyncSourceRecord.swift */; }; @@ -611,8 +573,7 @@ 11B3565617F5E45C0B86AFED /* ReceiveViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CFED85A9315089223E3 /* ReceiveViewModel.swift */; }; 11B356562D2B4F5BCAB4FC80 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357C3907AC1134C7A95DB /* AboutView.swift */; }; 11B3565D4E4EAD663143ED9B /* BaseCurrencySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D203692A7F12D67242D /* BaseCurrencySettingsView.swift */; }; - 11B3565DC90BD451F8DE7120 /* SendEvmHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B0F9BC5C4CDBC5B041D /* SendEvmHandler.swift */; }; - 11B35664B1EDEAB99B7B51AE /* MarketCategoryModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350CAB1C54A2CAA4C76F6 /* MarketCategoryModule.swift */; }; + 11B3565DC90BD451F8DE7120 /* EvmSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B0F9BC5C4CDBC5B041D /* EvmSendHandler.swift */; }; 11B356655BCF0A3919AD5120 /* ActivateSubscriptionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3508AB65CCBDC18FEF2A6 /* ActivateSubscriptionViewController.swift */; }; 11B35665CC02390699802C61 /* RestoreBinanceModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35995E0D358AC4DA2FA74 /* RestoreBinanceModule.swift */; }; 11B3566FB55E128CD6F22DAE /* TransactionSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3514BFFAE3CE09F4DB2EA /* TransactionSettings.swift */; }; @@ -646,7 +607,6 @@ 11B356C6FEEFD7A1B854FB46 /* AccountRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E9B9C7A88B7584507DF /* AccountRecord.swift */; }; 11B356C983C2A2B552D214A4 /* ListSectionFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352970EA9924258E5BB75 /* ListSectionFooter.swift */; }; 11B356CF0D78F2DC6F28B4BD /* LaunchErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A4096D259C9B1540D10 /* LaunchErrorViewController.swift */; }; - 11B356CF55DB1BE22071B24E /* MarketMultiSortHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350FAB6F1A6E1FCFACB2F /* MarketMultiSortHeaderViewModel.swift */; }; 11B356D4E85B0A0133F6870C /* RestorePrivateKeyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351EBA5DE11150CE2E3F9 /* RestorePrivateKeyService.swift */; }; 11B356D60C39544F165547AA /* TermsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351DAF31FBE0834EBC066 /* TermsService.swift */; }; 11B356D67F706464900DBD25 /* CexCoinService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3590EB4E34B278277E8E4 /* CexCoinService.swift */; }; @@ -690,7 +650,6 @@ 11B3573B753DB244EEBAAA35 /* ReceiveSelectCoinViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358B8D6DFEAEDE84D53DE /* ReceiveSelectCoinViewController.swift */; }; 11B3573B8B8DA5C1DE332EB6 /* EvmNetworkViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35711A471C5A45DD87108 /* EvmNetworkViewController.swift */; }; 11B3573F7ED8577EF9F12EF9 /* EvmAccountRestoreStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350A3E8F85D6FA2E17173 /* EvmAccountRestoreStateManager.swift */; }; - 11B357405174FF9F9BEB3704 /* MarketTopModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F9DA79410E7B9C1B0F8 /* MarketTopModule.swift */; }; 11B357408E3CCC0B93C42F67 /* WCSignEthereumTransactionRequestViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A74A323368707589FA3 /* WCSignEthereumTransactionRequestViewModel.swift */; }; 11B3574287AAA5FC16E3E3DA /* NavigationRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3578FB80AA013BD351A26 /* NavigationRow.swift */; }; 11B35749EBFB7FE593BECE9E /* ExtendedKeyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F95A84DD0F232E5A9CD /* ExtendedKeyViewModel.swift */; }; @@ -718,13 +677,11 @@ 11B357A607396E857705024F /* WalletTokenCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B64097CCFA552310E3D /* WalletTokenCell.swift */; }; 11B357A9F8949912C12A17D7 /* NftCollectionOverviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351436E090F4C05243103 /* NftCollectionOverviewViewModel.swift */; }; 11B357AD2632BDF26DCB4BFC /* HorizontalDivider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D0EBAF33901578520E1 /* HorizontalDivider.swift */; }; - 11B357ADA154348A3C1A987B /* CoinTreasuriesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351E253E310F1738EBE13 /* CoinTreasuriesViewController.swift */; }; 11B357AE8B51E09D0EB60D87 /* NftPriceRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B451378835F7F060012 /* NftPriceRecord.swift */; }; 11B357BA09F0FA21477F0A59 /* CoinOverviewViewModelNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35363A530051B79BFFFD0 /* CoinOverviewViewModelNew.swift */; }; 11B357BADA228BE93B8451E7 /* AddEvmSyncSourceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350B29037572DDAAF9E16 /* AddEvmSyncSourceViewModel.swift */; }; 11B357BD9D9681D0D79DDEBE /* UITabBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350369A891BEA3A525E5B /* UITabBarItem.swift */; }; 11B357BF378060E7E35F7052 /* AdditionalDataCellNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E67C1B1AB7A13074894 /* AdditionalDataCellNew.swift */; }; - 11B357BF7588CB317EA62167 /* MarketOverviewCategoryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A0AF4D03160AF66D1D9 /* MarketOverviewCategoryService.swift */; }; 11B357C425D633543FD109C3 /* DuressModeSelectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3554BC96C9C24C24CC2B0 /* DuressModeSelectView.swift */; }; 11B357C5FC1B7FDE86244DA5 /* SingleSelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CAE2327342F9CEC6AC9 /* SingleSelectorViewController.swift */; }; 11B357D1A2BD673DAB7B4C61 /* SecondaryButtonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3587A6A05EFF1036F6C4B /* SecondaryButtonCell.swift */; }; @@ -744,7 +701,6 @@ 11B357F22958C3AB3818F20A /* AmountInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C3E03A9679D4B7E0D29 /* AmountInputCell.swift */; }; 11B357F36D1D90B2C54999AE /* BottomGradientWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3557DF76CFEBE7DA50D81 /* BottomGradientWrapper.swift */; }; 11B357F4C63379217B25AA75 /* RestoreSelectModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358C7DF6F82875527031E /* RestoreSelectModule.swift */; }; - 11B357FDC1C6BD6C39FE6853 /* MarketAdvancedSearchResultModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3598FB2653DB1DC1429CA /* MarketAdvancedSearchResultModule.swift */; }; 11B357FE4C2E1EC8E26ED68F /* StorageMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A1AE56A94BEB52AC4D1 /* StorageMigrator.swift */; }; 11B357FF80E87451A99BEE4A /* AccountRecord_v_0_36.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356A734526DECD9606A66 /* AccountRecord_v_0_36.swift */; }; 11B357FF94D326846E12B940 /* WalletManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352D547F1BB38D2AD6AD5 /* WalletManager.swift */; }; @@ -758,7 +714,6 @@ 11B3580BC3B5D2CBC68854D2 /* UIWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353356496AA219686B993 /* UIWindow.swift */; }; 11B3580CD18A931ABAA6C122 /* BiometryType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359A35AEB7964A94AFFC0 /* BiometryType.swift */; }; 11B3580E4A964C65BF8EDDE9 /* StackViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35AAE4114A56DF13ECF0F /* StackViewCell.swift */; }; - 11B358122FE64E16EC25F095 /* MarketOverviewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3554159E6E5B7C1E71F04 /* MarketOverviewService.swift */; }; 11B358137165A40C30218032 /* TermsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B462980B0617E11FB05 /* TermsModule.swift */; }; 11B358164F9FBBE78CBC806A /* NftActivityModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35252F90F25774BDD2CB3 /* NftActivityModule.swift */; }; 11B35819C7596909297311B2 /* Eip20Kit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3524B273DD5AB2FF5C7A6 /* Eip20Kit.swift */; }; @@ -792,7 +747,6 @@ 11B3586009D99341D46E1824 /* GuideCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EC5CADBD290DDD3DE1C /* GuideCell.swift */; }; 11B358622B30F9ED5B734A94 /* WalletHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3597A2B0B529BE97F85C8 /* WalletHeaderCell.swift */; }; 11B358623111DC1A8ED499DC /* EvmAddressViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35962622F74F89FD32D2B /* EvmAddressViewModel.swift */; }; - 11B358634BEC799056209B93 /* MarketTopPairsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355C615D9FE4290671D5D /* MarketTopPairsModule.swift */; }; 11B358657FCC50C9B3A10294 /* ManageWalletsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C4D645B4468F84EADB7 /* ManageWalletsViewController.swift */; }; 11B3586BF6AC0538272E71A4 /* NftCollectionModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35708A630D70385F34A8B /* NftCollectionModule.swift */; }; 11B3586F6BFCA16BDFD5921D /* DuressModeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A81FB3D4C06BBFEE7E7 /* DuressModeModule.swift */; }; @@ -811,7 +765,6 @@ 11B3588E8DA44A45661351D7 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35614C6E244926AF48701 /* Account.swift */; }; 11B358902CE8D7EF2AD38448 /* CoinService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3550B863970039577CD00 /* CoinService.swift */; }; 11B35890939A3326B352A0FB /* ActiveAccountStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B354C4B46DF1A50103F026 /* ActiveAccountStorage.swift */; }; - 11B358946C5E7A72712EACB2 /* MarketCategoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DB992C240A4CF24938A /* MarketCategoryView.swift */; }; 11B3589541E87A032D9D2D50 /* BottomSingleSelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EF3688D60C8E6823267 /* BottomSingleSelectorViewController.swift */; }; 11B35895C483A56545A6700E /* CexDepositNetworkSelectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C227EDC2D4ED188A0FC /* CexDepositNetworkSelectViewController.swift */; }; 11B35897566548048FCEC11E /* WalletBlockchainElementService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35AE5785634316A1A5DA8 /* WalletBlockchainElementService.swift */; }; @@ -840,7 +793,6 @@ 11B358C3281DE0A34D192CF0 /* SwitchAccountModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351AEFE48529C069B892F /* SwitchAccountModule.swift */; }; 11B358C4D4C466ACCEF0E4C7 /* MultiSwapMainField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D0D43137223A01FC2DA /* MultiSwapMainField.swift */; }; 11B358C72B4E7F70331084AA /* SendEvmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35113CB935A0E54504C1C /* SendEvmViewController.swift */; }; - 11B358CB129212E2A0E455E4 /* MarketAdvancedSearchResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353A1CC274EDBF8A67DEA /* MarketAdvancedSearchResultViewController.swift */; }; 11B358D01760F90518DA612F /* SendHandlerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353B02ADF5EC5CC83FB33 /* SendHandlerFactory.swift */; }; 11B358D0D4AE015DC9FECF29 /* Kmm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D813B2B43683404CCD6 /* Kmm.swift */; }; 11B358D1687049E5DACEBC96 /* AppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352884D47E0B23DCF2C2C /* AppManager.swift */; }; @@ -856,11 +808,8 @@ 11B358E7A9BC36B1B562A5B4 /* NftMetadataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359FE71F5DE6AAD2BA3D8 /* NftMetadataManager.swift */; }; 11B358EC0A19773B1455CF62 /* LockoutManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3576F224007FD4154EBE8 /* LockoutManager.swift */; }; 11B358EC68642C5A57ACB803 /* IMultiSwapProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357975CCFB31CCEF29F97 /* IMultiSwapProvider.swift */; }; - 11B358F04D8F15D43CB2BAA6 /* MarketCategoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DB992C240A4CF24938A /* MarketCategoryView.swift */; }; 11B358F1E7C72C1F42EC456F /* CoinToggleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350CDE31673BA1673B620 /* CoinToggleViewModel.swift */; }; 11B358F2CD17616038016E59 /* NftRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B354B32BD428041237570A /* NftRecord.swift */; }; - 11B358F511E01944DA31FF7D /* MarketListMarketPairDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3557E5ACDC89EF79C8C0C /* MarketListMarketPairDecorator.swift */; }; - 11B358F9D6842ECD84E80752 /* MarketCategoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352AC4F5BE70D055293D7 /* MarketCategoryViewModel.swift */; }; 11B3590189E28D408E207E19 /* CexDepositService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356B9F833E1AEE0D6D589 /* CexDepositService.swift */; }; 11B35902128F12FB06B0CA5E /* BaseUnlockViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351F8A0A9EB045377C152 /* BaseUnlockViewModel.swift */; }; 11B359029FFF4106B703694C /* CexDepositNetworkSelectModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BBC5BBCC258824A80F3 /* CexDepositNetworkSelectModule.swift */; }; @@ -877,7 +826,6 @@ 11B35920CB7EA5E3322F6D7F /* InputStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353BA87FDCB1BCBA92E61 /* InputStackView.swift */; }; 11B359264A7E2CFD0925A778 /* CoinMajorHoldersService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C6498078B1AFF406256 /* CoinMajorHoldersService.swift */; }; 11B35927C89712EF8ED36981 /* InputRowModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B7E25636164A4B65CEC /* InputRowModifier.swift */; }; - 11B35929AD3C9F27463392C6 /* TopPlatformViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BB370AE2C896BB9F877 /* TopPlatformViewController.swift */; }; 11B3592EC9C229FD531ED6B5 /* BalanceButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3585EF1DA625D906AF9B5 /* BalanceButtonsView.swift */; }; 11B3593134900A8FC7C075B6 /* NftService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355129D9F61172FCAB8C0 /* NftService.swift */; }; 11B359330CB6A60E5960CEC2 /* CoinMarketsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BFB11BC4B9FF7D53B8D /* CoinMarketsView.swift */; }; @@ -888,8 +836,7 @@ 11B3594BEB2E05413B0DB30F /* MultiSwapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35FC4CD29F471F497819B /* MultiSwapView.swift */; }; 11B3594DD9B54E11190B4CD5 /* PoolProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359636E1AA1BC72CF7B11 /* PoolProvider.swift */; }; 11B3594FCD35038663CD4FEF /* ReceiveViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CFED85A9315089223E3 /* ReceiveViewModel.swift */; }; - 11B3594FF624369050E43F8F /* SendDataNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35AD24681D0A122E6A3C5 /* SendDataNew.swift */; }; - 11B359515EE181B7C3D773D3 /* MarketOverviewTopPlatformsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3582259AD3A0C55CF6D2C /* MarketOverviewTopPlatformsService.swift */; }; + 11B3594FF624369050E43F8F /* SendData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35AD24681D0A122E6A3C5 /* SendData.swift */; }; 11B35951600F986F1C424E24 /* PasscodeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B5570E7513DF2A455BB /* PasscodeManager.swift */; }; 11B359528BEC580E0BE7E7D3 /* ValueLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355166E437B7ADB8B8EBA /* ValueLevel.swift */; }; 11B35953182487E864EB4946 /* ActivateSubscriptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351E1107158B6A2BF2149 /* ActivateSubscriptionService.swift */; }; @@ -904,7 +851,6 @@ 11B35960AA711C4D5947BFE7 /* RestoreSettingsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3532946EA785A7C65D193 /* RestoreSettingsService.swift */; }; 11B35963BA1215A80E8B26D0 /* CexDepositModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35011026CE084AE40FE6F /* CexDepositModule.swift */; }; 11B35963D09A27ACE74B3A52 /* PublicKeysModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35111F25CE7D0C8E0B29B /* PublicKeysModule.swift */; }; - 11B35968A3A43727ED6FB0B7 /* FavoriteCoinRecordStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C7B8BA65E9AA3BB7AFB /* FavoriteCoinRecordStorage.swift */; }; 11B35968D5BDA7A46C900548 /* AddressInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A382720D6531AE92F72 /* AddressInputView.swift */; }; 11B3596AE38880C5899769D5 /* CoinOverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3506BFA73130CA9A1FF71 /* CoinOverviewView.swift */; }; 11B3596F09D52300F7F0067D /* NftCollectionOverviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BAABF1F6A9EFF769C47 /* NftCollectionOverviewViewController.swift */; }; @@ -917,7 +863,6 @@ 11B3597BAC71185E1A7C5AE2 /* CoinMajorHoldersModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35995DE2AAD2186441E38 /* CoinMajorHoldersModule.swift */; }; 11B3597E4CCEF19A8E4D1222 /* BottomMultiSelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351AF16B02C80A859D535 /* BottomMultiSelectorViewController.swift */; }; 11B3597E6EA5BF1AF3A58295 /* CoinMajorHoldersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35AB755E196D299B81BFB /* CoinMajorHoldersViewController.swift */; }; - 11B3597F2512CDE8172D7C81 /* SendModuleNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35683F0E48309E8298427 /* SendModuleNew.swift */; }; 11B3598300402F09E04EFCCE /* CoinAnalyticsIssuesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35466E6DC969B551B10D3 /* CoinAnalyticsIssuesView.swift */; }; 11B35983B0C3D4EFC115043A /* MarkdownImageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3563ED22080EE222848A5 /* MarkdownImageCell.swift */; }; 11B35988DD7E3E4E2EEE4444 /* SendTonFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35ECB140E2565C55165A0 /* SendTonFactory.swift */; }; @@ -926,11 +871,11 @@ 11B3598BE9C7A456A70B5DFD /* BackupVerifyWordsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3576FCFC9394BA37975FC /* BackupVerifyWordsViewModel.swift */; }; 11B3598CCF5AB4D78AD4D32B /* RestoreMnemonicViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3598A8D7D1A8D5E17BE15 /* RestoreMnemonicViewModel.swift */; }; 11B3598D4F303310AC88FE90 /* BaseCurrencySettingsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B57AFCEAA3AA071F07F /* BaseCurrencySettingsModule.swift */; }; - 11B3598EE09251933E7FFD5D /* SendViewModelNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357FD9D760D9671A3DF24 /* SendViewModelNew.swift */; }; + 11B3598EE09251933E7FFD5D /* PreSendViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357FD9D760D9671A3DF24 /* PreSendViewModel.swift */; }; 11B3599054A1F3D3F94444AA /* WalletSorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355ABE6F12B78194DDEDD /* WalletSorter.swift */; }; 11B35990CB6691F679D241C8 /* CoinMajorHoldersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359E4C84921BEAB994792 /* CoinMajorHoldersViewModel.swift */; }; 11B35991B877DCF82D6E51B5 /* BaseUniswapMultiSwapProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D67C8EEB29CB302973A /* BaseUniswapMultiSwapProvider.swift */; }; - 11B359926B194C0207B1C8E6 /* SendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353355FF7FBE72BF60981 /* SendView.swift */; }; + 11B359926B194C0207B1C8E6 /* PreSendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353355FF7FBE72BF60981 /* PreSendView.swift */; }; 11B35993ADB991F644E5EE98 /* PasscodeLockState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B6F5261FF3F9ECBC02E /* PasscodeLockState.swift */; }; 11B35995C701EA79184EC4A8 /* CoinAuditsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3566146F353C8B6C919CA /* CoinAuditsViewController.swift */; }; 11B35996BB3F179501DC0B08 /* BottomSheetTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B354C3C105D13E1B382178 /* BottomSheetTitleView.swift */; }; @@ -942,7 +887,6 @@ 11B359AA65D8E9BE25175DAF /* TermsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351DAF31FBE0834EBC066 /* TermsService.swift */; }; 11B359AA9A3F1FD68323C64E /* BlockchainSettingRecord_v_0_24.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3526A40F07F6C8E77BEF9 /* BlockchainSettingRecord_v_0_24.swift */; }; 11B359B09D7E49066368CFE0 /* WalletViewItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3570624266AA63F869105 /* WalletViewItemFactory.swift */; }; - 11B359B11871F76B25426D58 /* MarketAdvancedSearchModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DCB7125B0046592414B /* MarketAdvancedSearchModule.swift */; }; 11B359B7C572FDCA7CD68320 /* FaqService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352B4E116BEC01B972A39 /* FaqService.swift */; }; 11B359B8A2F9D6DDA45E0931 /* StatExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35ED0A8819AB7EA27D368 /* StatExtensions.swift */; }; 11B359BA446B27E6D369B35E /* RestoreModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350BD0CE4F979CA88EFF0 /* RestoreModule.swift */; }; @@ -950,16 +894,13 @@ 11B359BD68E234293DCF33CC /* AppStatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35496770FA251785E5581 /* AppStatusViewModel.swift */; }; 11B359BF322A7B912C778348 /* WatchlistWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B0879F715C0777919AA /* WatchlistWidget.swift */; }; 11B359C05619611CBCFC89AC /* EvmBlockchainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356D15E318829D9C7F5F1 /* EvmBlockchainManager.swift */; }; - 11B359C0E0A7F674061B1199 /* SendModuleNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35683F0E48309E8298427 /* SendModuleNew.swift */; }; 11B359C198AA7A141522E5E9 /* EvmAccountManagerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F980B34E005B9F02B8F /* EvmAccountManagerFactory.swift */; }; - 11B359C2651DA1F00A3C613C /* CoinRankViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359575A4E090B236E84C7 /* CoinRankViewModel.swift */; }; 11B359C5A1F97662FDD57259 /* RestoreSettingRecord_v_0_25.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DFA83DA24A00D73EA7D /* RestoreSettingRecord_v_0_25.swift */; }; 11B359C829669CC55E530476 /* ExtendedKeyModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351F1248EDA20F7141AB8 /* ExtendedKeyModule.swift */; }; 11B359CF6F464D59295B7B31 /* SelectorButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3589B8D488ED1F6912287 /* SelectorButtonStyle.swift */; }; 11B359D10C2A7258AF0F2B60 /* CexDepositNetworkSelectViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DDED1BC5B541DB6B4B3 /* CexDepositNetworkSelectViewModel.swift */; }; 11B359D912BC5502A9FA0E57 /* InputRowModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B7E25636164A4B65CEC /* InputRowModifier.swift */; }; 11B359DBC1A8C5226C025E0D /* TokenTransactionsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DB5445B83B51C69D7AE /* TokenTransactionsService.swift */; }; - 11B359DDFEAF887EEE3063A7 /* MarketAdvancedSearchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C19608F6A314CF1F0C5 /* MarketAdvancedSearchService.swift */; }; 11B359E225E30E97A6354FAC /* ListSectionInfoHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B23F86488FDB41CC862 /* ListSectionInfoHeader.swift */; }; 11B359E7632FC042278ED912 /* ContactBookManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A6DE18A1E6E837DFB21 /* ContactBookManager.swift */; }; 11B359EA8B77C68A8D9BA4CA /* BottomGradientWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3557DF76CFEBE7DA50D81 /* BottomGradientWrapper.swift */; }; @@ -970,7 +911,6 @@ 11B359F4651EA254E5B0AD00 /* ManageAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A38C734DF3157C84678 /* ManageAccountViewController.swift */; }; 11B359F73F1D626BF832977F /* BackupModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358967A086CFE9DBB152B /* BackupModule.swift */; }; 11B359F812683F62595AFEE2 /* DateFormatterCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356300F9A6A12C29450E7 /* DateFormatterCache.swift */; }; - 11B359F926F72DF79E9245E3 /* CoinPriceListMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35ADBD038830223A8375D /* CoinPriceListMode.swift */; }; 11B359FB94269D9076C396D4 /* NftAssetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350E1584E954D281FA87D /* NftAssetView.swift */; }; 11B359FBC96E5ED356519001 /* WalletTokenListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35136653741E9703E61DE /* WalletTokenListViewModel.swift */; }; 11B35A07ED63F869C0203244 /* CexDepositNetworkSelectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C227EDC2D4ED188A0FC /* CexDepositNetworkSelectViewController.swift */; }; @@ -996,10 +936,9 @@ 11B35A3C2C4CC7A834D56517 /* MultiSwapPreSwapStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358DD4F0C5E4E5A4EEBA4 /* MultiSwapPreSwapStep.swift */; }; 11B35A3FD624D80C6D98A1CF /* TransactionSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D6BAEC40B3DA7A1FE95 /* TransactionSource.swift */; }; 11B35A40665567C6CE6EEBCC /* LanguageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3508A186E4CC100967FD3 /* LanguageSettingsView.swift */; }; - 11B35A426FD3D729DEB89DEA /* MarketTopViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3505AD2C1640DEAD8CFFC /* MarketTopViewController.swift */; }; 11B35A42BF19B93C6005FBD9 /* AddTokenService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356FFA77A8F6918B13FCA /* AddTokenService.swift */; }; 11B35A42D28B8BC4CDA57D8E /* AccountRecord_v_0_19.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350F6C5F6ABC288511AF0 /* AccountRecord_v_0_19.swift */; }; - 11B35A4428B94FF4E4F01AA9 /* SendDataNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35AD24681D0A122E6A3C5 /* SendDataNew.swift */; }; + 11B35A4428B94FF4E4F01AA9 /* SendData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35AD24681D0A122E6A3C5 /* SendData.swift */; }; 11B35A48CF68A2A45E1A429E /* PageDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351FDDBEF227E161F6A0E /* PageDescription.swift */; }; 11B35A4CBD60780E0870E77C /* NftAssetBriefMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359E32AEEE37347E255C4 /* NftAssetBriefMetadata.swift */; }; 11B35A4D9BD4B8C29FBAFACF /* AboutModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353E80D544DAF20B12B56 /* AboutModule.swift */; }; @@ -1009,7 +948,6 @@ 11B35A4F54E1310A7963593F /* PrivateKeysModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D55BE7717A87DA6FC43 /* PrivateKeysModule.swift */; }; 11B35A51BDF5BA34DA7B227E /* CexDepositNetworkSelectModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BBC5BBCC258824A80F3 /* CexDepositNetworkSelectModule.swift */; }; 11B35A52C122A756C0A43604 /* PrimaryButtonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B354E55E901615862E7CD4 /* PrimaryButtonCell.swift */; }; - 11B35A5A820C1BCC1A92E944 /* MarketTopViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3505AD2C1640DEAD8CFFC /* MarketTopViewController.swift */; }; 11B35A5B22BD628721B47E09 /* ScanQrBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351454D8FE8FEDA2C1EC9 /* ScanQrBlurView.swift */; }; 11B35A5B8DC265D419E69B05 /* CexAssetResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357C67623035CDF98B540 /* CexAssetResponse.swift */; }; 11B35A5C28DF128893703B6C /* RestoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3518EEF5AFC1C55FD07BA /* RestoreViewModel.swift */; }; @@ -1017,7 +955,6 @@ 11B35A5CD6B04D269E281A6A /* SyncerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B354BDDCAF7AF5A0582CAA /* SyncerState.swift */; }; 11B35A5DA8197D193B7CF8D9 /* AmountData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B617A9CE668EEF4978B /* AmountData.swift */; }; 11B35A66D997F86423C2F5A0 /* DonutChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35FC207C703EBF63FD56A /* DonutChartView.swift */; }; - 11B35A6F4E6931973277940A /* CoinPriceListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355D1DB2F95F1183FF2F8 /* CoinPriceListView.swift */; }; 11B35A71D59757280B126587 /* ReceiveDerivationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357EEC98939F9C7AA3271 /* ReceiveDerivationViewModel.swift */; }; 11B35A79476177D3BE7DE477 /* RestorePrivateKeyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351EBA5DE11150CE2E3F9 /* RestorePrivateKeyService.swift */; }; 11B35A7DBF1AFB1FDDB01CE0 /* SelfSizedSectionsTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3538D10D6256399A51F61 /* SelfSizedSectionsTableView.swift */; }; @@ -1029,8 +966,6 @@ 11B35A82220538FEE57546FB /* TransactionTypeFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3592E4DA65E72C0BC6BEB /* TransactionTypeFilter.swift */; }; 11B35A82532EC55909EFBAD8 /* LaunchScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3595BAA550B6BEC8C3F72 /* LaunchScreen.swift */; }; 11B35A8395C75C6FA6515F3C /* CexWithdrawViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35ABC3E6C990E3BFA0A7B /* CexWithdrawViewController.swift */; }; - 11B35A89B3F0D81C42DC5C40 /* CoinRankHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351664970D7EA1F7B50C7 /* CoinRankHeaderView.swift */; }; - 11B35A8BB87C68ACF4594C99 /* MarketTopModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F9DA79410E7B9C1B0F8 /* MarketTopModule.swift */; }; 11B35A8D8CA77B42650F4C03 /* Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351958604DF0B0AB3346C /* Publisher.swift */; }; 11B35A90F19DE20ABE21F423 /* AddTokenService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356FFA77A8F6918B13FCA /* AddTokenService.swift */; }; 11B35A91D104DA35C08032B2 /* AccountType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35542A7D7FE1BDC2E73E2 /* AccountType.swift */; }; @@ -1071,8 +1006,6 @@ 11B35AF420F4D90CD6C9E6E3 /* OneInchMultiSwapProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EF841FD4B3D13058F76 /* OneInchMultiSwapProvider.swift */; }; 11B35AF710C287EB89018342 /* UIWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353356496AA219686B993 /* UIWindow.swift */; }; 11B35AF765474AEE15CA4240 /* AccountRecord_v_0_20.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3506CB3D780A00F4BBBBE /* AccountRecord_v_0_20.swift */; }; - 11B35B02E53E6D7DDC12FC5D /* MarketOverviewTopPairsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352BDC42A2F717AFAE7BD /* MarketOverviewTopPairsService.swift */; }; - 11B35B03C6F88AFE3E45C13D /* SendAmountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352436A876FC59DF41C78 /* SendAmountView.swift */; }; 11B35B077A52041C9939C6E8 /* FaqCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3528090862B6792A76DA4 /* FaqCell.swift */; }; 11B35B086B0D62A9D7A10CD0 /* AddTokenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355E8892971578502EF33 /* AddTokenViewModel.swift */; }; 11B35B09AADB1FBF7DDE765C /* TransactionFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3567314F1A1DF8D1B2910 /* TransactionFilterView.swift */; }; @@ -1099,7 +1032,6 @@ 11B35B3821C6CCB647B98F9A /* MarkdownService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35801399EA004F5A2A1F7 /* MarkdownService.swift */; }; 11B35B3C7A60FEE011EFBF73 /* InputSecondaryCircleButtonWrapperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3573AF91C82342639A9B1 /* InputSecondaryCircleButtonWrapperView.swift */; }; 11B35B3F384758B223A7218C /* MainSettingsFooterCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BD9A836C953CCF8D077 /* MainSettingsFooterCell.swift */; }; - 11B35B3F5DE4F73225EBFF36 /* CoinRankViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359575A4E090B236E84C7 /* CoinRankViewModel.swift */; }; 11B35B40461A5D9841EEE4EF /* StatRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B9F4421EE65B8B09370 /* StatRecord.swift */; }; 11B35B501A30615698B04C96 /* AddEvmTokenBlockchainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352ABFDEAEEA84D3FDD8B /* AddEvmTokenBlockchainService.swift */; }; 11B35B507F2F843A5B3E4C7C /* EvmNetworkViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351895EE2816DE7BBC767 /* EvmNetworkViewModel.swift */; }; @@ -1107,7 +1039,6 @@ 11B35B5B8F3FEED445647E56 /* EvmCoinServiceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35410733A35D1558E55B2 /* EvmCoinServiceFactory.swift */; }; 11B35B5EED35DD5F8F8B19A8 /* ThemeListStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356EF92FFD23F4385A991 /* ThemeListStyle.swift */; }; 11B35B5FA3177BC9ED21B929 /* SwapApproveConfirmationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356671FA76C7DEDA50B94 /* SwapApproveConfirmationModule.swift */; }; - 11B35B6586B14C6A9F35E39D /* MarketAdvancedSearchResultService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358C7505D0DE60CD03B22 /* MarketAdvancedSearchResultService.swift */; }; 11B35B664765E4EAA7E0E14F /* Faq.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35450456BE5E3EE8F7391 /* Faq.swift */; }; 11B35B6C15E23CB81DFF5B9E /* FormCautionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350F5D363E9B1D6C9120F /* FormCautionCell.swift */; }; 11B35B6C74E672B2699E8207 /* NftModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356E0F2BC23304E545B13 /* NftModule.swift */; }; @@ -1118,7 +1049,6 @@ 11B35B81B3C6EDC01D0B3C1F /* EvmNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352EB0986D26399B7F89B /* EvmNetworkService.swift */; }; 11B35B925CE6EB25DF542611 /* CoinInvestorsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EC8B737D03ECC70CA80 /* CoinInvestorsService.swift */; }; 11B35B9351ADD99FA8919EEE /* FaqViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C09B59EF5DEB6D7EB07 /* FaqViewModel.swift */; }; - 11B35B970E8949F968960796 /* MarketListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3596381A93F3A3D2575D6 /* MarketListViewController.swift */; }; 11B35B99BA314135CB139D02 /* WCSignEthereumTransactionRequestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BEA44ADC0D844330FB7 /* WCSignEthereumTransactionRequestViewController.swift */; }; 11B35B99C84075296D6F26DE /* UnlinkViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35419B0C846238DDC50F3 /* UnlinkViewModel.swift */; }; 11B35B9BCE8AF0B54BFBED42 /* BalanceButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3585EF1DA625D906AF9B5 /* BalanceButtonsView.swift */; }; @@ -1138,7 +1068,6 @@ 11B35BC7BBEFE0AB8D8FE405 /* CexCoinSelectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352F071CE0EF1505A8380 /* CexCoinSelectViewController.swift */; }; 11B35BCC6C00E857CE562F16 /* EvmAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352D314A298B6B832F309 /* EvmAdapter.swift */; }; 11B35BCD6D0462E31D7EBA06 /* BackupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355E86612AEE00ED19CFE /* BackupManager.swift */; }; - 11B35BCF9FFC93255EFB2774 /* CoinRankHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351664970D7EA1F7B50C7 /* CoinRankHeaderView.swift */; }; 11B35BD102629037A4348B3C /* WCSignEthereumTransactionRequestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BEA44ADC0D844330FB7 /* WCSignEthereumTransactionRequestViewController.swift */; }; 11B35BD170E89520DF7D384E /* BaseUniswapMultiSwapProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D67C8EEB29CB302973A /* BaseUniswapMultiSwapProvider.swift */; }; 11B35BDBD856C645045553D2 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3572B7C2F16CD51F37FF0 /* UIImage.swift */; }; @@ -1171,7 +1100,6 @@ 11B35C2D31DE8081AC572B27 /* BaseUniswapV2MultiSwapProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358A102692DA01F91413D /* BaseUniswapV2MultiSwapProvider.swift */; }; 11B35C2D75133B8F13101A24 /* BalancePrimaryValueManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3566DC3A97A5CC3E2C729 /* BalancePrimaryValueManager.swift */; }; 11B35C2EB8D6EC593915640F /* CexDepositModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35011026CE084AE40FE6F /* CexDepositModule.swift */; }; - 11B35C2ED09C6D5660BB1236 /* MarketWatchlistToggleService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3562819DF141457837340 /* MarketWatchlistToggleService.swift */; }; 11B35C2F43F8135F22DD7FDF /* ActivateSubscriptionModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35997A9E413878F48313B /* ActivateSubscriptionModule.swift */; }; 11B35C2FBF875F81E13CC575 /* CoinService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3550B863970039577CD00 /* CoinService.swift */; }; 11B35C3418D6E7D94CB5C2AF /* RecoveryPhraseService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351A0B1AE5F612E6A5FEE /* RecoveryPhraseService.swift */; }; @@ -1184,21 +1112,16 @@ 11B35C47A06C0A4F7231C511 /* NftCollectionAssetsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35100DD6E2DBF905FD19B /* NftCollectionAssetsModule.swift */; }; 11B35C4875DAA9A92BD9B7EC /* CoinMajorHoldersService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C6498078B1AFF406256 /* CoinMajorHoldersService.swift */; }; 11B35C4A0250F05179488A91 /* CexWithdrawNetworkRaw.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EC03BB5316524050518 /* CexWithdrawNetworkRaw.swift */; }; - 11B35C4A68DD7F7AF6DC4F27 /* TopPlatformViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3527F1528AA697AAA6E61 /* TopPlatformViewModel.swift */; }; 11B35C4D4120D85CD32CAD0F /* TransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350CCAA0C9F2F5279F680 /* TransactionsViewController.swift */; }; 11B35C4E01C2AF2707691B96 /* CurrencyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BE8A0802ADE2FAB0012DE7F /* CurrencyManager.swift */; }; 11B35C5388370450DAF65C5B /* EvmUpdateStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E5C80435645132BCDD2 /* EvmUpdateStatus.swift */; }; 11B35C59583AFCDFD828B9D1 /* TextFieldStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35921FBDF6F9BBAA88803 /* TextFieldStackView.swift */; }; - 11B35C5E7A90AA7B302EB0CD /* MarketListMarketFieldDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353B060BDF272932D3522 /* MarketListMarketFieldDecorator.swift */; }; 11B35C5F856FB531028F8C0A /* CreateDuressPasscodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3501625BDD3F7D9BEA2F5 /* CreateDuressPasscodeViewModel.swift */; }; 11B35C60FE9B94994FCCB0CB /* TonTransactionRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A67733FE53B31E5EB3A /* TonTransactionRecord.swift */; }; 11B35C68D57727AB0DAC7753 /* NftAddressMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BEEB24CDB82D3F4E7C0 /* NftAddressMetadata.swift */; }; - 11B35C72EF5FB79182EAB119 /* MarketTopPairsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359715B07FD5316D72A07 /* MarketTopPairsViewModel.swift */; }; 11B35C7425B861D2F32384E8 /* ListSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350C0CB7083E2738D356C /* ListSectionHeader.swift */; }; 11B35C78C16D1F89FBC8F222 /* ReceiveViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359D1A38D53951CEE6F84 /* ReceiveViewController.swift */; }; 11B35C7BB534F9FFE5B6B3A6 /* TokenType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3519AE11DC423E0D078E2 /* TokenType.swift */; }; - 11B35C81D9DFBF07955D2461 /* CoinRankModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358A22655004017228F65 /* CoinRankModule.swift */; }; - 11B35C83BC2D0EA7FF5B4832 /* MarketCategoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3584888F2DB8CCFAA90DF /* MarketCategoryViewController.swift */; }; 11B35C8621E221DA1F157A5B /* AccountFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C4D6F474C2EB3687EB4 /* AccountFactory.swift */; }; 11B35C88ACACE26AC40F35BA /* PrimaryButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35968D12AAAC828AFE955 /* PrimaryButtonStyle.swift */; }; 11B35C8A1082D0A8F0B354B1 /* RestoreMnemonicHintCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358604E6B530C5DB22B92 /* RestoreMnemonicHintCell.swift */; }; @@ -1206,16 +1129,13 @@ 11B35C8A95E4C8D43F9279B6 /* EvmLabelStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F007444A766AF8CD20D /* EvmLabelStorage.swift */; }; 11B35C8BF55C38F198C3DAE6 /* BadgeViewNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352BD333C9D69ECB82884 /* BadgeViewNew.swift */; }; 11B35C8D53F838E7E5CA6EEC /* NftStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3502AEB7EF95A590A7B1B /* NftStorage.swift */; }; - 11B35C8E09922F59B200E347 /* MarketListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3596381A93F3A3D2575D6 /* MarketListViewController.swift */; }; 11B35C906C5278060BD5A04C /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351E61AB3FB570A4F7C66 /* Wallet.swift */; }; 11B35C943710774694282388 /* IMultiSwapQuote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350F532661482B6170F92 /* IMultiSwapQuote.swift */; }; 11B35C9570D3C283E9C943D5 /* CreatePasscodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3590ACA8DFA4196E8EC33 /* CreatePasscodeViewModel.swift */; }; 11B35C95EA77972246D5F3BD /* CexAssetRecordStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35AD211091A7C8619CEA2 /* CexAssetRecordStorage.swift */; }; 11B35CA25E02E397E167EEC3 /* QrCodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356F4578E266268264021 /* QrCodeCell.swift */; }; - 11B35CA3524A5FEA07135535 /* MultiSwapConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353F8063B95C6571AA517 /* MultiSwapConfirmationView.swift */; }; 11B35CA4F61231F536BE9A24 /* PancakeV2MultiSwapProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3564500A3378D6EB2A477 /* PancakeV2MultiSwapProvider.swift */; }; 11B35CA6259EDA3708695416 /* FaqUrlHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35759E226171A4969E66E /* FaqUrlHelper.swift */; }; - 11B35CA725B9BAD70E40197F /* MarketTopPairsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355C615D9FE4290671D5D /* MarketTopPairsModule.swift */; }; 11B35CA92AA402BE72B4F5D6 /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352648C452D611F1EDF61 /* Image.swift */; }; 11B35CAD54059FA55DF81972 /* PasscodeLockManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3505A43D9C2787B3BD153 /* PasscodeLockManager.swift */; }; 11B35CAD5A7E0C8709559FD2 /* WalletManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352D547F1BB38D2AD6AD5 /* WalletManager.swift */; }; @@ -1232,12 +1152,9 @@ 11B35CC202B611609C8445A4 /* BaseUniswapV3MultiSwapProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351FAE6B01F29FC37B3C2 /* BaseUniswapV3MultiSwapProvider.swift */; }; 11B35CC30F65AC6966F66BA4 /* MarkdownViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359B9C1E0BB4D32599695 /* MarkdownViewModel.swift */; }; 11B35CC39C37FD31AA98A105 /* CoinMajorHoldersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35AB755E196D299B81BFB /* CoinMajorHoldersViewController.swift */; }; - 11B35CC691C53A0B870BB910 /* MarketFilteredListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35ADF9BC4D149F86F23E4 /* MarketFilteredListService.swift */; }; 11B35CC9464E639F3A086B29 /* MarkdownHeader1Cell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D31D3EC415789CFA160 /* MarkdownHeader1Cell.swift */; }; 11B35CCAC0A3C35C1B9BD918 /* NftActivityViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DE76BBABD8F0914A0D2 /* NftActivityViewModel.swift */; }; - 11B35CCC81AE83E1CBB61504 /* MarketMultiSortHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350FAB6F1A6E1FCFACB2F /* MarketMultiSortHeaderViewModel.swift */; }; 11B35CCC9D6B7596502B4381 /* CoinInvestment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35264F7CE80AD5D9A540A /* CoinInvestment.swift */; }; - 11B35CD199C820EC89FBB546 /* MarketListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355BEB95969D89B3F8876 /* MarketListViewModel.swift */; }; 11B35CD742378BF04CDB73F0 /* NSAttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350BD364F07D1AC759865 /* NSAttributedString.swift */; }; 11B35CDC811A92DFAAF2C923 /* ListSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350C0CB7083E2738D356C /* ListSectionHeader.swift */; }; 11B35CE67B7F5C5A5244C951 /* MainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B6D6FCA3745DE0750BD /* MainService.swift */; }; @@ -1248,8 +1165,6 @@ 11B35D0542ACB56A31C61143 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3572B7C2F16CD51F37FF0 /* UIImage.swift */; }; 11B35D05FF90E999110698C0 /* RecipientAddressInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DBDADDA8D4F9D88C7AA /* RecipientAddressInputCell.swift */; }; 11B35D06AFC1BAC63F25D271 /* CoinAnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35996D668B9ADC60E6B9B /* CoinAnalyticsService.swift */; }; - 11B35D06C75228488867EB22 /* MarketOverviewTopPlatformsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3582259AD3A0C55CF6D2C /* MarketOverviewTopPlatformsService.swift */; }; - 11B35D06FF4A25AF74553C36 /* MarketOverviewTopPairsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35AB1D0CE5D8ECE7DDF65 /* MarketOverviewTopPairsViewModel.swift */; }; 11B35D098F3E0435A3C4512B /* MultiSwapButtonState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35140CD5BF8B1C26A6278 /* MultiSwapButtonState.swift */; }; 11B35D09F6CBE8A070C415F7 /* WalletTokenListViewItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C6E5282F55B88042F8D /* WalletTokenListViewItemFactory.swift */; }; 11B35D106185086B3BEC5119 /* Blockchain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3595BAB29E195A1317DD1 /* Blockchain.swift */; }; @@ -1257,21 +1172,17 @@ 11B35D10BB1FC795924ED987 /* BalanceHiddenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352FBA1B29357E0120055 /* BalanceHiddenManager.swift */; }; 11B35D11809FB5D34D8D6D3F /* BarPageControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3512EF5B66B852F5E05FB /* BarPageControl.swift */; }; 11B35D11D00301F7B67B0340 /* AppIconManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A309C359456D7DF1A03 /* AppIconManager.swift */; }; - 11B35D16F2F7B8E11A771B18 /* FavoriteCoinRecord_v_0_22.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E255F6CA21FFA9E6B42 /* FavoriteCoinRecord_v_0_22.swift */; }; + 11B35D16F2F7B8E11A771B18 /* FavoriteCoinRecord_v_0_38.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E255F6CA21FFA9E6B42 /* FavoriteCoinRecord_v_0_38.swift */; }; 11B35D19D00091E903B04472 /* MarkdownViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DF08505C3A7CB1BBBB4 /* MarkdownViewController.swift */; }; - 11B35D1F6602B517D19D5C76 /* TopPlatformViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BB370AE2C896BB9F877 /* TopPlatformViewController.swift */; }; 11B35D24064D9CE75ACCD59A /* CoinAnalyticsIssuesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35466E6DC969B551B10D3 /* CoinAnalyticsIssuesView.swift */; }; 11B35D24B98C73BD43CCD80B /* NftAssetRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359852B313E849499BC19 /* NftAssetRecord.swift */; }; 11B35D270BF38B1478789B5E /* MarkdownService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35801399EA004F5A2A1F7 /* MarkdownService.swift */; }; - 11B35D2B9A488D3382D8693D /* MarketHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3543F4D196A47EFE3E6F7 /* MarketHeaderCell.swift */; }; 11B35D2C16299881FE8BD910 /* MultiSwapQuotesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35062C72B1D98A2A4EDA9 /* MultiSwapQuotesView.swift */; }; 11B35D2C28E3116F58A543E2 /* BtcBlockchainSettingsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35469BE6FC454CD6D15B5 /* BtcBlockchainSettingsService.swift */; }; 11B35D3102B803096B6EE5B6 /* NftMetadataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359FE71F5DE6AAD2BA3D8 /* NftMetadataManager.swift */; }; 11B35D39722C76466D6397F3 /* CexCoinSelectViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B2E5AB093ABD8228769 /* CexCoinSelectViewModel.swift */; }; - 11B35D3E3B10A4E92FD01172 /* MarketAdvancedSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A12A3B7218DF597C172 /* MarketAdvancedSearchViewController.swift */; }; 11B35D40702B98DD14429562 /* FormAmountInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352A41EC99ADCC8F3E3E9 /* FormAmountInputView.swift */; }; 11B35D45BA3D8B9C5E2AE4ED /* ManageWalletsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3529B6C7C426755CE9E14 /* ManageWalletsService.swift */; }; - 11B35D46B65772A1CC17B099 /* MarketGlobalTvlMetricViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA8E1106E31D68FD9181D /* MarketGlobalTvlMetricViewController.swift */; }; 11B35D4B4DE7C4620C34AA11 /* AddEvmSyncSourceModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35504934CE3C31D523F82 /* AddEvmSyncSourceModule.swift */; }; 11B35D4CAE7D1CD1C169EDD4 /* TextDropDownAndSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359C62F476065C11EE049 /* TextDropDownAndSettingsView.swift */; }; 11B35D4CF0FBE2496CED70E4 /* EditPasscodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CF718BD36A9F07BC293 /* EditPasscodeViewModel.swift */; }; @@ -1301,12 +1212,9 @@ 11B35D835774B7F4E286BF32 /* AlertRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3516415E7A3217BBB1681 /* AlertRouter.swift */; }; 11B35D88633A14FD13E91702 /* TonAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F9B75F6663FAFCA3177 /* TonAdapter.swift */; }; 11B35D8E976E984302A81F8B /* ReceiveTokenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E1B9C559545FC3E6226 /* ReceiveTokenViewModel.swift */; }; - 11B35D90644F7735556DB3D5 /* TopPlatformViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3527F1528AA697AAA6E61 /* TopPlatformViewModel.swift */; }; 11B35D93B238BA992173E123 /* StackViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35AAE4114A56DF13ECF0F /* StackViewCell.swift */; }; 11B35D94B4E92789F681E293 /* Coin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F3FBFB1F4BE93B796DF /* Coin.swift */; }; 11B35D95692647EB9F73D9DB /* CoinProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3534997B5CD413DBDB7C7 /* CoinProvider.swift */; }; - 11B35D96A814579F37BAD3D0 /* CoinRankService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3513AC6560B9C37C342F3 /* CoinRankService.swift */; }; - 11B35DA145308F888592A7CF /* CoinPriceListMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35ADBD038830223A8375D /* CoinPriceListMode.swift */; }; 11B35DA1A83CE7E402309FE7 /* CreateAccountModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358145A0D9F93ACBC0301 /* CreateAccountModule.swift */; }; 11B35DA4CB435537AD4148D7 /* NftCollectionAssetsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3573B3FE1FD8B476375E6 /* NftCollectionAssetsViewModel.swift */; }; 11B35DA5492B0C4EC7130A19 /* AppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3586FDC91E3742847B7E0 /* AppConfig.swift */; }; @@ -1315,7 +1223,7 @@ 11B35DAC8D4C5DA2EC6D6E74 /* PasteInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3575530EE722514F89A61 /* PasteInputView.swift */; }; 11B35DB339D0E7CF183760F4 /* EnabledWalletCacheStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35025FD5E96FD1AB359E9 /* EnabledWalletCacheStorage.swift */; }; 11B35DB3DC5CAC18264E82D7 /* Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351958604DF0B0AB3346C /* Publisher.swift */; }; - 11B35DB4C3EAB5B4B9ABF904 /* SendConfirmField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359C9E2EFD391FB848618 /* SendConfirmField.swift */; }; + 11B35DB4C3EAB5B4B9ABF904 /* SendField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359C9E2EFD391FB848618 /* SendField.swift */; }; 11B35DBC25EF36ABA6E13857 /* SectionsTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35332D245CFF50A68F8CA /* SectionsTableView.swift */; }; 11B35DBD3329CD28158E4D6A /* NftHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357B1854BAD99C7CFB3DE /* NftHeaderViewModel.swift */; }; 11B35DC140F10F631B61581E /* SendEvmConfirmationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3538387B200C894A68ADF /* SendEvmConfirmationModule.swift */; }; @@ -1328,7 +1236,6 @@ 11B35DD9C17FDD3ED40BA321 /* CoinInvestorsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EA2F51B9257D036D3E6 /* CoinInvestorsModule.swift */; }; 11B35DDB41FFB254E91B6019 /* MarkdownTextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3536DB4D3D3D7771B3EA4 /* MarkdownTextCell.swift */; }; 11B35DDBD7EC98FAE5794F76 /* SecondaryButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3572105A456CCDD63E94D /* SecondaryButtonStyle.swift */; }; - 11B35DDC98FFF447333278FF /* MarketAdvancedSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A12A3B7218DF597C172 /* MarketAdvancedSearchViewController.swift */; }; 11B35DDD77B56489D1EB72C5 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3569F2E6BD5E9CBCFCA1F /* Token.swift */; }; 11B35DDE363387B6E7A1D3B9 /* TabButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351DBFA79DAF0A82A1925 /* TabButtonStyle.swift */; }; 11B35DDFAF0532881A4F68B0 /* AdditionalDataCellNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E67C1B1AB7A13074894 /* AdditionalDataCellNew.swift */; }; @@ -1338,7 +1245,6 @@ 11B35DF1D8B5125CF13A1812 /* RestoreMnemonicHintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CB288AF5A54B99A51E4 /* RestoreMnemonicHintView.swift */; }; 11B35DF3813AEB74E254A05A /* NftAssetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350E1584E954D281FA87D /* NftAssetView.swift */; }; 11B35DF625EA2A1412C2D984 /* DuressModeIntroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35420841B4F9B886A6507 /* DuressModeIntroView.swift */; }; - 11B35DFCEC1D363B160479EE /* MarketTopService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35770F0C72E1CD3F99985 /* MarketTopService.swift */; }; 11B35DFF8F15AA74356061A0 /* ReceiveSelectCoinService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35615F3ECB5D6E467B49A /* ReceiveSelectCoinService.swift */; }; 11B35DFFC539A1E72382C8F7 /* ManageAccountsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350911E00460DA8925165 /* ManageAccountsService.swift */; }; 11B35DFFD52E10918F760DD5 /* InputSecondaryButtonWrapperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D5C4EEEAABF83A67D95 /* InputSecondaryButtonWrapperView.swift */; }; @@ -1358,11 +1264,9 @@ 11B35E13FC5117E8F1829DCB /* RecipientAddressCautionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358B22BAF021E8FA028BF /* RecipientAddressCautionCell.swift */; }; 11B35E163CB471C55269E7EB /* QrCodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356F4578E266268264021 /* QrCodeCell.swift */; }; 11B35E165D6681B849F9A934 /* TextFieldStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35921FBDF6F9BBAA88803 /* TextFieldStackView.swift */; }; - 11B35E1853C328487870BACA /* SendAmountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EAECC2236EB081241B4 /* SendAmountViewModel.swift */; }; 11B35E1A30BE0E0432B4A064 /* AmountInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F2BE131B969BBEABDB9 /* AmountInputViewModel.swift */; }; 11B35E24B2F98C74E95DA3BE /* WalletAdapterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D1E91C730437BA69676 /* WalletAdapterService.swift */; }; 11B35E24FD61B2799C191811 /* MarkdownTextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3536DB4D3D3D7771B3EA4 /* MarkdownTextCell.swift */; }; - 11B35E255AF804AEE43FF46A /* MarketAdvancedSearchResultService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358C7505D0DE60CD03B22 /* MarketAdvancedSearchResultService.swift */; }; 11B35E276D1C91193B687718 /* WalletService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A5B004015DEA52AD5C9 /* WalletService.swift */; }; 11B35E2C07D04A795AD96220 /* ReceiveBitcoinCashCoinTypeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A81AD46F48B63E59ED3 /* ReceiveBitcoinCashCoinTypeViewModel.swift */; }; 11B35E2DA9242EAAAA9D99FC /* HsLabelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EDE31BA3EF80F78859A /* HsLabelProvider.swift */; }; @@ -1382,7 +1286,6 @@ 11B35E4FE3117F6B681F6748 /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351E61AB3FB570A4F7C66 /* Wallet.swift */; }; 11B35E553176CF41B5CB83C0 /* BlockchainTokensView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F60AFA103D0CD2369C3 /* BlockchainTokensView.swift */; }; 11B35E57C6406D2249A23E6F /* SendEvmTransactionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C7F043B6C41E53D43BC /* SendEvmTransactionService.swift */; }; - 11B35E584C30C56AE18DE076 /* TopPlatformHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357E05A8AF5608ECF5D5F /* TopPlatformHeaderCell.swift */; }; 11B35E5DDFA437BD43717962 /* WalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35269B569B8588DB9A23C /* WalletViewController.swift */; }; 11B35E5EFE34BE1A3760F81D /* BiometryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A6223272C5B3E261A24 /* BiometryManager.swift */; }; 11B35E5F3C6070DF6E1F6BAD /* BlockchainType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357B185E8FECB3924FDF2 /* BlockchainType.swift */; }; @@ -1429,34 +1332,28 @@ 11B35EBC6D5608F23DF8581E /* EvmAccountRestoreStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350A3E8F85D6FA2E17173 /* EvmAccountRestoreStateManager.swift */; }; 11B35EBDDAB95E919600AE72 /* NftActivityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B354FFF7ED1253E3BD804A /* NftActivityViewController.swift */; }; 11B35EBE688A7C4F9B92F865 /* ReceiveModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CF031BC81E4D401CA01 /* ReceiveModule.swift */; }; - 11B35EC2E4E5614FF64C7246 /* MarketMultiSortHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3503B9A985B4835FDB03D /* MarketMultiSortHeaderView.swift */; }; 11B35EC3B9E9C778183E1136 /* EvmNftAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C2397749C5654830540 /* EvmNftAdapter.swift */; }; 11B35ECCE5D888A506D7144A /* RestoreBinanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B354D96A80987DAB3B64A6 /* RestoreBinanceViewController.swift */; }; 11B35ED22837284580055F0A /* BalanceData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BDEB703708795B71C4E /* BalanceData.swift */; }; - 11B35ED2B4180D1FB4073100 /* SendAmountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EAECC2236EB081241B4 /* SendAmountViewModel.swift */; }; 11B35ED4BE062F70D930F92C /* MultiSwapSettingStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350910284BA2BF694FA17 /* MultiSwapSettingStorage.swift */; }; 11B35ED81BCE008EE5A71DE8 /* LockManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F57D462E2C9E9AEF67C /* LockManager.swift */; }; 11B35ED89BE760771022E8A8 /* BlockchainTokensService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35219C4AB26DC0D104E30 /* BlockchainTokensService.swift */; }; 11B35ED9D5F95988E9335440 /* CoinAnalyticsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3545402F742FE641B9B6C /* CoinAnalyticsModule.swift */; }; 11B35ED9F3C0EA3CCC4C0FF4 /* SyncErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3516207E568E7D54428CA /* SyncErrorView.swift */; }; - 11B35EDC3703B04ED8B72BA8 /* CoinTreasuriesModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F08C14B3F0D978E2E7F /* CoinTreasuriesModule.swift */; }; 11B35EE0660C0CE24235E4DF /* NftDatabaseStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A47AB0887477EDB200C /* NftDatabaseStorage.swift */; }; 11B35EE45B00510714693AA9 /* SyncerStateStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35FA71AA140CD3764C6BC /* SyncerStateStorage.swift */; }; 11B35EE923002E9FCC413422 /* ReceiveAddressViewItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353E0AC6E1DE4F81BDEF5 /* ReceiveAddressViewItemFactory.swift */; }; 11B35EECB57465598F305C5B /* ListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351B200B193534D0A66BF /* ListRow.swift */; }; 11B35EEE72F3A78415A2A552 /* HorizontalDivider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D0EBAF33901578520E1 /* HorizontalDivider.swift */; }; 11B35EF433960198D484E5E8 /* ReceiveTokenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E1B9C559545FC3E6226 /* ReceiveTokenViewModel.swift */; }; - 11B35EF70120304F6D4F5561 /* MarketMultiSortHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3503B9A985B4835FDB03D /* MarketMultiSortHeaderView.swift */; }; 11B35EF9D9E8C1A814005CFD /* MainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35747FAD8381F2AD48276 /* MainViewModel.swift */; }; 11B35EFAFFA1E30F7765FEB2 /* MainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B6D6FCA3745DE0750BD /* MainService.swift */; }; - 11B35EFEEB7F361BD5FCCC3D /* SendConfirmationNewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B91B5EAF2E193FDC04E /* SendConfirmationNewView.swift */; }; + 11B35EFEEB7F361BD5FCCC3D /* SendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B91B5EAF2E193FDC04E /* SendView.swift */; }; 11B35F09A49A90BF058CA500 /* ReceiveSelectCoinViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350575488360C1A598DF3 /* ReceiveSelectCoinViewModel.swift */; }; 11B35F0D313E455BCF24C42B /* PriceChangeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357889F003A0B33D9DF27 /* PriceChangeType.swift */; }; 11B35F0FF89A5B636C2EFE48 /* UniswapV2MultiSwapProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350E65AAA33943BAD2F1D /* UniswapV2MultiSwapProvider.swift */; }; - 11B35F1152FB1004E554B922 /* MarketOverviewMetricsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DCCC2D8CD00EF6A9A77 /* MarketOverviewMetricsCell.swift */; }; 11B35F134E5EF8572BF330CB /* NavigationRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3578FB80AA013BD351A26 /* NavigationRow.swift */; }; 11B35F1440C5946E9C3D94ED /* Auditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D8AF9D337A98530548D /* Auditor.swift */; }; - 11B35F173689829256427A34 /* MarketAdvancedSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356BEB2B4DFC3E9C950C5 /* MarketAdvancedSearchViewModel.swift */; }; 11B35F18FEEEAA9EC6043CA6 /* BaseCurrencySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D203692A7F12D67242D /* BaseCurrencySettingsView.swift */; }; 11B35F1949F7203F34347550 /* ModuleUnlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35FF02BBEDAEF446D0610 /* ModuleUnlockView.swift */; }; 11B35F20127C070137781ED5 /* AddTokenModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355267E1A6678B7B5FCF1 /* AddTokenModule.swift */; }; @@ -1464,12 +1361,10 @@ 11B35F2474FF811217F48132 /* WalletHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E859456CF982321B46F /* WalletHeaderView.swift */; }; 11B35F25D1209C6DB33ADA55 /* AdapterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3583932F270503C1DF3F0 /* AdapterFactory.swift */; }; 11B35F27274120E53E2C1ADE /* Faq.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35450456BE5E3EE8F7391 /* Faq.swift */; }; - 11B35F28C21E228AB3158716 /* MarketOverviewMetricsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DCCC2D8CD00EF6A9A77 /* MarketOverviewMetricsCell.swift */; }; 11B35F29DCAF273D1092C0A4 /* PasscodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359FC4FE023FBA0E1726C /* PasscodeView.swift */; }; 11B35F2F1770FB757E6FDCD8 /* NftRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B354B32BD428041237570A /* NftRecord.swift */; }; 11B35F3409AEFC534DC52137 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352978EC570F59F442BD5 /* View.swift */; }; 11B35F3525372880BC7B47DB /* EvmCoinServiceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35410733A35D1558E55B2 /* EvmCoinServiceFactory.swift */; }; - 11B35F3C9850408B9ADE0B16 /* DropdownSortHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F9BA41AC15436A4B977 /* DropdownSortHeaderView.swift */; }; 11B35F3DB270A794ADF675FB /* NoPasscodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356DAA35F1C6CCD7F25B5 /* NoPasscodeViewController.swift */; }; 11B35F3F123BFF155DA7F417 /* CoinAnalyticsHoldersCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359CE35C7483CCB956D13 /* CoinAnalyticsHoldersCell.swift */; }; 11B35F4294930802EC8C9CEA /* ReceiveAddressModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BB3B8928864A742C83E /* ReceiveAddressModule.swift */; }; @@ -1500,17 +1395,14 @@ 11B35F8649859802080BA580 /* NftViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D96B8963CDC30DC5643 /* NftViewModel.swift */; }; 11B35F890BE41B1BFAC3D75E /* RestoreMnemonicService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D55DCC92BED4FA87CA0 /* RestoreMnemonicService.swift */; }; 11B35F8BF4BD6481E6AF72AF /* TestNetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EE072CE5471B0DFF841 /* TestNetManager.swift */; }; - 11B35F8FB24AB02560A1D018 /* MarketAdvancedSearchResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353A1CC274EDBF8A67DEA /* MarketAdvancedSearchResultViewController.swift */; }; 11B35F906F9708CFC86E53FB /* NoPasscodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356DAA35F1C6CCD7F25B5 /* NoPasscodeViewController.swift */; }; 11B35F91AFFFD151AD80A625 /* ScanQrAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3560B257473CA93386D70 /* ScanQrAlertView.swift */; }; 11B35F91E53BA1F835DD4B4F /* HorizontalDivider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D0EBAF33901578520E1 /* HorizontalDivider.swift */; }; 11B35F98393E6F3B76381ECF /* ModuleUnlockViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B51E484CA62EC57790E /* ModuleUnlockViewModel.swift */; }; - 11B35F9CC94DB2BC7B43BB59 /* CoinRankViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A1E2AE3DC240D5B785E /* CoinRankViewController.swift */; }; 11B35F9E1AF528B31C6F383C /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352E52084020190C21D8C /* InputView.swift */; }; 11B35F9F489F4B358FCCE893 /* MarkdownParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3543968337A40168D3EB0 /* MarkdownParser.swift */; }; 11B35FA1970606C12E57C2EA /* ChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35AFE2C95FF73F75652D8 /* ChartView.swift */; }; - 11B35FA298822DABDB1CD109 /* SendViewModelNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357FD9D760D9671A3DF24 /* SendViewModelNew.swift */; }; - 11B35FA3A00690573A482BAC /* CoinRankViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A1E2AE3DC240D5B785E /* CoinRankViewController.swift */; }; + 11B35FA298822DABDB1CD109 /* PreSendViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357FD9D760D9671A3DF24 /* PreSendViewModel.swift */; }; 11B35FA6F9EE876BD65E9AD6 /* LaunchScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3595BAA550B6BEC8C3F72 /* LaunchScreen.swift */; }; 11B35FA70EB07440E1576A56 /* RowButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BAA4EA85B4A3A173498 /* RowButtonStyle.swift */; }; 11B35FAB3263E489CB9017FC /* AddTokenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356D5A5F32E88FEC7629D /* AddTokenViewController.swift */; }; @@ -1534,78 +1426,51 @@ 11B35FE0809AC8A716C41427 /* PrimaryButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35968D12AAAC828AFE955 /* PrimaryButtonStyle.swift */; }; 11B35FE603192CF3195115D0 /* LanguageSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35920D0AD623C5E4A4460 /* LanguageSettingsViewModel.swift */; }; 11B35FE8D60BFF31C3104484 /* SwitchAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350BC3E707879846AC0AA /* SwitchAccountViewModel.swift */; }; - 11B35FEB268E7C6B085B56C9 /* MarketOverviewTopPairsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35AB1D0CE5D8ECE7DDF65 /* MarketOverviewTopPairsViewModel.swift */; }; 11B35FEC7AA2A8887FCF0AE6 /* StatManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359980AA45D6B44151D7A /* StatManager.swift */; }; 11B35FF02BADC6D832836C44 /* EvmPrivateKeyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353262E45560C91FD6B65 /* EvmPrivateKeyViewModel.swift */; }; 11B35FF072E32C5D4028D0F6 /* TextInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35968F5DE9FDA6EC26FCD /* TextInputCell.swift */; }; 11B35FF10EF7FC47869EF295 /* CexWithdrawViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B2465CB748311AF03D5 /* CexWithdrawViewModel.swift */; }; - 11B35FF14C4B775E570375EC /* MarketTopPairsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359715B07FD5316D72A07 /* MarketTopPairsViewModel.swift */; }; 11B35FF1D956D812F815FA86 /* NftImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3522A9A7774977CF39A1D /* NftImageView.swift */; }; 11B35FF65FCE441A69822E1C /* InputPrefixWrapperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35743158763BD8E336770 /* InputPrefixWrapperView.swift */; }; 11B35FF681C01782693B3C4A /* SendEvmConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B354950B1534AD045FDA3A /* SendEvmConfirmationViewController.swift */; }; - 11B35FF6D36153F372C16C32 /* MarketWatchlistViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3566FE007887C3528583C /* MarketWatchlistViewModel.swift */; }; 11B35FF84A61FFBEC01CE15E /* BlockchainTokensViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350B29B000CD809F81228 /* BlockchainTokensViewModel.swift */; }; 11B35FFC8C3E4CF638397650 /* UnlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35D36E5D47264AE07D729 /* UnlockView.swift */; }; 11B35FFD159D864F6D914F08 /* AppearanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357511F8F17D8221B64E2 /* AppearanceView.swift */; }; 11B35FFE6FBDA949184E2BF2 /* AmountInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F2BE131B969BBEABDB9 /* AmountInputViewModel.swift */; }; - 179E746F1E3D7BC613BD0AFC /* FavoriteCoinRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179E7048A730489634E27043 /* FavoriteCoinRecord.swift */; }; - 179E7EBD494842280D9F19A4 /* FavoriteCoinRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179E7048A730489634E27043 /* FavoriteCoinRecord.swift */; }; 1A564001701A1E77AF7A651B /* BalanceErrorModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564AB0B646F7A92DD188F2 /* BalanceErrorModule.swift */; }; - 1A5640051485E3419FE674F1 /* MarketTopPlatformsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564995DE20E52E8E0F1E6A /* MarketTopPlatformsViewModel.swift */; }; - 1A56402D725D6AB0C4149066 /* MarketTopPlatformsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564995DE20E52E8E0F1E6A /* MarketTopPlatformsViewModel.swift */; }; 1A564032D3C011BCAA7D44A8 /* DeepLinkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564DEB9782FF55EFFD8CCA /* DeepLinkService.swift */; }; 1A56404471A7270782D6619F /* AppVersion_v_0_20.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5646C5FEE5A60A658B0180 /* AppVersion_v_0_20.swift */; }; - 1A564047000F9270ABC4AEC1 /* MarketTopPlatformsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564C4DB4A57CCF2C5EFB78 /* MarketTopPlatformsViewController.swift */; }; - 1A56405220ED225C0B973A7F /* MarketOverviewTopCoinsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564D661F3AE561D7FE9FAA /* MarketOverviewTopCoinsService.swift */; }; - 1A56405536E22BFF69EE9593 /* MarketOverviewTopCoinsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5640B4F6298D9F326C5EDE /* MarketOverviewTopCoinsViewModel.swift */; }; 1A564058DB366C810AC3C47A /* TitledHighlightedDescriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564CB28708314AE0A69424 /* TitledHighlightedDescriptionView.swift */; }; - 1A56405B48462C0750A47ECA /* MarketOverviewTopCoinsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5646218714BA81DE9B5631 /* MarketOverviewTopCoinsDataSource.swift */; }; 1A56405DB1540DEC70FD5CFA /* HighlightedDescriptionBaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564BCC9DD29DB5455669A5 /* HighlightedDescriptionBaseView.swift */; }; 1A56408B1A402BC99846F141 /* ZcashAddressParserItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5642E923141415CCFC9167 /* ZcashAddressParserItem.swift */; }; - 1A5640D097E24A155C1F2E56 /* MarketOverviewCategoryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5645B1C5FD344967B1F4B7 /* MarketOverviewCategoryCell.swift */; }; 1A5640D6FDF86EDB54213F9B /* DeepLinkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564580B3F739DAC59C623F /* DeepLinkManager.swift */; }; 1A5640DE72AC306799695F48 /* TopPlatformMarketCapFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564A1B86DF22E86F0BB442 /* TopPlatformMarketCapFetcher.swift */; }; - 1A56412970FD129426474522 /* MarketOverviewTopPlatformsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564B44985D1169593F202C /* MarketOverviewTopPlatformsDataSource.swift */; }; 1A56413DFF5B9A8E3F2A5EA6 /* ReleaseNotesMarkdownConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56432704A2E7A9BE78497B /* ReleaseNotesMarkdownConfig.swift */; }; 1A56415A4BB89B9156C6442D /* Decimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564D5E55767404ED6C88E0 /* Decimal.swift */; }; 1A564168590F031AA453E1D1 /* BalanceErrorModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564AB0B646F7A92DD188F2 /* BalanceErrorModule.swift */; }; 1A56416B9DC6DA281AD34575 /* AdapterState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5647ACB8A65C250F62E07D /* AdapterState.swift */; }; - 1A564176DEB9ED375113DA3B /* MarketTopPlatformsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56459D85D4859D8A0F4D5A /* MarketTopPlatformsModule.swift */; }; - 1A56417AE8B0F2513FC009A9 /* MarketListTopPlatformDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564C5CC7EC339C3113869D /* MarketListTopPlatformDecorator.swift */; }; - 1A56418183EC6CB602873B51 /* MarketListTopPlatformDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564C5CC7EC339C3113869D /* MarketListTopPlatformDecorator.swift */; }; 1A5641C097AAD5FC9D35F9EE /* TransactionDataSortMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564E6A6B7E18C287A1D77D /* TransactionDataSortMode.swift */; }; 1A5641F1A0F4E8FC6555D480 /* BtcBlockchainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5646B6231F2C52F27526F7 /* BtcBlockchainManager.swift */; }; - 1A56422231460374E3830E56 /* MarketListWatchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564555A67E4DC1DC935A04 /* MarketListWatchViewModel.swift */; }; - 1A5642348A701CF7CF5CD805 /* MarketOverviewGlobalDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5647FA18CC69113ECB6581 /* MarketOverviewGlobalDataSource.swift */; }; - 1A564240E0BC3E93EDB3BA22 /* MarketNftTopCollectionsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564D12426BCA027C67377E /* MarketNftTopCollectionsService.swift */; }; 1A56424B5327D4ADFD728B0B /* BlockchainSettingRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564C46FB773A67E29D9D32 /* BlockchainSettingRecord.swift */; }; 1A56427D35F4EDC473A732E7 /* ReadMoreTextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56485B094980B68B0A86AE /* ReadMoreTextCell.swift */; }; - 1A56427F500823630D07E75D /* MarketNftTopCollectionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5649E41FE690AF0A712426 /* MarketNftTopCollectionsViewModel.swift */; }; 1A5642F3BA1892109A596B61 /* MarkdownContentProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56417C27A95B429D9F2912 /* MarkdownContentProvider.swift */; }; 1A5642F3C5C414F65A3CC59D /* TitledHighlightedDescriptionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56444EB2F32DB662981653 /* TitledHighlightedDescriptionCell.swift */; }; - 1A564335057D41EECDC8021B /* MarketOverviewTopCoinsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564D661F3AE561D7FE9FAA /* MarketOverviewTopCoinsService.swift */; }; 1A5643651979E1907BE1B12C /* BlockchainSettingRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564C46FB773A67E29D9D32 /* BlockchainSettingRecord.swift */; }; 1A5643813B0713460096F6D1 /* AppVersionRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56446CCB15D32581396A59 /* AppVersionRecord.swift */; }; 1A5643A263F288A4E83409FA /* BalanceErrorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564702FB246F315983743E /* BalanceErrorViewModel.swift */; }; - 1A5643B0422BC87461CC25C5 /* MarketOverviewTopPlatformsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564B44985D1169593F202C /* MarketOverviewTopPlatformsDataSource.swift */; }; 1A5643BCA38A75A63D57F1AB /* SendEthereumErrorCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564920282E84A6E7EE05EB /* SendEthereumErrorCell.swift */; }; 1A5643BCDAD7CB0230CBB513 /* GradientClippingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564C4B0D13BC7214419A3E /* GradientClippingView.swift */; }; 1A5643C5A8BBFD6F2DC8C734 /* BtcRestoreMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564BE241BFC5DD59D0FB7C /* BtcRestoreMode.swift */; }; 1A5643C73BEDA01E4E378BEF /* ZcashAddressParserItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5642E923141415CCFC9167 /* ZcashAddressParserItem.swift */; }; - 1A5643CB57594E84707686A3 /* MarketOverviewGlobalDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5647FA18CC69113ECB6581 /* MarketOverviewGlobalDataSource.swift */; }; 1A5643CFF5657C9C6B7DBA19 /* ThemeSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564EA6F1CCDF88F78351F8 /* ThemeSearchViewController.swift */; }; 1A5643D323BAB0B8B45A9038 /* PrivacyPolicyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564CE10FD5FEC14EF38BD8 /* PrivacyPolicyViewController.swift */; }; 1A5643D7C72CDCB87E3D5F17 /* SendTransactionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56422C196B48931CDE1445 /* SendTransactionError.swift */; }; - 1A5643E2C1AB8F24605342A0 /* MarketListNftCollectionDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564CC5878BF33B8CE1F339 /* MarketListNftCollectionDecorator.swift */; }; 1A564406716193E4A2D9075F /* ReachabilityViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564814721244F4D4D87557 /* ReachabilityViewModel.swift */; }; 1A56440AD78BD32AEAE2A2EA /* PlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5644A21F9FEC4E2A7B0860 /* PlaceholderView.swift */; }; 1A56441AD76BA80CB74902CB /* TraitCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5648911028181BB1462CFE /* TraitCell.swift */; }; 1A5644271F4CD209136352EE /* FilterHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564B1C051AF2C87C670563 /* FilterHeaderView.swift */; }; - 1A56443EA3671FA2A40F1F7E /* TopPlatformModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564BDA5600859626D99BB4 /* TopPlatformModule.swift */; }; 1A56444313F5FB0DE9B06BE4 /* PrivacyPolicyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564CE10FD5FEC14EF38BD8 /* PrivacyPolicyViewController.swift */; }; - 1A56444C8342498C892E931E /* MarketNftTopCollectionsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564E5282C3C22DA85141AF /* MarketNftTopCollectionsModule.swift */; }; 1A5644CF2BEC2E7C6227BDC7 /* AppStatusModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56404C1C16B85434117DB7 /* AppStatusModule.swift */; }; - 1A5644FA9A9599F94EE16916 /* MarketNftTopCollectionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5649E41FE690AF0A712426 /* MarketNftTopCollectionsViewModel.swift */; }; 1A5644FE2885F71299CC66EA /* PlaceholderViewModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564CF35E7A07E96B704ADA /* PlaceholderViewModule.swift */; }; 1A564504E164177DD6EECFBA /* BalanceErrorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5646B5D68A302515565030 /* BalanceErrorService.swift */; }; 1A564512989CEBC48ABCB94E /* ConvertedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564A6A5C4F3080690AE93F /* ConvertedError.swift */; }; @@ -1614,15 +1479,11 @@ 1A5645335BEED7A26D53A6B9 /* AddressUri.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5649C6FFC694CD18A8B39A /* AddressUri.swift */; }; 1A56454D8C03B7B1141635DA /* EnabledWallet_v_0_13.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564A55E5866D6081EA6F69 /* EnabledWallet_v_0_13.swift */; }; 1A564564359185321F81541D /* TitledHighlightedDescriptionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56444EB2F32DB662981653 /* TitledHighlightedDescriptionCell.swift */; }; - 1A564586DCCE6E87784B9E6E /* MarketListWatchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564555A67E4DC1DC935A04 /* MarketListWatchViewModel.swift */; }; - 1A5645A90CB5AAEF02745AC7 /* MarketNftTopCollectionsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564E5282C3C22DA85141AF /* MarketNftTopCollectionsModule.swift */; }; 1A5645BF7E9F7DF6B0C8912C /* PlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5644A21F9FEC4E2A7B0860 /* PlaceholderView.swift */; }; 1A5645C7334EED7FF57FFD47 /* DashAddressParserItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564EA8CD67010BFFC57AAB /* DashAddressParserItem.swift */; }; - 1A5645CA87E32639CEE6681F /* MarketOverviewGlobalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5649C48B3AABC56D2512ED /* MarketOverviewGlobalViewModel.swift */; }; 1A5645D6AC7D344E6D3D0CAF /* AppVersion_v_0_20.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5646C5FEE5A60A658B0180 /* AppVersion_v_0_20.swift */; }; 1A5645DA5F609D469D12A6E1 /* AppVersionRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56446CCB15D32581396A59 /* AppVersionRecord.swift */; }; 1A5645DE1329BC81C1A9B711 /* BtcBlockchainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5646B6231F2C52F27526F7 /* BtcBlockchainManager.swift */; }; - 1A5646322B606C56DFFA324A /* NftCollectionsMultiSortHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5643A672A508BC4CBCABDD /* NftCollectionsMultiSortHeaderViewModel.swift */; }; 1A56463AB918839385E8CDBD /* BlockchainSettingsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56443BF752CB6537E45F5A /* BlockchainSettingsStorage.swift */; }; 1A56463FADAB4646BA106A5D /* TopPlatformMarketCapFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564A1B86DF22E86F0BB442 /* TopPlatformMarketCapFetcher.swift */; }; 1A56464440899E3299F79D32 /* JailbreakService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56469A2F3EAAEDECFB4034 /* JailbreakService.swift */; }; @@ -1631,22 +1492,15 @@ 1A564665243CEEBF646D1328 /* ConvertedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564A6A5C4F3080690AE93F /* ConvertedError.swift */; }; 1A564699CFB7CCE8BD3E5245 /* AppVersionStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564AFF2709E27114985A8D /* AppVersionStorage.swift */; }; 1A5646C3220E1735309D2927 /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564B7C35C5B235D2BBAC2C /* AppVersion.swift */; }; - 1A5646E52D8DFADAA5ACFCAD /* TopPlatformsMultiSortHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564D8F8A8A63BC9BEAAD56 /* TopPlatformsMultiSortHeaderViewModel.swift */; }; 1A5646E67996AB355694A35E /* EnabledWallet_v_0_13.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564A55E5866D6081EA6F69 /* EnabledWallet_v_0_13.swift */; }; 1A5646F57C3677EBC462673C /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56489DE231CDDCA75CAEB3 /* AppError.swift */; }; 1A5647072B937BE4B69FFA1D /* SendEthereumErrorCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564920282E84A6E7EE05EB /* SendEthereumErrorCell.swift */; }; - 1A56470F4D18CCC43B85DEB3 /* MarketOverviewCategoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564ADD13E597F423249CA3 /* MarketOverviewCategoryViewModel.swift */; }; 1A56475828E9121D78E02D67 /* BitcoinCashAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564BF724C69C237E309C07 /* BitcoinCashAdapter.swift */; }; 1A564776B26828610A295FDF /* DashAddressParserItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564EA8CD67010BFFC57AAB /* DashAddressParserItem.swift */; }; - 1A5647BB75003E8292C8144B /* BaseMarketOverviewTopListDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5644E4694DBB0E6E0B10CC /* BaseMarketOverviewTopListDataSource.swift */; }; 1A5647EF2A5B8141F0BE4320 /* UIDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56447C12D91108517ED217 /* UIDevice.swift */; }; 1A56481FC0EAFA9FC32E1E21 /* ScanQrViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564A144576DB93334E1682 /* ScanQrViewController.swift */; }; 1A56483DCD5E1942AFAC0A1C /* ReachabilityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564D8AD6D160F27C021F48 /* ReachabilityService.swift */; }; - 1A5648701B55E09FD8E4585D /* MarketOverviewTopPlatformsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56477F6FC71270AD53A3AE /* MarketOverviewTopPlatformsViewModel.swift */; }; - 1A5648AB801E8DAA9B3D288E /* MarketOverviewGlobalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564EDF0FD6A1D1575D1EFB /* MarketOverviewGlobalService.swift */; }; 1A5648ACB0A6B11E0E39A1B0 /* AppVersionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5641CDB00EF52E18BF70F3 /* AppVersionManager.swift */; }; - 1A5648ACF2A5B2F7420DA5F6 /* MarketTopPlatformsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56459D85D4859D8A0F4D5A /* MarketTopPlatformsModule.swift */; }; - 1A5648B10B34B402167EEA84 /* TopPlatformService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5649C0BD100768C726B4FB /* TopPlatformService.swift */; }; 1A5648B6D95710939690F22A /* PerformanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5641E505FE004F601943C4 /* PerformanceTableViewCell.swift */; }; 1A5648C0787EBA8CDCB6F761 /* ReachabilityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564D8AD6D160F27C021F48 /* ReachabilityService.swift */; }; 1A5648D075682B17EFE9CBB6 /* AddressUri.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5649C6FFC694CD18A8B39A /* AddressUri.swift */; }; @@ -1658,19 +1512,14 @@ 1A564977D009E610D8D194AE /* TitledHighlightedDescriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564CB28708314AE0A69424 /* TitledHighlightedDescriptionView.swift */; }; 1A56498C73C42F16A2D96A8D /* HighlightedDescriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564730E8F235240D62124B /* HighlightedDescriptionView.swift */; }; 1A5649926B0064083045BEEB /* BalanceErrorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5646B5D68A302515565030 /* BalanceErrorService.swift */; }; - 1A56499414D2E3BBFF260D14 /* MarketNftTopCollectionsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564D12426BCA027C67377E /* MarketNftTopCollectionsService.swift */; }; - 1A5649ADA72E39CE24BB64FC /* MarketOverviewCategoryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564A6D161EAD22626332C1 /* MarketOverviewCategoryDataSource.swift */; }; 1A5649CEE6EFCC06A10F69CF /* TraitCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5648911028181BB1462CFE /* TraitCell.swift */; }; 1A5649F72A0116C66DCBA153 /* AppVersionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5641CDB00EF52E18BF70F3 /* AppVersionManager.swift */; }; 1A564A1CA02CF6F6DC37B50A /* AcademyMarkdownConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564215DD6F0D54C1F6C4F7 /* AcademyMarkdownConfig.swift */; }; 1A564A24327DB692A068D909 /* FilterHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564293D88587642800717B /* FilterHeaderCell.swift */; }; 1A564A2B23326B3057C37962 /* AdapterError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564504718E7D19F379A9F7 /* AdapterError.swift */; }; 1A564A2FCE3C764029FECB7B /* GradientClippingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564C4B0D13BC7214419A3E /* GradientClippingView.swift */; }; - 1A564A45605FA993F9680646 /* MarketOverviewTopCoinsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5640B4F6298D9F326C5EDE /* MarketOverviewTopCoinsViewModel.swift */; }; - 1A564A4CF522A7A959482AA6 /* MarketOverviewGlobalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564EDF0FD6A1D1575D1EFB /* MarketOverviewGlobalService.swift */; }; 1A564A7CB794B4367A87C5E2 /* FilterHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564293D88587642800717B /* FilterHeaderCell.swift */; }; 1A564A905233BD9AB9241916 /* ButtonState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5642E9B9E5592E04373C16 /* ButtonState.swift */; }; - 1A564A9E6FF0EB1C6F2AA102 /* MarketTopPlatformsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564C4DB4A57CCF2C5EFB78 /* MarketTopPlatformsViewController.swift */; }; 1A564AB50152BF82514BAC3C /* HighlightedDescriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564730E8F235240D62124B /* HighlightedDescriptionView.swift */; }; 1A564AB8471E098F68FDB9D7 /* BinanceAddressParserItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564872B7C5F76D8CE55A8B /* BinanceAddressParserItem.swift */; }; 1A564ABF3417C13718D78F57 /* ThemeSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564EA6F1CCDF88F78351F8 /* ThemeSearchViewController.swift */; }; @@ -1682,49 +1531,32 @@ 1A564BA5366D4F0B921F5EE8 /* ThemeActionSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564E86CCEAD0F9956664D4 /* ThemeActionSheetController.swift */; }; 1A564BAB51E12A8F37D870B5 /* PerformanceSideCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564B73ABBEFC58A13D501E /* PerformanceSideCollectionViewCell.swift */; }; 1A564BB34D0EAA2E8BE8B498 /* DeepLinkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564580B3F739DAC59C623F /* DeepLinkManager.swift */; }; - 1A564BC0945C7CA8330A604E /* MarketNftTopCollectionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5641A724199908970CFB54 /* MarketNftTopCollectionsViewController.swift */; }; - 1A564BC7CE38935CD443C235 /* MarketOverviewTopCoinsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5646218714BA81DE9B5631 /* MarketOverviewTopCoinsDataSource.swift */; }; 1A564BC84E6A4F836994ABD3 /* AdapterError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564504718E7D19F379A9F7 /* AdapterError.swift */; }; 1A564C10B9782D53375736C8 /* ReleaseNotesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5644074A3ACB1DFB63FF92 /* ReleaseNotesService.swift */; }; 1A564C11B7785DCEA2472065 /* BitcoinCashAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564BF724C69C237E309C07 /* BitcoinCashAdapter.swift */; }; - 1A564C13834A2EE853542795 /* MarketListNftCollectionDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564CC5878BF33B8CE1F339 /* MarketListNftCollectionDecorator.swift */; }; - 1A564C1395A1264F1A9B3AB5 /* TopPlatformModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564BDA5600859626D99BB4 /* TopPlatformModule.swift */; }; - 1A564C2E1B66C2C508F8327D /* MarketOverviewTopPlatformsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56477F6FC71270AD53A3AE /* MarketOverviewTopPlatformsViewModel.swift */; }; - 1A564C6CCA15813506F20561 /* MarketOverviewCategoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564ADD13E597F423249CA3 /* MarketOverviewCategoryViewModel.swift */; }; 1A564C756284485E93C0F78D /* AdapterState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5647ACB8A65C250F62E07D /* AdapterState.swift */; }; 1A564CA2A36B55AC41BBA0F4 /* SendTransactionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56422C196B48931CDE1445 /* SendTransactionError.swift */; }; 1A564CBB4708B7DAC6D6A2AD /* BlockchainSettingsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56443BF752CB6537E45F5A /* BlockchainSettingsStorage.swift */; }; - 1A564CC4790F0CED826C131F /* MarketOverviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564206FEC56546760B9BEA /* MarketOverviewViewModel.swift */; }; 1A564CFD8F22A2F5FDB346EA /* JailbreakService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56469A2F3EAAEDECFB4034 /* JailbreakService.swift */; }; 1A564D02348C91416FD011FC /* TraitsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564FF31C5E879781A2D5E0 /* TraitsCell.swift */; }; 1A564D209D3AFA40F808C8FB /* PerformanceContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5646483957D74946973BEE /* PerformanceContentCollectionViewCell.swift */; }; 1A564D2917CF5C38C84C031B /* BitcoinBaseAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56450DA6DF97C9E1FFE987 /* BitcoinBaseAdapter.swift */; }; 1A564D3DB55C8CB8B5AED664 /* BalanceErrorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564702FB246F315983743E /* BalanceErrorViewModel.swift */; }; - 1A564D4581F280245579C9DF /* BaseMarketOverviewTopListDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5644E4694DBB0E6E0B10CC /* BaseMarketOverviewTopListDataSource.swift */; }; 1A564D53F049C4784CAC814D /* BtcRestoreMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564BE241BFC5DD59D0FB7C /* BtcRestoreMode.swift */; }; 1A564D69A570D801AF59FF58 /* ReadMoreTextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56485B094980B68B0A86AE /* ReadMoreTextCell.swift */; }; 1A564D81725888B31D56F389 /* PerformanceContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5646483957D74946973BEE /* PerformanceContentCollectionViewCell.swift */; }; 1A564D86E3D7E200D9B81592 /* AppVersionStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564AFF2709E27114985A8D /* AppVersionStorage.swift */; }; 1A564DBDAA43925DC8E39164 /* TraitsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564FF31C5E879781A2D5E0 /* TraitsCell.swift */; }; 1A564DC4E55C1BE1C4D4CDA9 /* HighlightedDescriptionBaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564BCC9DD29DB5455669A5 /* HighlightedDescriptionBaseView.swift */; }; - 1A564DCC65AA9EADE56F2B7F /* MarketNftTopCollectionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5641A724199908970CFB54 /* MarketNftTopCollectionsViewController.swift */; }; - 1A564DFD9A3B16E7DA518F67 /* NftCollectionsMultiSortHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5643A672A508BC4CBCABDD /* NftCollectionsMultiSortHeaderViewModel.swift */; }; - 1A564E08B4F6C5B1CDB121F6 /* MarketTopPlatformsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5648E7534C2E7F16C4A2D4 /* MarketTopPlatformsService.swift */; }; 1A564E09FB049006167E033B /* PerformanceSideCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564B73ABBEFC58A13D501E /* PerformanceSideCollectionViewCell.swift */; }; 1A564E0B7DB0060B9600FCB1 /* BalanceErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564CE56CEC73B78C9DB6B5 /* BalanceErrorViewController.swift */; }; - 1A564E1912184BFC886548D9 /* MarketOverviewCategoryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564A6D161EAD22626332C1 /* MarketOverviewCategoryDataSource.swift */; }; - 1A564E2897197EB14584A62E /* MarketOverviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564206FEC56546760B9BEA /* MarketOverviewViewModel.swift */; }; 1A564E69BA99DF8CD4562902 /* PlaceholderViewModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564CF35E7A07E96B704ADA /* PlaceholderViewModule.swift */; }; 1A564E8CA0CFBE8B1E232B60 /* PerformanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5641E505FE004F601943C4 /* PerformanceTableViewCell.swift */; }; - 1A564EBD7F2BB47C7C209EC5 /* TopPlatformsMultiSortHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564D8F8A8A63BC9BEAAD56 /* TopPlatformsMultiSortHeaderViewModel.swift */; }; 1A564EDBD3E4B37299E199B7 /* AppStatusModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56404C1C16B85434117DB7 /* AppStatusModule.swift */; }; 1A564EDED3DA0C27A223C54A /* BasePerformanceCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564D7B1F36B1C4AB4CBF3A /* BasePerformanceCollectionViewCell.swift */; }; 1A564EE727CD892E7F2E5715 /* ScanQrViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564A144576DB93334E1682 /* ScanQrViewController.swift */; }; - 1A564EF252E8C535BEB0548B /* MarketTopPlatformsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5648E7534C2E7F16C4A2D4 /* MarketTopPlatformsService.swift */; }; 1A564F382602A283C370E7FB /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56489DE231CDDCA75CAEB3 /* AppError.swift */; }; - 1A564F3C50FC28F2AF4AF4ED /* MarketOverviewGlobalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5649C48B3AABC56D2512ED /* MarketOverviewGlobalViewModel.swift */; }; 1A564F73B7FE144D39DEA34F /* UIDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A56447C12D91108517ED217 /* UIDevice.swift */; }; - 1A564FC84916FF6D6224FB33 /* MarketOverviewCategoryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5645B1C5FD344967B1F4B7 /* MarketOverviewCategoryCell.swift */; }; 1A564FEF3AEC87712186DD28 /* DeepLinkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564DEB9782FF55EFFD8CCA /* DeepLinkService.swift */; }; 1A564FFA4566A5019E744E8E /* TransactionDataSortMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564E6A6B7E18C287A1D77D /* TransactionDataSortMode.swift */; }; 2FA5D01A5570C6DE5D07E2C4 /* EvmFeeViewItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DCD7797C13645F3D5F8A /* EvmFeeViewItemFactory.swift */; }; @@ -1869,20 +1701,6 @@ 2FA5DFD5AFFA720FA28CCD6A /* TransactionInfoViewItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D299198FEF01FB5D06DE /* TransactionInfoViewItemFactory.swift */; }; 3A73FC69258B1AD100FE4D34 /* MarketModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA3173853A16B2433AEC0 /* MarketModule.swift */; }; 3A73FC6A258B1AD200FE4D34 /* MarketModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA3173853A16B2433AEC0 /* MarketModule.swift */; }; - 3A73FC74258B1ADA00FE4D34 /* MarketViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA8FDCCC09B609C7D0FEA /* MarketViewController.swift */; }; - 3A73FC75258B1ADB00FE4D34 /* MarketViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA8FDCCC09B609C7D0FEA /* MarketViewController.swift */; }; - 3A73FC78258B1AE500FE4D34 /* MarketViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAA9DFF1F23B0B8A8CEAD /* MarketViewModel.swift */; }; - 3A73FC7A258B1AE500FE4D34 /* MarketViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAA9DFF1F23B0B8A8CEAD /* MarketViewModel.swift */; }; - 3A73FC99258B1AF600FE4D34 /* MarketWatchlistViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAA1B62A6A1A278BE06AA /* MarketWatchlistViewController.swift */; }; - 3A73FC9B258B1AF700FE4D34 /* MarketWatchlistModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA7D27615D192FBC5486E /* MarketWatchlistModule.swift */; }; - 3A73FC9C258B1AF700FE4D34 /* MarketWatchlistViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAA1B62A6A1A278BE06AA /* MarketWatchlistViewController.swift */; }; - 3A73FC9E258B1AF700FE4D34 /* MarketWatchlistModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA7D27615D192FBC5486E /* MarketWatchlistModule.swift */; }; - 3A73FCAA258B1AFC00FE4D34 /* MarketMetricView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA50A504CFA74CA19A415 /* MarketMetricView.swift */; }; - 3A73FCAD258B1AFC00FE4D34 /* GradientPercentCircle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAA3930D3CB65CC545658 /* GradientPercentCircle.swift */; }; - 3A73FCAE258B1AFD00FE4D34 /* MarketMetricView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA50A504CFA74CA19A415 /* MarketMetricView.swift */; }; - 3A73FCB1258B1AFD00FE4D34 /* GradientPercentCircle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAA3930D3CB65CC545658 /* GradientPercentCircle.swift */; }; - 3AB682BE25BADD97002197A5 /* MarketOverviewModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB682BC25BADD97002197A5 /* MarketOverviewModule.swift */; }; - 3AB682BF25BADD97002197A5 /* MarketOverviewModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB682BC25BADD97002197A5 /* MarketOverviewModule.swift */; }; 3AF9F61A253EF555000626A8 /* ZcashTransactionPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF9F617253EF555000626A8 /* ZcashTransactionPool.swift */; }; 3AF9F61B253EF555000626A8 /* ZcashTransactionPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF9F617253EF555000626A8 /* ZcashTransactionPool.swift */; }; 3AF9F61D253EF555000626A8 /* ZcashAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF9F618253EF555000626A8 /* ZcashAdapter.swift */; }; @@ -1899,14 +1717,10 @@ 5039F973269C5A9B004711B8 /* ReleaseNotesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5039F971269C5A9A004711B8 /* ReleaseNotesViewController.swift */; }; 5046E671259C491000A941E5 /* InfoSeparatorHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA6FB0AF97333CD9D007F /* InfoSeparatorHeaderView.swift */; }; 5046E672259C491100A941E5 /* InfoSeparatorHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA6FB0AF97333CD9D007F /* InfoSeparatorHeaderView.swift */; }; - 505E7F742897C6DA00229BF2 /* TopPlatformService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5649C0BD100768C726B4FB /* TopPlatformService.swift */; }; 50701ACE25B041E600EDE51B /* JailbreakTestManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564D2011D7A4E80B2B7B92 /* JailbreakTestManager.swift */; }; 50701ACF25B041E600EDE51B /* JailbreakTestManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A564D2011D7A4E80B2B7B92 /* JailbreakTestManager.swift */; }; - 58AAA004F244AFFC352ADCEF /* MarketSingleSortHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA15F4FA7B9EC091EDFF3 /* MarketSingleSortHeaderView.swift */; }; 58AAA0444AD1508917D349CF /* UniswapSettingsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAFF6E494F623AD62AF95 /* UniswapSettingsService.swift */; }; 58AAA0642CB9B7B19C6235B5 /* AmountDecimalParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAF740AE2BEBD7CEBD563 /* AmountDecimalParser.swift */; }; - 58AAA07B54F11F0DF5E3E876 /* MarketPostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA263DAB58FD63E6A9351 /* MarketPostViewController.swift */; }; - 58AAA08E3204C7E7326E1DF9 /* MarketTvlSortHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA78BB269FEBB430092A3 /* MarketTvlSortHeaderViewModel.swift */; }; 58AAA097F8417B30693547FE /* CoinPageMarkdownParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA444C885BCC354F1B7B3 /* CoinPageMarkdownParser.swift */; }; 58AAA0BEEC3EF61DCB80C6BA /* DebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAA6EA2DAD95D2CB417FD /* DebugViewController.swift */; }; 58AAA0C0DB5C901FE58F5F9B /* FeeSliderWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAC2D03916C80C8DC5FE5 /* FeeSliderWrapper.swift */; }; @@ -1919,30 +1733,22 @@ 58AAA126020F429D94D77A76 /* OneInchSettingsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA75A0580C45CC08D89E8 /* OneInchSettingsService.swift */; }; 58AAA1283FC7F83F62FC5961 /* FeeSliderValueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA3C555FBFB5423CCF8E0 /* FeeSliderValueView.swift */; }; 58AAA13C1DC5FA79BCA1D732 /* UdnAddressParserItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA9A4761030BE9F60C85E /* UdnAddressParserItem.swift */; }; - 58AAA153DF6764D7FEA99D63 /* MarketGlobalTvlMetricService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAADBB7B760C189AD6032F /* MarketGlobalTvlMetricService.swift */; }; 58AAA15A6F863CD3940FEBCC /* CoinCardModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA8AD6794AD2AAF462B7B /* CoinCardModule.swift */; }; 58AAA1721028D6504074A158 /* CoinSelectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA9BBAB97C2D21A83956C /* CoinSelectViewController.swift */; }; 58AAA187A094370FA7CD2BDD /* InfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA83833C970A2DC467715 /* InfoViewController.swift */; }; 58AAA188EBE3E1CC463D8D9F /* SwapAllowanceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA7C8532511E4BCD9C8D9 /* SwapAllowanceCell.swift */; }; 58AAA1AAD335F236D130FCBB /* SwapConfirmationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAB39CAE1453B9ED024E4 /* SwapConfirmationModule.swift */; }; - 58AAA1B716FCD40947F4F95C /* MarketOverviewHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA2521E8F8845D96AB865 /* MarketOverviewHeaderCell.swift */; }; 58AAA1D61188628A61F1E047 /* DebugInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA16C7E337511638808E5 /* DebugInteractor.swift */; }; 58AAA1EC97682F9347B8CA32 /* ChartModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA5C1F206B755B477A30B /* ChartModule.swift */; }; 58AAA203CEB400A883379822 /* DownloadService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA2B7F82F42442ED27A67 /* DownloadService.swift */; }; 58AAA2100166FDFB110FA6D0 /* ChartConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA9D5D29115C5F435CF1B /* ChartConfiguration.swift */; }; - 58AAA2494D41B33DC091A3E6 /* MarketPostService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA657E30EBD52A5E06ACF /* MarketPostService.swift */; }; - 58AAA25AF71DF84F42A27157 /* MarketPostViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA11651E3CE29A461BF42 /* MarketPostViewModel.swift */; }; 58AAA27FE69D5AF00F771C7F /* Global.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA8B7E47FF3B010851E58 /* Global.swift */; }; 58AAA289FEAE983739189B8D /* RecipientAddressViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA19E66FCA0575AE33FAA /* RecipientAddressViewModel.swift */; }; - 58AAA2960B54658E2614D72E /* MarketGlobalMetricService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA7B3EA0C8B9FDEC41837 /* MarketGlobalMetricService.swift */; }; 58AAA29A7E913ABAB6592FD7 /* SwapCoinCardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA80C6D2024281A5FA3E5 /* SwapCoinCardViewModel.swift */; }; 58AAA29F80AC65D37BD1D289 /* CoinSelectViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAB692B7C326319D186E4 /* CoinSelectViewModel.swift */; }; 58AAA2CEB9DB7E34921D7778 /* SwapDeadlineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAFF25BF263B5EC4188F7 /* SwapDeadlineViewModel.swift */; }; 58AAA2EBAFC1C443C48BA857 /* CoinChartFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA55A4A6A97C25F84034F /* CoinChartFactory.swift */; }; - 58AAA31D8AD811C0C5434426 /* MarketPostModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA42A6EB5242006547A92 /* MarketPostModule.swift */; }; - 58AAA331B4A743D9183F8449 /* MarketListTvlDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA9B26F62DB74FF3830D5 /* MarketListTvlDecorator.swift */; }; 58AAA34F0F6195DF86596A41 /* ChartConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA9D5D29115C5F435CF1B /* ChartConfiguration.swift */; }; - 58AAA35AF4F4454E0E9C7C60 /* MarketSingleSortHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA51AD262FBDC3D69EEF8 /* MarketSingleSortHeaderViewModel.swift */; }; 58AAA3692F9336D6AD79367F /* StepBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA422A0530C0C07E19F2F /* StepBadgeView.swift */; }; 58AAA3762D78142A83A22F50 /* SwapViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAEB2257174A64DE5E51B /* SwapViewModel.swift */; }; 58AAA393DF93EFFA5047589B /* FeeSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA24B6EB7103CB072A53D /* FeeSlider.swift */; }; @@ -1951,13 +1757,10 @@ 58AAA3A6458CB87F359F6366 /* SwapConfirmationAmountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA5BF9EE200DAA24AD42A /* SwapConfirmationAmountCell.swift */; }; 58AAA3BE2D0DEF53CE6CEE97 /* EvmKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAC5B00009B199A687EF3 /* EvmKitManager.swift */; }; 58AAA3F0AFD0D0F5FCD24DEF /* SelectorButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAE153E9683CB79DCF857 /* SelectorButton.swift */; }; - 58AAA3FE5DE72D3CEFFE4399 /* MarketPostService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA657E30EBD52A5E06ACF /* MarketPostService.swift */; }; 58AAA4109DE36F8934808DE0 /* MarketGlobalModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA7D7F06F7C044DF9CE0A /* MarketGlobalModule.swift */; }; 58AAA410C9996BA929E3CEEF /* InfoModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAD7AC450FEF913E5417F /* InfoModule.swift */; }; 58AAA415B26725FEF4A1128D /* DoubleSpendInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA72BD42697A894C2383C /* DoubleSpendInfoViewController.swift */; }; 58AAA431B5939F7961B65CCF /* CALayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAB126AA1B83DD40C426F /* CALayer.swift */; }; - 58AAA48687661E27807E9DF1 /* FavoritesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA0ED0FCFACF791EC865C /* FavoritesManager.swift */; }; - 58AAA488935A7DE6CF7C592D /* MarketGlobalMetricService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA7B3EA0C8B9FDEC41837 /* MarketGlobalMetricService.swift */; }; 58AAA48ED47FD19F368385FA /* MetricChartViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAAD2AA132E9B13726D8B /* MetricChartViewController.swift */; }; 58AAA49049C7EA04AABC41E9 /* SwapSlippageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAA86B59A4D59F08EB334 /* SwapSlippageViewModel.swift */; }; 58AAA4915E1B70248A8DC620 /* SwapApproveViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA43491E0E4F17D020455 /* SwapApproveViewModel.swift */; }; @@ -1965,7 +1768,6 @@ 58AAA4A4D0D7398E7184E7AB /* UITextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAFBDA192A490C33DBB95 /* UITextView.swift */; }; 58AAA4AEC3E593C4732D85FF /* MetricChartViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA765922F6668954C238E /* MetricChartViewModel.swift */; }; 58AAA4B64068280C684EE5C1 /* MetricChartModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAB934A3F1B6490245F1D /* MetricChartModule.swift */; }; - 58AAA4C8FEC03BAFB1B4863E /* MarketGlobalTvlMetricService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAADBB7B760C189AD6032F /* MarketGlobalTvlMetricService.swift */; }; 58AAA4E0703CDD2C6A4D1EED /* DebugPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA4022C5803D2440F43C7 /* DebugPresenter.swift */; }; 58AAA4E1C32E0B48A4CFECCD /* PaymentRequestAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA7A94D25C20240FD75C6 /* PaymentRequestAddress.swift */; }; 58AAA4F6DE67EE385EA955C6 /* CoinOverviewViewItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA0B8ECE5854FAB9362AC /* CoinOverviewViewItemFactory.swift */; }; @@ -1980,12 +1782,8 @@ 58AAA57E1D7AF0943273E58A /* UdnAddressParserItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA9A4761030BE9F60C85E /* UdnAddressParserItem.swift */; }; 58AAA59DF74C10DDCA79F35F /* AdditionalDataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA9E19A578FD13792D2B7 /* AdditionalDataView.swift */; }; 58AAA5A70BBDBD3A9D572261 /* OneInchSettingsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA75A0580C45CC08D89E8 /* OneInchSettingsService.swift */; }; - 58AAA5C000029E9EB74C46C4 /* MarketGlobalMetricViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA02D981360FF0CC50A19 /* MarketGlobalMetricViewController.swift */; }; 58AAA5D1FB4312F714C1D097 /* OneInchSettingsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA78B50AEDADC56B9DEBD /* OneInchSettingsModule.swift */; }; - 58AAA5EF1B46CAFB40139AD2 /* MarketPostModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA42A6EB5242006547A92 /* MarketPostModule.swift */; }; 58AAA604A4728EF375F8937D /* OneInchSettingsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA0D499D632E44F7BE172 /* OneInchSettingsDataSource.swift */; }; - 58AAA60557D3A9E3AE7372E0 /* MarketListDefiDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAFE702C2E51EEE209C56 /* MarketListDefiDecorator.swift */; }; - 58AAA626C0B12976749A948E /* MarketTvlSortHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA4A4F31EAB9164B33299 /* MarketTvlSortHeaderView.swift */; }; 58AAA6371183D7FB9606FEDA /* OneInchSendEvmTransactionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA83DD3B2F70F35F0DFE6 /* OneInchSendEvmTransactionService.swift */; }; 58AAA63F99B1FC1B88B5C8A0 /* UniswapSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA18F732998DCAA76E47C /* UniswapSettings.swift */; }; 58AAA64C3C4E4FA0E4213000 /* SwapApproveModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA251C8ADF1AE43EAF65F /* SwapApproveModule.swift */; }; @@ -2003,9 +1801,7 @@ 58AAA745D76A13D06800BDD1 /* EvmKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAC5B00009B199A687EF3 /* EvmKitManager.swift */; }; 58AAA747269D5AE1BBDDA2F7 /* LastBlockInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA9D55F97CE089EC67766 /* LastBlockInfo.swift */; }; 58AAA75358DF98C1D7191B81 /* DoubleSpendInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA72BD42697A894C2383C /* DoubleSpendInfoViewController.swift */; }; - 58AAA76E5789D2C9EAC9A2B6 /* MarketSingleSortHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA51AD262FBDC3D69EEF8 /* MarketSingleSortHeaderViewModel.swift */; }; 58AAA775B9228EEC791ED73D /* MarketGlobalFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA0A9EA8A2210522F38EE /* MarketGlobalFetcher.swift */; }; - 58AAA7B0CC093B05F7487496 /* MarketGlobalMetricModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA775FE9B46DA2910F508 /* MarketGlobalMetricModule.swift */; }; 58AAA7B99324DDA9C53692AD /* SwapApproveViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA681BF5F2CBDCD0D8898 /* SwapApproveViewController.swift */; }; 58AAA7C644D47F3B57DF97E0 /* CALayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAB126AA1B83DD40C426F /* CALayer.swift */; }; 58AAA7E6994159CD5FFDB31E /* InputBadgeWrapperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA885C8749392E263A615 /* InputBadgeWrapperView.swift */; }; @@ -2023,18 +1819,15 @@ 58AAA8D086D1DCFADA9A0938 /* OneInchSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA7365FA7771481534710 /* OneInchSettingsViewModel.swift */; }; 58AAA8D2A6FD519EFC668EC5 /* CoinPageModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA331E9A43EB0C7F186B1 /* CoinPageModule.swift */; }; 58AAA8D40B3399937CFEB0F2 /* PaymentRequestAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA7A94D25C20240FD75C6 /* PaymentRequestAddress.swift */; }; - 58AAA8D67EB6C19719BD760B /* MarketWatchlistService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAABDFE887324FC10AC290 /* MarketWatchlistService.swift */; }; 58AAA8E5EA8901CF69DDE43D /* LockDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA16E4AB334B67FFD891A /* LockDelegate.swift */; }; 58AAA900E2644527A2C78863 /* MetricChartService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA8CB22CEF84A71CF044F /* MetricChartService.swift */; }; 58AAA9053CD38F13CD944E2A /* SwapApproveAmountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA1233617C06AC975285A /* SwapApproveAmountView.swift */; }; 58AAA926E1D95F61CA06EFB8 /* SwapConfirmationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAB39CAE1453B9ED024E4 /* SwapConfirmationModule.swift */; }; - 58AAA937A06DD40BD9A64C71 /* MarketGlobalMetricViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA02D981360FF0CC50A19 /* MarketGlobalMetricViewController.swift */; }; 58AAA93B19192D8AE2590A4F /* DebugRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA905C4D766F902C08B65 /* DebugRouter.swift */; }; 58AAA9475B1C057E82B25C76 /* OneInchFeeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAF4E4A24283A9DF0191F /* OneInchFeeService.swift */; }; 58AAA96EA2327421BB3A115B /* SwapDeadlineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAFF25BF263B5EC4188F7 /* SwapDeadlineViewModel.swift */; }; 58AAA9771EA29EC8CDA33C58 /* OneInchSettingsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA78B50AEDADC56B9DEBD /* OneInchSettingsModule.swift */; }; 58AAA98A15442365CFE776F3 /* KeyboardAwareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAACC6B72757C7EDAA8577 /* KeyboardAwareViewController.swift */; }; - 58AAA996622FCD647B51A3C5 /* MarketGlobalMetricModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA775FE9B46DA2910F508 /* MarketGlobalMetricModule.swift */; }; 58AAA996A8547DBE1BF378CE /* SwapApproveService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAADE7CE3F004B908CEDA1 /* SwapApproveService.swift */; }; 58AAA9A289DE179B76AFA99F /* KeyboardAwareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAACC6B72757C7EDAA8577 /* KeyboardAwareViewController.swift */; }; 58AAA9AAB91A30A18A1F724E /* AddressUriParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAD40C9A99F0EDEFCAD14 /* AddressUriParser.swift */; }; @@ -2042,7 +1835,6 @@ 58AAA9AEFE1043B01BEC2D6A /* CoinSelectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA9BBAB97C2D21A83956C /* CoinSelectViewController.swift */; }; 58AAA9B29938CA65FA3CB3F0 /* AdditionalDataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA9E19A578FD13792D2B7 /* AdditionalDataView.swift */; }; 58AAA9E3B53672B3C37B727E /* StepBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA422A0530C0C07E19F2F /* StepBadgeView.swift */; }; - 58AAAA07DC05EF7F912EA184 /* MarketListTvlDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA9B26F62DB74FF3830D5 /* MarketListTvlDecorator.swift */; }; 58AAAA19D6C7812306A164EA /* SwapCoinCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAFB203A455CB53996F97 /* SwapCoinCardCell.swift */; }; 58AAAA2323F7CFCAB96FCF04 /* CoinPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA353DAC061C2123948FC /* CoinPageViewController.swift */; }; 58AAAA3E05FEB10822DFC8EE /* SwapStepCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA024EB2E850DD3277419 /* SwapStepCell.swift */; }; @@ -2051,23 +1843,15 @@ 58AAAA6AF87DE0EE337BB8AA /* GradientLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAE622FCAB8C2400A3149 /* GradientLayer.swift */; }; 58AAAA71882CB345D56BBA00 /* CoinChartFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA55A4A6A97C25F84034F /* CoinChartFactory.swift */; }; 58AAAA7B0493F3790D49AA14 /* SwapAllowanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAACE967F3E51A57A38835 /* SwapAllowanceViewModel.swift */; }; - 58AAAA8975F5B63340672D00 /* MarketWatchlistService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAABDFE887324FC10AC290 /* MarketWatchlistService.swift */; }; 58AAAAC61D3D8AD1AC4BEEAE /* AdditionalDataWithErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA8483BC1E85730F04CA3 /* AdditionalDataWithErrorView.swift */; }; - 58AAAAC777502E0C331C109F /* MarketGlobalDefiMetricService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAD81E45666E783B8B2EA /* MarketGlobalDefiMetricService.swift */; }; 58AAAAD2D124AA0323629B0E /* SwapApproveModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA251C8ADF1AE43EAF65F /* SwapApproveModule.swift */; }; 58AAAAE261DEB08128441641 /* AmountDecimalParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAF740AE2BEBD7CEBD563 /* AmountDecimalParser.swift */; }; 58AAAAE64799E5DD40D4C54A /* MetricChartModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAB934A3F1B6490245F1D /* MetricChartModule.swift */; }; - 58AAAAEC33A43B54E8E4D3FB /* MarketPostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA263DAB58FD63E6A9351 /* MarketPostViewController.swift */; }; - 58AAAAED41A83519EFB94237 /* MarketPostViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA11651E3CE29A461BF42 /* MarketPostViewModel.swift */; }; 58AAAAEDC64AE5716BC07673 /* SwapSlippageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAA86B59A4D59F08EB334 /* SwapSlippageViewModel.swift */; }; 58AAAAF8C20AB0E0299A36B8 /* CoinSelectModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAACD2595FD785AEF3C379 /* CoinSelectModule.swift */; }; 58AAAB04DD6CB4DBEE62526C /* ChartModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA5C1F206B755B477A30B /* ChartModule.swift */; }; - 58AAAB25F7EDB3EAE26E690C /* MarketGlobalTvlMetricViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA8E1106E31D68FD9181D /* MarketGlobalTvlMetricViewController.swift */; }; 58AAAB32C3D3DCBD004513C7 /* CoinChartViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA8F53B1A92A391DB1CFF /* CoinChartViewModel.swift */; }; - 58AAAB41ED4FC861D1C99AAC /* MarketOverviewHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA2521E8F8845D96AB865 /* MarketOverviewHeaderCell.swift */; }; - 58AAAB5D89B03B266A4F9B57 /* MarketOverviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAFB549AE163AD4F920DD /* MarketOverviewViewController.swift */; }; 58AAAB67059E3F289F557860 /* OneInchSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA7365FA7771481534710 /* OneInchSettingsViewModel.swift */; }; - 58AAAB9BE155C6F7630BCE31 /* MarketSingleSortHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA15F4FA7B9EC091EDFF3 /* MarketSingleSortHeaderView.swift */; }; 58AAAB9E86439B6DE1D22538 /* EvmAddressParserItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAAC62C5B05463D339BCC /* EvmAddressParserItem.swift */; }; 58AAABA864BFEC1F10F57E4B /* CoinChartService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA13C7C5B258310BA61AF /* CoinChartService.swift */; }; 58AAABC21BB15CAC923A13CB /* CoinCardModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA8AD6794AD2AAF462B7B /* CoinCardModule.swift */; }; @@ -2078,10 +1862,8 @@ 58AAAC4F4425F8C33B604020 /* SwapPendingAllowanceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAACF9B71A0C0DCFF7117E /* SwapPendingAllowanceService.swift */; }; 58AAAC635552C279592F60F9 /* EvmAddressParserItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAAC62C5B05463D339BCC /* EvmAddressParserItem.swift */; }; 58AAAC9A4813120F3B786D18 /* SwapConfirmationAmountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA5BF9EE200DAA24AD42A /* SwapConfirmationAmountCell.swift */; }; - 58AAACA5A8EC9B2A3182395F /* MarketGlobalTvlFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAEA0582FFB81EB6C6263 /* MarketGlobalTvlFetcher.swift */; }; 58AAACCD229B4D4D525A8182 /* CoinPageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA6CBFBB0EA959466977D /* CoinPageViewModel.swift */; }; 58AAACD7A57AA93736CDB54D /* SwapApproveAmountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA1233617C06AC975285A /* SwapApproveAmountView.swift */; }; - 58AAACF322E073F1DDA1FBDC /* MarketTvlSortHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA78BB269FEBB430092A3 /* MarketTvlSortHeaderViewModel.swift */; }; 58AAAD10078FF803A3C27F7C /* MetricChartService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA8CB22CEF84A71CF044F /* MetricChartService.swift */; }; 58AAAD15C27B67A91EA11F76 /* AddressUriParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAD40C9A99F0EDEFCAD14 /* AddressUriParser.swift */; }; 58AAAD1BFFE70A777DDF27A9 /* AddressParserChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA7305EF2A12D3DC82B32 /* AddressParserChain.swift */; }; @@ -2109,20 +1891,14 @@ 58AAAE6CFB04C47AC24A69D7 /* MarketGlobalFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA0A9EA8A2210522F38EE /* MarketGlobalFetcher.swift */; }; 58AAAE7CA65D1A4906C5E65E /* SwapSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA9828D8742CD3D9995D9 /* SwapSwitchCell.swift */; }; 58AAAE9383600A15C521DBD8 /* InfoModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAD7AC450FEF913E5417F /* InfoModule.swift */; }; - 58AAAEA69AA123E96665E2B7 /* MarketGlobalDefiMetricService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAD81E45666E783B8B2EA /* MarketGlobalDefiMetricService.swift */; }; - 58AAAEB0F729B839B9B99A04 /* MarketListDefiDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAFE702C2E51EEE209C56 /* MarketListDefiDecorator.swift */; }; 58AAAEDDF791C28174360A15 /* MetricChartFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAC7C8A0B2AECDD436A14 /* MetricChartFactory.swift */; }; 58AAAEEA99ACF936862312B2 /* SwapSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA9828D8742CD3D9995D9 /* SwapSwitchCell.swift */; }; - 58AAAEF20C684F912ED5D7AE /* FavoritesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA0ED0FCFACF791EC865C /* FavoritesManager.swift */; }; 58AAAF011B2E9CDF8455CA7B /* BaseEvmAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA656A81B2C12F618FB44 /* BaseEvmAdapter.swift */; }; 58AAAF041A3FE53A28893E74 /* RecipientAddressViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA19E66FCA0575AE33FAA /* RecipientAddressViewModel.swift */; }; 58AAAF222E553D8DCD123AB2 /* SwapAllowanceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA892221B1283EFC14B9E /* SwapAllowanceService.swift */; }; 58AAAF3B68B9F64DF20FA5AB /* FeeSliderWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAC2D03916C80C8DC5FE5 /* FeeSliderWrapper.swift */; }; 58AAAF4236075971CC88F7ED /* SwapApproveService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAADE7CE3F004B908CEDA1 /* SwapApproveService.swift */; }; - 58AAAF56EF620164D797D60F /* MarketTvlSortHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA4A4F31EAB9164B33299 /* MarketTvlSortHeaderView.swift */; }; 58AAAF6248682EE23B3C3D5A /* DebugLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA18F75B95ACBBAE94DF3 /* DebugLogger.swift */; }; - 58AAAF886ADA156E5559EE5B /* MarketOverviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAFB549AE163AD4F920DD /* MarketOverviewViewController.swift */; }; - 58AAAFC5FE754A2286161D16 /* MarketGlobalTvlFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAEA0582FFB81EB6C6263 /* MarketGlobalTvlFetcher.swift */; }; 58AAAFD1F07293D4691F2294 /* MetricChartFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAC7C8A0B2AECDD436A14 /* MetricChartFactory.swift */; }; 58AAAFDBA357FC699C07C334 /* AdditionalDataWithErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA8483BC1E85730F04CA3 /* AdditionalDataWithErrorView.swift */; }; 58AAAFE644C1B236B9714B47 /* CoinSelectViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAB692B7C326319D186E4 /* CoinSelectViewModel.swift */; }; @@ -2145,11 +1921,35 @@ 6B29072A2AF0CB8A006157D6 /* EventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B29071E2AF0CB8A006157D6 /* EventHandler.swift */; }; 6B55E33B2AF26D6400616B60 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 6B55E33A2AF26D6400616B60 /* Starscream */; }; 6B55E33D2AF26D7A00616B60 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 6B55E33C2AF26D7A00616B60 /* Starscream */; }; + 6B5F5E0E2C0C65F700E03EB2 /* MarketPlatformViewNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B5F5E0D2C0C65F700E03EB2 /* MarketPlatformViewNew.swift */; }; + 6B5F5E0F2C0C65F700E03EB2 /* MarketPlatformViewNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B5F5E0D2C0C65F700E03EB2 /* MarketPlatformViewNew.swift */; }; + 6B5F5E112C0C660900E03EB2 /* MarketPlatformViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B5F5E102C0C660900E03EB2 /* MarketPlatformViewModel.swift */; }; + 6B5F5E122C0C660900E03EB2 /* MarketPlatformViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B5F5E102C0C660900E03EB2 /* MarketPlatformViewModel.swift */; }; + 6B5F5E152C0DDD7100E03EB2 /* RankView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B5F5E142C0DDD7100E03EB2 /* RankView.swift */; }; + 6B5F5E162C0DDD7500E03EB2 /* RankView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B5F5E142C0DDD7100E03EB2 /* RankView.swift */; }; + 6B5F5E182C0DDD8700E03EB2 /* RankViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B5F5E172C0DDD8700E03EB2 /* RankViewModel.swift */; }; + 6B5F5E192C0DDD8700E03EB2 /* RankViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B5F5E172C0DDD8700E03EB2 /* RankViewModel.swift */; }; + 6B8BD39E2C11B959003ADE10 /* TextFieldAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B8BD39D2C11B959003ADE10 /* TextFieldAlert.swift */; }; + 6B8BD39F2C11B959003ADE10 /* TextFieldAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B8BD39D2C11B959003ADE10 /* TextFieldAlert.swift */; }; 6BA5117D2BCFA06F00CB5A54 /* FirstAppearModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BA5117C2BCFA06F00CB5A54 /* FirstAppearModifier.swift */; }; 6BA5117E2BCFA06F00CB5A54 /* FirstAppearModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BA5117C2BCFA06F00CB5A54 /* FirstAppearModifier.swift */; }; 6BAAF3472B9B245C00EFE5B2 /* ShimmerEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BAAF3442B9B245C00EFE5B2 /* ShimmerEffect.swift */; }; 6BAAF3492B9B245C00EFE5B2 /* SlideButtonStyling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BAAF3452B9B245C00EFE5B2 /* SlideButtonStyling.swift */; }; 6BAAF34B2B9B245C00EFE5B2 /* SlideButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BAAF3462B9B245C00EFE5B2 /* SlideButton.swift */; }; + 6BB14F6B2BF49E7100E879B2 /* WalletButtonHiddenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB14F6A2BF49E7100E879B2 /* WalletButtonHiddenManager.swift */; }; + 6BB14F6C2BF49E7100E879B2 /* WalletButtonHiddenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB14F6A2BF49E7100E879B2 /* WalletButtonHiddenManager.swift */; }; + 6BB14F722BFE550600E879B2 /* MarketEtfFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB14F712BFE550600E879B2 /* MarketEtfFetcher.swift */; }; + 6BB14F732BFE550600E879B2 /* MarketEtfFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB14F712BFE550600E879B2 /* MarketEtfFetcher.swift */; }; + 6BB14F752C01D04200E879B2 /* CheckBoxUiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB14F742C01D04200E879B2 /* CheckBoxUiView.swift */; }; + 6BB14F762C01D04200E879B2 /* CheckBoxUiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB14F742C01D04200E879B2 /* CheckBoxUiView.swift */; }; + 6BB14F7B2C05FBAC00E879B2 /* MarketTvlViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB14F7A2C05FAB600E879B2 /* MarketTvlViewModel.swift */; }; + 6BB14F7C2C05FBAD00E879B2 /* MarketTvlViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB14F7A2C05FAB600E879B2 /* MarketTvlViewModel.swift */; }; + 6BB14F7D2C05FBAF00E879B2 /* MarketTvlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB14F792C05FAB600E879B2 /* MarketTvlView.swift */; }; + 6BB14F7E2C05FBB000E879B2 /* MarketTvlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB14F792C05FAB600E879B2 /* MarketTvlView.swift */; }; + 6BB14F802C06F19300E879B2 /* DefiCoin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB14F7F2C06F19300E879B2 /* DefiCoin.swift */; }; + 6BB14F812C06F19300E879B2 /* DefiCoin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB14F7F2C06F19300E879B2 /* DefiCoin.swift */; }; + 6BBCE4A32BDA419200ABBD55 /* Web3Wallet in Frameworks */ = {isa = PBXBuildFile; productRef = 6BBCE4A22BDA419200ABBD55 /* Web3Wallet */; }; + 6BBCE4A52BDA419B00ABBD55 /* Web3Wallet in Frameworks */ = {isa = PBXBuildFile; productRef = 6BBCE4A42BDA419B00ABBD55 /* Web3Wallet */; }; 6BCD53002A161F4100993F20 /* BackupCloudModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCD52F72A161F4100993F20 /* BackupCloudModule.swift */; }; 6BCD53012A161F4100993F20 /* BackupCloudModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCD52F72A161F4100993F20 /* BackupCloudModule.swift */; }; 6BCD53022A161F4100993F20 /* ICloudBackupTermsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCD52F92A161F4100993F20 /* ICloudBackupTermsViewModel.swift */; }; @@ -2234,7 +2034,6 @@ ABC9A140E5EF9D0AD4234689 /* SendEip1155ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A9C09ECB9B0CCBAD8C21 /* SendEip1155ViewController.swift */; }; ABC9A14731CC409C5EBC4978 /* DismissPanGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA526C8BADA56269C4E0 /* DismissPanGestureRecognizer.swift */; }; ABC9A14877872E0FC7C9D0D2 /* ActionSheetTapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ACC91C29905F8E7A2046 /* ActionSheetTapView.swift */; }; - ABC9A160495EB67472B97E61 /* MarketWatchlistDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8B6A5C590B23C6F83C3 /* MarketWatchlistDecorator.swift */; }; ABC9A1636549E626FB32F71A /* WalletTokenBalanceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB2DC4C4412EFE6BEFF7 /* WalletTokenBalanceCell.swift */; }; ABC9A1798D6E2E4C868DA366 /* Eip1559FeeSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8E02C3486492E3B12F2 /* Eip1559FeeSettingsViewModel.swift */; }; ABC9A190E478402B48410FCC /* AlertButtonTintColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A87A0C7AA22B19520502 /* AlertButtonTintColor.swift */; }; @@ -2269,7 +2068,7 @@ ABC9A2542EA47C2ED85C06B9 /* WalletConnectListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE5FD79ECC4AC85B86FA /* WalletConnectListViewController.swift */; }; ABC9A2545322919129F163D5 /* SwapInputCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC2EF759D639F6CEA256 /* SwapInputCardView.swift */; }; ABC9A2559D001CA67E8F10C7 /* WalletConnectMainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD18F3E73F96DD6C4FA9 /* WalletConnectMainService.swift */; }; - ABC9A25C4149CC1DC03B853E /* SendViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AAF2ADD900F32D87C7BE /* SendViewModel.swift */; }; + ABC9A25C4149CC1DC03B853E /* SendViewModelOld.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AAF2ADD900F32D87C7BE /* SendViewModelOld.swift */; }; ABC9A266809A701B69765151 /* IntegerAmountInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA527E63E18179CB689A /* IntegerAmountInputCell.swift */; }; ABC9A2692A01293B1229EF50 /* WalletTokenBalanceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A352F3EAA38107897CEF /* WalletTokenBalanceService.swift */; }; ABC9A2746046C136F98F970A /* BackupContact.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC50307ABA3DF7034E1D /* BackupContact.swift */; }; @@ -2292,7 +2091,6 @@ ABC9A2CA505DB49DE0FB28DD /* WalletTokenBalanceCustomAmountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD448DC071D8800C6B12 /* WalletTokenBalanceCustomAmountCell.swift */; }; ABC9A2D0ACEDCFA5FDB04D89 /* IndicatorDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A12E4155640075755699 /* IndicatorDataSource.swift */; }; ABC9A2D3D28955B8AD82AFC3 /* BackupTypeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A5CDF9153AECED3DE50C /* BackupTypeView.swift */; }; - ABC9A2DA629CF38FD7B893EC /* MarketWatchlistDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8B6A5C590B23C6F83C3 /* MarketWatchlistDecorator.swift */; }; ABC9A2E2F6D884CC8444C029 /* WCSignMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A0643C6E6FA30D7EE473 /* WCSignMessageHandler.swift */; }; ABC9A2E71264B12B7FFC3736 /* WalletConnectListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3BEB33F6DBE2395FD11 /* WalletConnectListService.swift */; }; ABC9A2E921AE00E0AF5067DE /* CoinProChartModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A021D71EDD24DFB6BA62 /* CoinProChartModule.swift */; }; @@ -2439,7 +2237,7 @@ ABC9A6904E4DAE4C34EAEAE7 /* WCRequestPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8E515FB42EDE9ABD7DE /* WCRequestPayload.swift */; }; ABC9A69264C2086E4B3B09D2 /* WalletTokenBalanceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A352F3EAA38107897CEF /* WalletTokenBalanceService.swift */; }; ABC9A69A1A01DBD07CAAC9CD /* ContactBookAddressViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A55B0E99C1DD25839EDB /* ContactBookAddressViewController.swift */; }; - ABC9A69BADD39C6E9239A2A1 /* SendViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AAF2ADD900F32D87C7BE /* SendViewModel.swift */; }; + ABC9A69BADD39C6E9239A2A1 /* SendViewModelOld.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AAF2ADD900F32D87C7BE /* SendViewModelOld.swift */; }; ABC9A69C4D41D6E8B5DFBA97 /* UniswapV3DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE97D361FBF43F46F016 /* UniswapV3DataSource.swift */; }; ABC9A69FA41A9BC474DD1915 /* DiffLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A916C64B5EA9D96B8FDA /* DiffLabel.swift */; }; ABC9A6A484F9B3F7F1054379 /* WalletConnectMainPendingRequestService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AFF8093DEB7AFD7DBBCC /* WalletConnectMainPendingRequestService.swift */; }; @@ -2483,7 +2281,6 @@ ABC9A79CFCEBAC442A1B791D /* BackupAppModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A37065F4A8459C416F0A /* BackupAppModule.swift */; }; ABC9A7A9053C6ECF618D0E4A /* WalletConnectSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A4544AB5CA22ADE16417 /* WalletConnectSession.swift */; }; ABC9A7A9E27CC5F93BE5018B /* WalletConnectListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3BEB33F6DBE2395FD11 /* WalletConnectListService.swift */; }; - ABC9A7AF4EE29CDE045ADEF7 /* MarketCategoryMarketCapFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ADC1A3B17225B6CC0869 /* MarketCategoryMarketCapFetcher.swift */; }; ABC9A7C2087C3A641C3F9AD4 /* Shake.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A104D916039D690E454E /* Shake.swift */; }; ABC9A7CBFDC0DF741E29EA44 /* Integer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF26FDCB363793BF66E1 /* Integer.swift */; }; ABC9A7E1F93B0A85976C826D /* UniswapV3Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A253877D9FB972EFB8D7 /* UniswapV3Provider.swift */; }; @@ -2676,7 +2473,6 @@ ABC9AD2688A8DF327A3F92FC /* NoAccountWalletTokenListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A141E4C255C3E450863E /* NoAccountWalletTokenListService.swift */; }; ABC9AD27E074CF3FA292C647 /* IndicatorAdviceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A044BFF4E76CD17835CA /* IndicatorAdviceView.swift */; }; ABC9AD3001AAA0570B503876 /* ManageBarButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A6CFDF38D413679D2088 /* ManageBarButtonView.swift */; }; - ABC9AD3276132B33F6045AFF /* MarketCategoryMarketCapFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ADC1A3B17225B6CC0869 /* MarketCategoryMarketCapFetcher.swift */; }; ABC9AD41E7C88963F6512905 /* ChartIndicatorsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3758FE2D56036DF27FF /* ChartIndicatorsRepository.swift */; }; ABC9AD46006A85E907826E2B /* EnabledWalletCache_v_0_36.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A68AFE3CF24D2B88808F /* EnabledWalletCache_v_0_36.swift */; }; ABC9AD46AE6B5F432E0D2085 /* WalletTokenBalanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A52822CE6B8830CF5EF4 /* WalletTokenBalanceViewModel.swift */; }; @@ -2820,6 +2616,8 @@ D023D26E2A24CD4F004F65B0 /* TronKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D023D26C2A24CD4F004F65B0 /* TronKitManager.swift */; }; D023D2712A25CF61004F65B0 /* TronAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D023D2702A25CF61004F65B0 /* TronAdapter.swift */; }; D023D2722A25CF61004F65B0 /* TronAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D023D2702A25CF61004F65B0 /* TronAdapter.swift */; }; + D02447D92C09FA5200A04BBC /* CoinTreasuriesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02447D82C09FA5200A04BBC /* CoinTreasuriesView.swift */; }; + D02447DA2C09FA5200A04BBC /* CoinTreasuriesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02447D82C09FA5200A04BBC /* CoinTreasuriesView.swift */; }; D02A67BC272A7460009B2C1C /* TweetCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02A67B0272A7460009B2C1C /* TweetCell.swift */; }; D02A67BD272A7460009B2C1C /* TweetCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02A67B0272A7460009B2C1C /* TweetCell.swift */; }; D02A67BF272A7460009B2C1C /* TweetsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02A67B1272A7460009B2C1C /* TweetsProvider.swift */; }; @@ -2840,6 +2638,10 @@ D02A67D5272A7460009B2C1C /* CoinTweetsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02A67BA272A7460009B2C1C /* CoinTweetsService.swift */; }; D02A67D7272A7460009B2C1C /* CoinTweetsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02A67BB272A7460009B2C1C /* CoinTweetsModule.swift */; }; D02A67D8272A7460009B2C1C /* CoinTweetsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02A67BB272A7460009B2C1C /* CoinTweetsModule.swift */; }; + D033289F2BF6199600BBB364 /* InfoNewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D033289E2BF6199600BBB364 /* InfoNewView.swift */; }; + D03328A02BF6199600BBB364 /* InfoNewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D033289E2BF6199600BBB364 /* InfoNewView.swift */; }; + D03F74822BF76D0A004FBCFA /* GasPriceData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03F74812BF76D0A004FBCFA /* GasPriceData.swift */; }; + D03F74832BF76D0A004FBCFA /* GasPriceData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03F74812BF76D0A004FBCFA /* GasPriceData.swift */; }; D04D98EA268055A2001A3135 /* TransactionRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04D98E9268055A2001A3135 /* TransactionRecord.swift */; }; D04D98EB268055A2001A3135 /* TransactionRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04D98E9268055A2001A3135 /* TransactionRecord.swift */; }; D04DD5412B68C0EF00219B87 /* HighlightedTiltledTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04DD5402B68C0EF00219B87 /* HighlightedTiltledTextView.swift */; }; @@ -2850,6 +2652,8 @@ D0532CC22B149E110015DF40 /* WatchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0532CC02B149E110015DF40 /* WatchViewController.swift */; }; D0532CC42B149E450015DF40 /* WatchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0532CC32B149E450015DF40 /* WatchService.swift */; }; D0532CC52B149E450015DF40 /* WatchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0532CC32B149E450015DF40 /* WatchService.swift */; }; + D054DAE32BE5123F0040B7C9 /* InitialTransactionSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D054DAE22BE5123F0040B7C9 /* InitialTransactionSettings.swift */; }; + D054DAE42BE5123F0040B7C9 /* InitialTransactionSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D054DAE22BE5123F0040B7C9 /* InitialTransactionSettings.swift */; }; D05E968D2A25D6C6002CCD71 /* Trc20Adapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05E968C2A25D6C6002CCD71 /* Trc20Adapter.swift */; }; D05E968E2A25D6C6002CCD71 /* Trc20Adapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05E968C2A25D6C6002CCD71 /* Trc20Adapter.swift */; }; D05E96902A261D82002CCD71 /* TronTransactionAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05E968F2A261D82002CCD71 /* TronTransactionAdapter.swift */; }; @@ -2890,6 +2694,12 @@ D07157E52A2DDAA6006F141F /* SendTronViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07157E32A2DDAA6006F141F /* SendTronViewController.swift */; }; D0740B1B2B87585000B085F9 /* ResendBitcoinService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0740B1A2B87585000B085F9 /* ResendBitcoinService.swift */; }; D0740B1C2B87585100B085F9 /* ResendBitcoinService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0740B1A2B87585000B085F9 /* ResendBitcoinService.swift */; }; + D084F6BE2BEB94F700407FA4 /* OutputSelectView2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D084F6BD2BEB94F700407FA4 /* OutputSelectView2.swift */; }; + D084F6BF2BEB94F700407FA4 /* OutputSelectView2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D084F6BD2BEB94F700407FA4 /* OutputSelectView2.swift */; }; + D084F6C12BEB951C00407FA4 /* OutputSelectorViewModel2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D084F6C02BEB951C00407FA4 /* OutputSelectorViewModel2.swift */; }; + D084F6C22BEB951C00407FA4 /* OutputSelectorViewModel2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D084F6C02BEB951C00407FA4 /* OutputSelectorViewModel2.swift */; }; + D086A9162BF4D08400462024 /* SendParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D086A9152BF4D08400462024 /* SendParameters.swift */; }; + D086A9172BF4D08400462024 /* SendParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D086A9152BF4D08400462024 /* SendParameters.swift */; }; D087627329815DAE00E6FFD4 /* ChooseWatchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087626E29815DAD00E6FFD4 /* ChooseWatchViewController.swift */; }; D087627429815DAE00E6FFD4 /* ChooseWatchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087626E29815DAD00E6FFD4 /* ChooseWatchViewController.swift */; }; D087627629815DAE00E6FFD4 /* ChooseWatchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087626F29815DAD00E6FFD4 /* ChooseWatchViewModel.swift */; }; @@ -2908,6 +2718,14 @@ D09200C6293F21720091981A /* RestoreNonStandardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09200C0293F21710091981A /* RestoreNonStandardViewModel.swift */; }; D09200C8293F21720091981A /* RestoreNonStandardModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09200C1293F21720091981A /* RestoreNonStandardModule.swift */; }; D09200C9293F21720091981A /* RestoreNonStandardModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09200C1293F21720091981A /* RestoreNonStandardModule.swift */; }; + D092C5882C12DC8D0060D915 /* PriceChangeModeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D502C0703B400515664 /* PriceChangeModeManager.swift */; }; + D092C5892C12DC8E0060D915 /* PriceChangeModeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D502C0703B400515664 /* PriceChangeModeManager.swift */; }; + D092C58B2C12DE3D0060D915 /* PriceChangeManager+HsTimePeriod.swift in Sources */ = {isa = PBXBuildFile; fileRef = D092C58A2C12DE3D0060D915 /* PriceChangeManager+HsTimePeriod.swift */; }; + D092C58C2C12DE3D0060D915 /* PriceChangeManager+HsTimePeriod.swift in Sources */ = {isa = PBXBuildFile; fileRef = D092C58A2C12DE3D0060D915 /* PriceChangeManager+HsTimePeriod.swift */; }; + D092C58D2C12DE800060D915 /* PriceChangeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D4D2C07020300515664 /* PriceChangeMode.swift */; }; + D092C58E2C12DE810060D915 /* PriceChangeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D4D2C07020300515664 /* PriceChangeMode.swift */; }; + D09C5C632C076C0E00E6909E /* CoinIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09C5C622C076C0E00E6909E /* CoinIconView.swift */; }; + D09C5C642C076C0E00E6909E /* CoinIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09C5C622C076C0E00E6909E /* CoinIconView.swift */; }; D09D768C2A2E066E004311E6 /* SendTronConfirmationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09D768B2A2E066E004311E6 /* SendTronConfirmationModule.swift */; }; D09D768E2A2E06D6004311E6 /* SendTronConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09D768D2A2E06D6004311E6 /* SendTronConfirmationViewController.swift */; }; D09D768F2A2E06D6004311E6 /* SendTronConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09D768D2A2E06D6004311E6 /* SendTronConfirmationViewController.swift */; }; @@ -2916,6 +2734,12 @@ D09D76942A2E07BD004311E6 /* SendTronConfirmationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09D76932A2E07BD004311E6 /* SendTronConfirmationService.swift */; }; D09D76952A2E07BD004311E6 /* SendTronConfirmationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09D76932A2E07BD004311E6 /* SendTronConfirmationService.swift */; }; D09D76992A2F3682004311E6 /* SendTronConfirmationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09D768B2A2E066E004311E6 /* SendTronConfirmationModule.swift */; }; + D0A6902B2C00ACF600E59296 /* CautionDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A6902A2C00ACF600E59296 /* CautionDataSource.swift */; }; + D0A6902C2C00ACF600E59296 /* CautionDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A6902A2C00ACF600E59296 /* CautionDataSource.swift */; }; + D0A6902E2C04969300E59296 /* CautionDataSourceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A6902D2C04969300E59296 /* CautionDataSourceViewModel.swift */; }; + D0A6902F2C04969300E59296 /* CautionDataSourceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A6902D2C04969300E59296 /* CautionDataSourceViewModel.swift */; }; + D0A690342C05D01C00E59296 /* UIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A690332C05D01C00E59296 /* UIImageView.swift */; }; + D0A690352C05D01C00E59296 /* UIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A690332C05D01C00E59296 /* UIImageView.swift */; }; D0A980A92B5E3C0900127AF4 /* StepChangeButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A980A82B5E3C0900127AF4 /* StepChangeButtonsView.swift */; }; D0A980AA2B5E3C0900127AF4 /* StepChangeButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A980A82B5E3C0900127AF4 /* StepChangeButtonsView.swift */; }; D0A980AF2B60E73F00127AF4 /* LegacyFeeSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A980AE2B60E73F00127AF4 /* LegacyFeeSettingsView.swift */; }; @@ -2936,6 +2760,20 @@ D0DA740F272A6EFC0072BE86 /* UnicodeURL in Frameworks */ = {isa = PBXBuildFile; productRef = D0DA740E272A6EFC0072BE86 /* UnicodeURL */; }; D0DA7411272A6F180072BE86 /* IDNSDK in Frameworks */ = {isa = PBXBuildFile; productRef = D0DA7410272A6F180072BE86 /* IDNSDK */; }; D0DA7413272A6F180072BE86 /* UnicodeURL in Frameworks */ = {isa = PBXBuildFile; productRef = D0DA7412272A6F180072BE86 /* UnicodeURL */; }; + D0DEFF042BD1253C004C9DF0 /* BitcoinFeeSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DEFF022BD1253B004C9DF0 /* BitcoinFeeSettingsViewModel.swift */; }; + D0DEFF052BD1253C004C9DF0 /* BitcoinFeeSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DEFF022BD1253B004C9DF0 /* BitcoinFeeSettingsViewModel.swift */; }; + D0DEFF062BD1253C004C9DF0 /* BitcoinFeeSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DEFF032BD1253B004C9DF0 /* BitcoinFeeSettingsView.swift */; }; + D0DEFF072BD1253C004C9DF0 /* BitcoinFeeSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DEFF032BD1253B004C9DF0 /* BitcoinFeeSettingsView.swift */; }; + D0DEFF0A2BD1257F004C9DF0 /* BitcoinTransactionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DEFF082BD1257E004C9DF0 /* BitcoinTransactionService.swift */; }; + D0DEFF0B2BD1257F004C9DF0 /* BitcoinTransactionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DEFF082BD1257E004C9DF0 /* BitcoinTransactionService.swift */; }; + D0DEFF0C2BD1257F004C9DF0 /* BitcoinFeeData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DEFF092BD1257F004C9DF0 /* BitcoinFeeData.swift */; }; + D0DEFF0D2BD1257F004C9DF0 /* BitcoinFeeData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DEFF092BD1257F004C9DF0 /* BitcoinFeeData.swift */; }; + D0E5E84F2BE22172005080A4 /* BitcoinSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5E84E2BE22172005080A4 /* BitcoinSendHandler.swift */; }; + D0E5E8502BE22172005080A4 /* BitcoinSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5E84E2BE22172005080A4 /* BitcoinSendHandler.swift */; }; + D0E5E8522BE260C8005080A4 /* BitcoinPreSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5E8512BE260C8005080A4 /* BitcoinPreSendHandler.swift */; }; + D0E5E8532BE260C8005080A4 /* BitcoinPreSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5E8512BE260C8005080A4 /* BitcoinPreSendHandler.swift */; }; + D0E5E8552BE38AA2005080A4 /* TronSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5E8542BE38AA2005080A4 /* TronSendHandler.swift */; }; + D0E5E8562BE38AA2005080A4 /* TronSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5E8542BE38AA2005080A4 /* TronSendHandler.swift */; }; D0E659BB2B875003000D8981 /* ResendBitcoinViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E659BA2B875003000D8981 /* ResendBitcoinViewModel.swift */; }; D0E659BC2B875003000D8981 /* ResendBitcoinViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E659BA2B875003000D8981 /* ResendBitcoinViewModel.swift */; }; D0EC34DB2A4450B100BB308B /* HCaptcha in Frameworks */ = {isa = PBXBuildFile; productRef = D0EC34DA2A4450B100BB308B /* HCaptcha */; }; @@ -2948,14 +2786,62 @@ D0F132A82B6B990500C7310E /* RbfDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F132A62B6B990500C7310E /* RbfDataSource.swift */; }; D0F9F5172B99857700C3190A /* FeeSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F9F5162B99857700C3190A /* FeeSettings.swift */; }; D0F9F5182B99857700C3190A /* FeeSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F9F5162B99857700C3190A /* FeeSettings.swift */; }; + D311DA1C2BD114B00013DB8F /* MarketView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D311DA1B2BD114B00013DB8F /* MarketView.swift */; }; + D311DA1D2BD114B00013DB8F /* MarketView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D311DA1B2BD114B00013DB8F /* MarketView.swift */; }; + D311DA1F2BD115240013DB8F /* MarketGlobalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D311DA1E2BD115240013DB8F /* MarketGlobalViewModel.swift */; }; + D311DA202BD115240013DB8F /* MarketGlobalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D311DA1E2BD115240013DB8F /* MarketGlobalViewModel.swift */; }; + D311DA222BD23C230013DB8F /* MarketAdvancedSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D311DA212BD23C230013DB8F /* MarketAdvancedSearchView.swift */; }; + D311DA232BD23C230013DB8F /* MarketAdvancedSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D311DA212BD23C230013DB8F /* MarketAdvancedSearchView.swift */; }; + D311DA252BD23C890013DB8F /* ScrollableTabHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D311DA242BD23C890013DB8F /* ScrollableTabHeaderView.swift */; }; + D311DA262BD23C890013DB8F /* ScrollableTabHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D311DA242BD23C890013DB8F /* ScrollableTabHeaderView.swift */; }; + D31369862BEA187E00BA6B5B /* ZcashSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D31369852BEA187E00BA6B5B /* ZcashSendHandler.swift */; }; + D31369872BEA187E00BA6B5B /* ZcashSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D31369852BEA187E00BA6B5B /* ZcashSendHandler.swift */; }; + D31369892BEA188D00BA6B5B /* ZcashPreSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D31369882BEA188D00BA6B5B /* ZcashPreSendHandler.swift */; }; + D313698A2BEA188D00BA6B5B /* ZcashPreSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D31369882BEA188D00BA6B5B /* ZcashPreSendHandler.swift */; }; D31C4760238BF176008CB818 /* MnemonicDerivation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D31C4759238BF175008CB818 /* MnemonicDerivation.swift */; }; D31C4761238BF176008CB818 /* MnemonicDerivation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D31C4759238BF175008CB818 /* MnemonicDerivation.swift */; }; D31C4763238BF176008CB818 /* FeeRateState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D31C475A238BF175008CB818 /* FeeRateState.swift */; }; D31C4764238BF176008CB818 /* FeeRateState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D31C475A238BF175008CB818 /* FeeRateState.swift */; }; + D3384D092BFCB43800515664 /* MarketWatchlistSignalsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D082BFCB43800515664 /* MarketWatchlistSignalsView.swift */; }; + D3384D0A2BFCB43800515664 /* MarketWatchlistSignalsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D082BFCB43800515664 /* MarketWatchlistSignalsView.swift */; }; + D3384D0C2BFCB8F000515664 /* MarketWatchlistSignalBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D0B2BFCB8F000515664 /* MarketWatchlistSignalBadge.swift */; }; + D3384D0D2BFCB8F000515664 /* MarketWatchlistSignalBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D0B2BFCB8F000515664 /* MarketWatchlistSignalBadge.swift */; }; + D3384D102BFDCBDE00515664 /* MarketEtfView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D0F2BFDCBDE00515664 /* MarketEtfView.swift */; }; + D3384D112BFDCBDE00515664 /* MarketEtfView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D0F2BFDCBDE00515664 /* MarketEtfView.swift */; }; + D3384D132BFDCBE700515664 /* MarketEtfViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D122BFDCBE700515664 /* MarketEtfViewModel.swift */; }; + D3384D142BFDCBE700515664 /* MarketEtfViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D122BFDCBE700515664 /* MarketEtfViewModel.swift */; }; + D3384D162BFDEF6800515664 /* Etf.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D152BFDEF6800515664 /* Etf.swift */; }; + D3384D172BFDEF6800515664 /* Etf.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D152BFDEF6800515664 /* Etf.swift */; }; + D3384D1A2BFF0CAF00515664 /* MarketMarketCapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D192BFF0CAF00515664 /* MarketMarketCapView.swift */; }; + D3384D1B2BFF0CAF00515664 /* MarketMarketCapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D192BFF0CAF00515664 /* MarketMarketCapView.swift */; }; + D3384D1D2BFF0CB800515664 /* MarketMarketCapViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D1C2BFF0CB800515664 /* MarketMarketCapViewModel.swift */; }; + D3384D1E2BFF0CB800515664 /* MarketMarketCapViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D1C2BFF0CB800515664 /* MarketMarketCapViewModel.swift */; }; + D3384D212BFF0CCA00515664 /* MarketVolumeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D202BFF0CCA00515664 /* MarketVolumeView.swift */; }; + D3384D222BFF0CCA00515664 /* MarketVolumeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D202BFF0CCA00515664 /* MarketVolumeView.swift */; }; + D3384D242BFF0CD100515664 /* MarketVolumeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D232BFF0CD100515664 /* MarketVolumeViewModel.swift */; }; + D3384D252BFF0CD100515664 /* MarketVolumeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D232BFF0CD100515664 /* MarketVolumeViewModel.swift */; }; + D3384D4E2C07020300515664 /* PriceChangeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D4D2C07020300515664 /* PriceChangeMode.swift */; }; + D3384D4F2C07020300515664 /* PriceChangeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D4D2C07020300515664 /* PriceChangeMode.swift */; }; + D3384D512C0703B400515664 /* PriceChangeModeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D502C0703B400515664 /* PriceChangeModeManager.swift */; }; + D3384D522C0703B400515664 /* PriceChangeModeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D502C0703B400515664 /* PriceChangeModeManager.swift */; }; D339A93D29126D0F00B895BE /* HsCryptoKit in Frameworks */ = {isa = PBXBuildFile; productRef = D339A93C29126D0F00B895BE /* HsCryptoKit */; }; D339A93F29126D2A00B895BE /* HsCryptoKit in Frameworks */ = {isa = PBXBuildFile; productRef = D339A93E29126D2A00B895BE /* HsCryptoKit */; }; + D3402AEE2BF5D58B003BF6F8 /* WatchlistViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3402AED2BF5D58B003BF6F8 /* WatchlistViewModel.swift */; }; + D3402AEF2BF5D58B003BF6F8 /* WatchlistViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3402AED2BF5D58B003BF6F8 /* WatchlistViewModel.swift */; }; + D3402AF12BF5D59D003BF6F8 /* WatchlistModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3402AF02BF5D59D003BF6F8 /* WatchlistModifier.swift */; }; + D3402AF22BF5D59D003BF6F8 /* WatchlistModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3402AF02BF5D59D003BF6F8 /* WatchlistModifier.swift */; }; + D3402AF72BF71C11003BF6F8 /* WatchlistManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3402AF62BF71C11003BF6F8 /* WatchlistManager.swift */; }; + D3402AF82BF71C11003BF6F8 /* WatchlistManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3402AF62BF71C11003BF6F8 /* WatchlistManager.swift */; }; D3447DEA25E38300009928D9 /* WalletConnectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B968B299A67FC7FEAE3 /* WalletConnectManager.swift */; }; D3447DEB25E38300009928D9 /* WalletConnectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B968B299A67FC7FEAE3 /* WalletConnectManager.swift */; }; + D34903172BE8DF48005F147B /* BinanceSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D34903162BE8DF48005F147B /* BinanceSendHandler.swift */; }; + D34903182BE8DF48005F147B /* BinanceSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D34903162BE8DF48005F147B /* BinanceSendHandler.swift */; }; + D349031A2BE8DF5F005F147B /* BinancePreSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D34903192BE8DF5F005F147B /* BinancePreSendHandler.swift */; }; + D349031B2BE8DF5F005F147B /* BinancePreSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D34903192BE8DF5F005F147B /* BinancePreSendHandler.swift */; }; + D34A29B62BFB4E3200F63036 /* WatchlistSortBy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D34A29B52BFB4E3200F63036 /* WatchlistSortBy.swift */; }; + D34A29B72BFB4E3200F63036 /* WatchlistSortBy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D34A29B52BFB4E3200F63036 /* WatchlistSortBy.swift */; }; + D34A29B82BFB4E3200F63036 /* WatchlistSortBy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D34A29B52BFB4E3200F63036 /* WatchlistSortBy.swift */; }; + D34A29B92BFB4E3200F63036 /* WatchlistSortBy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D34A29B52BFB4E3200F63036 /* WatchlistSortBy.swift */; }; D350DDB02AE2526E00CF1989 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = D350DDAF2AE2526E00CF1989 /* Localizable.xcstrings */; }; D350DDB12AE2526E00CF1989 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = D350DDAF2AE2526E00CF1989 /* Localizable.xcstrings */; }; D350DDB22AE27E3B00CF1989 /* AppWidget.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = D350DDB92AE27E3B00CF1989 /* AppWidget.intentdefinition */; }; @@ -3040,6 +2926,42 @@ D36DE100272FD92F000BC916 /* SwapSelectProviderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36DE0F5272FD92F000BC916 /* SwapSelectProviderViewModel.swift */; }; D36E0C2A28D084AB00B622B9 /* CollectionViewCenteredFlowLayout in Frameworks */ = {isa = PBXBuildFile; productRef = D36E0C2928D084AB00B622B9 /* CollectionViewCenteredFlowLayout */; }; D36E0C2C28D084CB00B622B9 /* CollectionViewCenteredFlowLayout in Frameworks */ = {isa = PBXBuildFile; productRef = D36E0C2B28D084CB00B622B9 /* CollectionViewCenteredFlowLayout */; }; + D36E50812BF7534700C361BD /* WatchlistManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3402AF62BF71C11003BF6F8 /* WatchlistManager.swift */; }; + D36E50822BF7534900C361BD /* WatchlistManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3402AF62BF71C11003BF6F8 /* WatchlistManager.swift */; }; + D36E50842BF75B6900C361BD /* WatchlistTimePeriod.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36E50832BF75B6900C361BD /* WatchlistTimePeriod.swift */; }; + D36E50852BF75B6900C361BD /* WatchlistTimePeriod.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36E50832BF75B6900C361BD /* WatchlistTimePeriod.swift */; }; + D36E50862BF75B6C00C361BD /* WatchlistTimePeriod.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36E50832BF75B6900C361BD /* WatchlistTimePeriod.swift */; }; + D36E50872BF75B6D00C361BD /* WatchlistTimePeriod.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36E50832BF75B6900C361BD /* WatchlistTimePeriod.swift */; }; + D36E508A2BF76FA700C361BD /* WatchlistEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36E50892BF76FA700C361BD /* WatchlistEntry.swift */; }; + D36E508B2BF76FA700C361BD /* WatchlistEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36E50892BF76FA700C361BD /* WatchlistEntry.swift */; }; + D36E508D2BF76FB400C361BD /* WatchlistProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36E508C2BF76FB400C361BD /* WatchlistProvider.swift */; }; + D36E508E2BF76FB400C361BD /* WatchlistProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36E508C2BF76FB400C361BD /* WatchlistProvider.swift */; }; + D36E50932BF7852D00C361BD /* CoinListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36E50922BF7852D00C361BD /* CoinListView.swift */; }; + D36E50942BF7852D00C361BD /* CoinListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36E50922BF7852D00C361BD /* CoinListView.swift */; }; + D3833AD72BEE1A7900ACECFB /* MarketWatchlistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3833AD62BEE1A7900ACECFB /* MarketWatchlistView.swift */; }; + D3833AD82BEE1A7900ACECFB /* MarketWatchlistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3833AD62BEE1A7900ACECFB /* MarketWatchlistView.swift */; }; + D3833ADA2BEE1A8300ACECFB /* MarketWatchlistViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3833AD92BEE1A8300ACECFB /* MarketWatchlistViewModel.swift */; }; + D3833ADB2BEE1A8300ACECFB /* MarketWatchlistViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3833AD92BEE1A8300ACECFB /* MarketWatchlistViewModel.swift */; }; + D3833ADE2BEE3FE000ACECFB /* MarketPlatformsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3833ADD2BEE3FE000ACECFB /* MarketPlatformsView.swift */; }; + D3833ADF2BEE3FE000ACECFB /* MarketPlatformsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3833ADD2BEE3FE000ACECFB /* MarketPlatformsView.swift */; }; + D3833AE12BEE3FE800ACECFB /* MarketPlatformsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3833AE02BEE3FE800ACECFB /* MarketPlatformsViewModel.swift */; }; + D3833AE22BEE3FE800ACECFB /* MarketPlatformsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3833AE02BEE3FE800ACECFB /* MarketPlatformsViewModel.swift */; }; + D3833AEA2BEE4CAA00ACECFB /* TopPlatform.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3833AE92BEE4CAA00ACECFB /* TopPlatform.swift */; }; + D3833AEB2BEE4CAA00ACECFB /* TopPlatform.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3833AE92BEE4CAA00ACECFB /* TopPlatform.swift */; }; + D3833AF22BF20B8600ACECFB /* MarketPairsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3833AF12BF20B8600ACECFB /* MarketPairsView.swift */; }; + D3833AF32BF20B8600ACECFB /* MarketPairsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3833AF12BF20B8600ACECFB /* MarketPairsView.swift */; }; + D3833AF52BF20B8D00ACECFB /* MarketPairsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3833AF42BF20B8D00ACECFB /* MarketPairsViewModel.swift */; }; + D3833AF62BF20B8D00ACECFB /* MarketPairsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3833AF42BF20B8D00ACECFB /* MarketPairsViewModel.swift */; }; + D3833AF82BF2181800ACECFB /* MarketPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3833AF72BF2181800ACECFB /* MarketPair.swift */; }; + D3833AF92BF2181800ACECFB /* MarketPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3833AF72BF2181800ACECFB /* MarketPair.swift */; }; + D3833AFC2BF335C700ACECFB /* MarketNewsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3833AFB2BF335C700ACECFB /* MarketNewsView.swift */; }; + D3833AFD2BF335C700ACECFB /* MarketNewsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3833AFB2BF335C700ACECFB /* MarketNewsView.swift */; }; + D3833AFF2BF335D100ACECFB /* MarketNewsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3833AFE2BF335D100ACECFB /* MarketNewsViewModel.swift */; }; + D3833B002BF335D100ACECFB /* MarketNewsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3833AFE2BF335D100ACECFB /* MarketNewsViewModel.swift */; }; + D3833B022BF38A8000ACECFB /* MarketTabViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3833B012BF38A8000ACECFB /* MarketTabViewModel.swift */; }; + D3833B032BF38A8000ACECFB /* MarketTabViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3833B012BF38A8000ACECFB /* MarketTabViewModel.swift */; }; + D3833B052BF4AFB800ACECFB /* MarqueeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3833B042BF4AFB800ACECFB /* MarqueeView.swift */; }; + D3833B062BF4AFB800ACECFB /* MarqueeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3833B042BF4AFB800ACECFB /* MarqueeView.swift */; }; D38404E4218317DF007D50AD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3285F4520BD158E00644076 /* AppDelegate.swift */; }; D38404E8218317DF007D50AD /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3B62A8820CA40DC005A9F80 /* MainViewController.swift */; }; D38404F9218317DF007D50AD /* MainModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B96D2BC5994AC8EC794 /* MainModule.swift */; }; @@ -3100,6 +3022,16 @@ D38406C221832757007D50AD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D3285F4C20BD158F00644076 /* Assets.xcassets */; }; D38406C5218327E3007D50AD /* AppIcon.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D38406C3218327B1007D50AD /* AppIcon.xcassets */; }; D38406C921832968007D50AD /* AppIconDev.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D38406C821832968007D50AD /* AppIconDev.xcassets */; }; + D389BC462C0DCF4100724504 /* Advice.swift in Sources */ = {isa = PBXBuildFile; fileRef = D389BC452C0DCF4100724504 /* Advice.swift */; }; + D389BC472C0DCF4100724504 /* Advice.swift in Sources */ = {isa = PBXBuildFile; fileRef = D389BC452C0DCF4100724504 /* Advice.swift */; }; + D389BC492C0DDA8F00724504 /* HsTimePeriod.swift in Sources */ = {isa = PBXBuildFile; fileRef = D389BC482C0DDA8F00724504 /* HsTimePeriod.swift */; }; + D389BC4A2C0DDA8F00724504 /* HsTimePeriod.swift in Sources */ = {isa = PBXBuildFile; fileRef = D389BC482C0DDA8F00724504 /* HsTimePeriod.swift */; }; + D389BC4C2C0DDCF500724504 /* MarketAdvancedSearchBlockchainsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D389BC4B2C0DDCF500724504 /* MarketAdvancedSearchBlockchainsView.swift */; }; + D389BC4D2C0DDCF500724504 /* MarketAdvancedSearchBlockchainsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D389BC4B2C0DDCF500724504 /* MarketAdvancedSearchBlockchainsView.swift */; }; + D389BC4F2C0DEF1800724504 /* MarketAdvancedSearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D389BC4E2C0DEF1800724504 /* MarketAdvancedSearchResultsView.swift */; }; + D389BC502C0DEF1800724504 /* MarketAdvancedSearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D389BC4E2C0DEF1800724504 /* MarketAdvancedSearchResultsView.swift */; }; + D389BC522C0DEF2200724504 /* MarketAdvancedSearchResultsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D389BC512C0DEF2200724504 /* MarketAdvancedSearchResultsViewModel.swift */; }; + D389BC532C0DEF2200724504 /* MarketAdvancedSearchResultsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D389BC512C0DEF2200724504 /* MarketAdvancedSearchResultsViewModel.swift */; }; D3948EF22ADA846400FAE566 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D3948EF12ADA846400FAE566 /* WidgetKit.framework */; }; D3948EF42ADA846400FAE566 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D3948EF32ADA846400FAE566 /* SwiftUI.framework */; }; D3948EF72ADA846400FAE566 /* AppWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3948EF62ADA846400FAE566 /* AppWidgetBundle.swift */; }; @@ -3117,12 +3049,28 @@ D3993DB828F42595008720FB /* WalletConnect in Frameworks */ = {isa = PBXBuildFile; productRef = D3993DB728F42595008720FB /* WalletConnect */; }; D3993DC228F42992008720FB /* UnstoppableDomainsResolution in Frameworks */ = {isa = PBXBuildFile; productRef = D3993DC128F42992008720FB /* UnstoppableDomainsResolution */; }; D3993DC428F429AA008720FB /* UnstoppableDomainsResolution in Frameworks */ = {isa = PBXBuildFile; productRef = D3993DC328F429AA008720FB /* UnstoppableDomainsResolution */; }; + D3A580882BE4DAA2003953F4 /* EvmSendData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A580872BE4DAA2003953F4 /* EvmSendData.swift */; }; + D3A580892BE4DAA2003953F4 /* EvmSendData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A580872BE4DAA2003953F4 /* EvmSendData.swift */; }; + D3A5808B2BE4DB11003953F4 /* WalletConnectSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A5808A2BE4DB11003953F4 /* WalletConnectSendHandler.swift */; }; + D3A5808C2BE4DB11003953F4 /* WalletConnectSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A5808A2BE4DB11003953F4 /* WalletConnectSendHandler.swift */; }; + D3A580942BE8AA80003953F4 /* BitcoinSendSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A580932BE8AA80003953F4 /* BitcoinSendSettingsView.swift */; }; + D3A580952BE8AA80003953F4 /* BitcoinSendSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A580932BE8AA80003953F4 /* BitcoinSendSettingsView.swift */; }; + D3A580972BE8AA90003953F4 /* BitcoinSendSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A580962BE8AA90003953F4 /* BitcoinSendSettingsViewModel.swift */; }; + D3A580982BE8AA90003953F4 /* BitcoinSendSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A580962BE8AA90003953F4 /* BitcoinSendSettingsViewModel.swift */; }; D3AF5A8929FFD85800C1399E /* RxCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = D3AF5A8829FFD85800C1399E /* RxCocoa */; }; D3AF5A8B29FFD85800C1399E /* RxRelay in Frameworks */ = {isa = PBXBuildFile; productRef = D3AF5A8A29FFD85800C1399E /* RxRelay */; }; D3AF5A8D29FFD85800C1399E /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = D3AF5A8C29FFD85800C1399E /* RxSwift */; }; D3AF5A8F29FFD87400C1399E /* RxCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = D3AF5A8E29FFD87400C1399E /* RxCocoa */; }; D3AF5A9129FFD87500C1399E /* RxRelay in Frameworks */ = {isa = PBXBuildFile; productRef = D3AF5A9029FFD87500C1399E /* RxRelay */; }; D3AF5A9329FFD87500C1399E /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = D3AF5A9229FFD87500C1399E /* RxSwift */; }; + D3B73E272BDBC6120067429D /* IPreSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3B73E262BDBC6120067429D /* IPreSendHandler.swift */; }; + D3B73E282BDBC6120067429D /* IPreSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3B73E262BDBC6120067429D /* IPreSendHandler.swift */; }; + D3B73E2A2BDBC61D0067429D /* EvmPreSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3B73E292BDBC61D0067429D /* EvmPreSendHandler.swift */; }; + D3B73E2B2BDBC61D0067429D /* EvmPreSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3B73E292BDBC61D0067429D /* EvmPreSendHandler.swift */; }; + D3B73E2D2BDF6B6D0067429D /* MultiSwapSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3B73E2C2BDF6B6D0067429D /* MultiSwapSendHandler.swift */; }; + D3B73E2E2BDF6B6D0067429D /* MultiSwapSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3B73E2C2BDF6B6D0067429D /* MultiSwapSendHandler.swift */; }; + D3B73E302BDFC5580067429D /* PriceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3B73E2F2BDFC5580067429D /* PriceRow.swift */; }; + D3B73E312BDFC5580067429D /* PriceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3B73E2F2BDFC5580067429D /* PriceRow.swift */; }; D3BA257F2ADFAD7C002B13EA /* AppWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3948EF62ADA846400FAE566 /* AppWidgetBundle.swift */; }; D3BA25842ADFAD7C002B13EA /* SingleCoinPriceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CFAA2B156E439EB18B3 /* SingleCoinPriceView.swift */; }; D3BA25852ADFAD7C002B13EA /* SingleCoinPriceEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E514EF2784402C7DC2A /* SingleCoinPriceEntry.swift */; }; @@ -3137,7 +3085,6 @@ D3BA259F2ADFAF23002B13EA /* HsToolKit in Frameworks */ = {isa = PBXBuildFile; productRef = D3BA25982ADFAF23002B13EA /* HsToolKit */; }; D3BA25A02ADFAF23002B13EA /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D3948F092ADA887300FAE566 /* Intents.framework */; }; D3BA25A72ADFB042002B13EA /* IntentExtension Prod.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D3BA25A52ADFAF23002B13EA /* IntentExtension Prod.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - D3BC25802B0B5E1E0092F682 /* TonKitKmm.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D3BC257F2B0B5E1E0092F682 /* TonKitKmm.xcframework */; }; D3BC25812B0B5E1E0092F682 /* TonKitKmm.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D3BC257F2B0B5E1E0092F682 /* TonKitKmm.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; D3BC25832B0B5E460092F682 /* TonKitKmm.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D3BC257F2B0B5E1E0092F682 /* TonKitKmm.xcframework */; }; D3BC25842B0B5E460092F682 /* TonKitKmm.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D3BC257F2B0B5E1E0092F682 /* TonKitKmm.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -3157,6 +3104,26 @@ D3C187E2290FD00E00FE1900 /* ComponentKit in Frameworks */ = {isa = PBXBuildFile; productRef = D3C187E1290FD00E00FE1900 /* ComponentKit */; }; D3C187E4290FD00E00FE1900 /* HUD in Frameworks */ = {isa = PBXBuildFile; productRef = D3C187E3290FD00E00FE1900 /* HUD */; }; D3C187E8290FD00E00FE1900 /* SectionsTableView in Frameworks */ = {isa = PBXBuildFile; productRef = D3C187E7290FD00E00FE1900 /* SectionsTableView */; }; + D3D13A5F2C0D9DCB002484BC /* MarketAdvancedSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D13A5E2C0D9DCB002484BC /* MarketAdvancedSearchViewModel.swift */; }; + D3D13A602C0D9DCB002484BC /* MarketAdvancedSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D13A5E2C0D9DCB002484BC /* MarketAdvancedSearchViewModel.swift */; }; + D3DB51992BD63D680091BBDB /* MarketSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DB51982BD63D680091BBDB /* MarketSearchViewModel.swift */; }; + D3DB519A2BD63D680091BBDB /* MarketSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DB51982BD63D680091BBDB /* MarketSearchViewModel.swift */; }; + D3DB519C2BD685180091BBDB /* RedactedModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DB519B2BD685180091BBDB /* RedactedModifier.swift */; }; + D3DB519D2BD685180091BBDB /* RedactedModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DB519B2BD685180091BBDB /* RedactedModifier.swift */; }; + D3DB519F2BD6854A0091BBDB /* MarketSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DB519E2BD6854A0091BBDB /* MarketSearchView.swift */; }; + D3DB51A02BD6854A0091BBDB /* MarketSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DB519E2BD6854A0091BBDB /* MarketSearchView.swift */; }; + D3DB51A22BD6857E0091BBDB /* MarketGlobalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DB51A12BD6857E0091BBDB /* MarketGlobalView.swift */; }; + D3DB51A32BD6857E0091BBDB /* MarketGlobalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DB51A12BD6857E0091BBDB /* MarketGlobalView.swift */; }; + D3DB51A52BD685B40091BBDB /* MarketTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DB51A42BD685B40091BBDB /* MarketTabView.swift */; }; + D3DB51A62BD685B40091BBDB /* MarketTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DB51A42BD685B40091BBDB /* MarketTabView.swift */; }; + D3DB51A82BD787490091BBDB /* MarketCoinsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DB51A72BD787490091BBDB /* MarketCoinsView.swift */; }; + D3DB51A92BD787490091BBDB /* MarketCoinsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DB51A72BD787490091BBDB /* MarketCoinsView.swift */; }; + D3DB51AB2BD787A00091BBDB /* MarketCoinsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DB51AA2BD787A00091BBDB /* MarketCoinsViewModel.swift */; }; + D3DB51AC2BD787A00091BBDB /* MarketCoinsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DB51AA2BD787A00091BBDB /* MarketCoinsViewModel.swift */; }; + D3DB51AF2BD7AF860091BBDB /* DiffText.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DB51AE2BD7AF860091BBDB /* DiffText.swift */; }; + D3DB51B02BD7AF860091BBDB /* DiffText.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DB51AE2BD7AF860091BBDB /* DiffText.swift */; }; + D3DB51B22BD912A00091BBDB /* MarketInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DB51B12BD912A00091BBDB /* MarketInfo.swift */; }; + D3DB51B32BD912A00091BBDB /* MarketInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DB51B12BD912A00091BBDB /* MarketInfo.swift */; }; D3DD671C2BC3BB9300EC7F78 /* BaseEvmMultiSwapQuote.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DD671B2BC3BB9300EC7F78 /* BaseEvmMultiSwapQuote.swift */; }; D3DD671D2BC3BB9300EC7F78 /* BaseEvmMultiSwapQuote.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DD671B2BC3BB9300EC7F78 /* BaseEvmMultiSwapQuote.swift */; }; D3DD671F2BC3BD1200EC7F78 /* BaseUniswapMultiSwapQuote.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DD671E2BC3BD1200EC7F78 /* BaseUniswapMultiSwapQuote.swift */; }; @@ -3182,6 +3149,18 @@ D3E323C82AE7B8E400F73914 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = D3E323C72AE7B8E400F73914 /* KeychainAccess */; }; D3E323CA2AE7B8F400F73914 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = D3E323C92AE7B8F400F73914 /* KeychainAccess */; }; D3F7D6412A94D53500477BB1 /* BaseTransactionsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B106BD8E4DBD67B7700 /* BaseTransactionsService.swift */; }; + D3F9B0252BE38AF1009FFA95 /* RegularSendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F9B0242BE38AF1009FFA95 /* RegularSendView.swift */; }; + D3F9B0262BE38AF1009FFA95 /* RegularSendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F9B0242BE38AF1009FFA95 /* RegularSendView.swift */; }; + D3F9B02B2BE3A9A1009FFA95 /* MultiSwapSendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F9B02A2BE3A9A1009FFA95 /* MultiSwapSendView.swift */; }; + D3F9B02C2BE3A9A1009FFA95 /* MultiSwapSendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F9B02A2BE3A9A1009FFA95 /* MultiSwapSendView.swift */; }; + D3F9B0312BE3B39D009FFA95 /* EvmDecoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F9B0302BE3B39D009FFA95 /* EvmDecoration.swift */; }; + D3F9B0322BE3B39D009FFA95 /* EvmDecoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F9B0302BE3B39D009FFA95 /* EvmDecoration.swift */; }; + D3F9B0342BE3B3A7009FFA95 /* EvmDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F9B0332BE3B3A7009FFA95 /* EvmDecorator.swift */; }; + D3F9B0352BE3B3A7009FFA95 /* EvmDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F9B0332BE3B3A7009FFA95 /* EvmDecorator.swift */; }; + D3F9B0372BE3B5AA009FFA95 /* WalletConnectSendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F9B0362BE3B5AA009FFA95 /* WalletConnectSendView.swift */; }; + D3F9B0382BE3B5AA009FFA95 /* WalletConnectSendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F9B0362BE3B5AA009FFA95 /* WalletConnectSendView.swift */; }; + D3F9B03A2BE3BB36009FFA95 /* WalletConnectSendViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F9B0392BE3BB36009FFA95 /* WalletConnectSendViewModel.swift */; }; + D3F9B03B2BE3BB36009FFA95 /* WalletConnectSendViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F9B0392BE3BB36009FFA95 /* WalletConnectSendViewModel.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -3272,11 +3251,8 @@ 11B3502637A858E6DDF9471B /* EvmSyncSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EvmSyncSource.swift; sourceTree = ""; }; 11B3502AEB7EF95A590A7B1B /* NftStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftStorage.swift; sourceTree = ""; }; 11B350369A891BEA3A525E5B /* UITabBarItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITabBarItem.swift; sourceTree = ""; }; - 11B3503B9A985B4835FDB03D /* MarketMultiSortHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketMultiSortHeaderView.swift; sourceTree = ""; }; - 11B350465C489A233625E8F2 /* MarketOverviewTopPairsDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewTopPairsDataSource.swift; sourceTree = ""; }; 11B350575488360C1A598DF3 /* ReceiveSelectCoinViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveSelectCoinViewModel.swift; sourceTree = ""; }; 11B3505A43D9C2787B3BD153 /* PasscodeLockManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasscodeLockManager.swift; sourceTree = ""; }; - 11B3505AD2C1640DEAD8CFFC /* MarketTopViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketTopViewController.swift; sourceTree = ""; }; 11B35062C72B1D98A2A4EDA9 /* MultiSwapQuotesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiSwapQuotesView.swift; sourceTree = ""; }; 11B3506758F70E9014947BB3 /* CexWithdrawConfirmViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexWithdrawConfirmViewModel.swift; sourceTree = ""; }; 11B3506BFA73130CA9A1FF71 /* CoinOverviewView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinOverviewView.swift; sourceTree = ""; }; @@ -3298,7 +3274,6 @@ 11B350BD0CE4F979CA88EFF0 /* RestoreModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreModule.swift; sourceTree = ""; }; 11B350BD364F07D1AC759865 /* NSAttributedString.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSAttributedString.swift; sourceTree = ""; }; 11B350C0CB7083E2738D356C /* ListSectionHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListSectionHeader.swift; sourceTree = ""; }; - 11B350CAB1C54A2CAA4C76F6 /* MarketCategoryModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketCategoryModule.swift; sourceTree = ""; }; 11B350CCAA0C9F2F5279F680 /* TransactionsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionsViewController.swift; sourceTree = ""; }; 11B350CD0F79715E1A5EE8BF /* NftCollectionAssetsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionAssetsService.swift; sourceTree = ""; }; 11B350CDE31673BA1673B620 /* CoinToggleViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinToggleViewModel.swift; sourceTree = ""; }; @@ -3311,7 +3286,6 @@ 11B350F532661482B6170F92 /* IMultiSwapQuote.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IMultiSwapQuote.swift; sourceTree = ""; }; 11B350F5D363E9B1D6C9120F /* FormCautionCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormCautionCell.swift; sourceTree = ""; }; 11B350F6C5F6ABC288511AF0 /* AccountRecord_v_0_19.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountRecord_v_0_19.swift; sourceTree = ""; }; - 11B350FAB6F1A6E1FCFACB2F /* MarketMultiSortHeaderViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketMultiSortHeaderViewModel.swift; sourceTree = ""; }; 11B350FFC1582D23FD709970 /* BlockchainSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockchainSettingsView.swift; sourceTree = ""; }; 11B35100DD6E2DBF905FD19B /* NftCollectionAssetsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionAssetsModule.swift; sourceTree = ""; }; 11B35102BB1E66987670CD1F /* AddBep2TokenBlockchainService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddBep2TokenBlockchainService.swift; sourceTree = ""; }; @@ -3322,7 +3296,6 @@ 11B3513049D27CB1FA264600 /* MnemonicPhraseCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MnemonicPhraseCell.swift; sourceTree = ""; }; 11B35136653741E9703E61DE /* WalletTokenListViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletTokenListViewModel.swift; sourceTree = ""; }; 11B3513A7417C236F56E5383 /* CoinValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinValue.swift; sourceTree = ""; }; - 11B3513AC6560B9C37C342F3 /* CoinRankService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinRankService.swift; sourceTree = ""; }; 11B35140CD5BF8B1C26A6278 /* MultiSwapButtonState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiSwapButtonState.swift; sourceTree = ""; }; 11B351436E090F4C05243103 /* NftCollectionOverviewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionOverviewViewModel.swift; sourceTree = ""; }; 11B351454D8FE8FEDA2C1EC9 /* ScanQrBlurView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScanQrBlurView.swift; sourceTree = ""; }; @@ -3335,7 +3308,6 @@ 11B351628BA5984C6EBB412E /* CreateAccountSimpleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateAccountSimpleViewController.swift; sourceTree = ""; }; 11B35163E7C4454BBA9E2E9E /* AlertItemCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertItemCell.swift; sourceTree = ""; }; 11B3516415E7A3217BBB1681 /* AlertRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertRouter.swift; sourceTree = ""; }; - 11B351664970D7EA1F7B50C7 /* CoinRankHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinRankHeaderView.swift; sourceTree = ""; }; 11B3517B0E763E2C217654A7 /* RecoveryPhraseModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseModule.swift; sourceTree = ""; }; 11B3517F84E9913C9030E749 /* CexWithdrawConfirmViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexWithdrawConfirmViewController.swift; sourceTree = ""; }; 11B35185ECC372A193D00A00 /* CoinTreasury.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinTreasury.swift; sourceTree = ""; }; @@ -3358,7 +3330,6 @@ 11B351DBFA79DAF0A82A1925 /* TabButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabButtonStyle.swift; sourceTree = ""; }; 11B351E034126F57DB7B4263 /* NftActivityService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftActivityService.swift; sourceTree = ""; }; 11B351E1107158B6A2BF2149 /* ActivateSubscriptionService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivateSubscriptionService.swift; sourceTree = ""; }; - 11B351E253E310F1738EBE13 /* CoinTreasuriesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinTreasuriesViewController.swift; sourceTree = ""; }; 11B351E61AB3FB570A4F7C66 /* Wallet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = ""; }; 11B351E7E7B2593D31FB04FD /* TopCoinsWidget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopCoinsWidget.swift; sourceTree = ""; }; 11B351EBA5DE11150CE2E3F9 /* RestorePrivateKeyService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestorePrivateKeyService.swift; sourceTree = ""; }; @@ -3375,7 +3346,6 @@ 11B3522A9A7774977CF39A1D /* NftImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftImageView.swift; sourceTree = ""; }; 11B3522CBA84677E00D44983 /* CoinTreasuriesViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinTreasuriesViewModel.swift; sourceTree = ""; }; 11B35239B1D2F94B326FC703 /* BirthdayInputViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BirthdayInputViewController.swift; sourceTree = ""; }; - 11B352436A876FC59DF41C78 /* SendAmountView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendAmountView.swift; sourceTree = ""; }; 11B3524B273DD5AB2FF5C7A6 /* Eip20Kit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Eip20Kit.swift; sourceTree = ""; }; 11B35252F90F25774BDD2CB3 /* NftActivityModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftActivityModule.swift; sourceTree = ""; }; 11B3525F8436F286491A241F /* BinanceChainKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BinanceChainKit.swift; sourceTree = ""; }; @@ -3385,7 +3355,6 @@ 11B3526A40F07F6C8E77BEF9 /* BlockchainSettingRecord_v_0_24.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockchainSettingRecord_v_0_24.swift; sourceTree = ""; }; 11B3526E11EC0F9CFCC69D17 /* SendAvailableBalanceViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendAvailableBalanceViewModel.swift; sourceTree = ""; }; 11B352782BB83C4E447092DB /* BtcBlockchainSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BtcBlockchainSettingsView.swift; sourceTree = ""; }; - 11B3527F1528AA697AAA6E61 /* TopPlatformViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopPlatformViewModel.swift; sourceTree = ""; }; 11B3528090862B6792A76DA4 /* FaqCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaqCell.swift; sourceTree = ""; }; 11B352884D47E0B23DCF2C2C /* AppManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppManager.swift; sourceTree = ""; }; 11B3528DDD55DDA1BAC2BADB /* ActiveAccount_v_0_36.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActiveAccount_v_0_36.swift; sourceTree = ""; }; @@ -3402,11 +3371,9 @@ 11B352A41EC99ADCC8F3E3E9 /* FormAmountInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormAmountInputView.swift; sourceTree = ""; }; 11B352A8C9C3AA2AB1776F3C /* UnlinkViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnlinkViewController.swift; sourceTree = ""; }; 11B352ABFDEAEEA84D3FDD8B /* AddEvmTokenBlockchainService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddEvmTokenBlockchainService.swift; sourceTree = ""; }; - 11B352AC4F5BE70D055293D7 /* MarketCategoryViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketCategoryViewModel.swift; sourceTree = ""; }; 11B352B4E116BEC01B972A39 /* FaqService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaqService.swift; sourceTree = ""; }; 11B352BACB38FE566F6F575B /* NftEventMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftEventMetadata.swift; sourceTree = ""; }; 11B352BD333C9D69ECB82884 /* BadgeViewNew.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BadgeViewNew.swift; sourceTree = ""; }; - 11B352BDC42A2F717AFAE7BD /* MarketOverviewTopPairsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewTopPairsService.swift; sourceTree = ""; }; 11B352C2F20DB6266112BE68 /* TransactionsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionsService.swift; sourceTree = ""; }; 11B352C35227943125FF2008 /* HeaderAmountView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeaderAmountView.swift; sourceTree = ""; }; 11B352CFEDEBF0A01CC7073D /* GuidesModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GuidesModule.swift; sourceTree = ""; }; @@ -3433,7 +3400,7 @@ 11B3532946EA785A7C65D193 /* RestoreSettingsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreSettingsService.swift; sourceTree = ""; }; 11B3532A1DC90E3D0E3403F8 /* ReceiveAddressViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveAddressViewModel.swift; sourceTree = ""; }; 11B35332D245CFF50A68F8CA /* SectionsTableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SectionsTableView.swift; sourceTree = ""; }; - 11B353355FF7FBE72BF60981 /* SendView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendView.swift; sourceTree = ""; }; + 11B353355FF7FBE72BF60981 /* PreSendView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreSendView.swift; sourceTree = ""; }; 11B353356496AA219686B993 /* UIWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIWindow.swift; sourceTree = ""; }; 11B35336293A4473DD9F5C8B /* RestoreSettingsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreSettingsModule.swift; sourceTree = ""; }; 11B35340910590E6FCF05A90 /* NftCollectionAssetsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionAssetsViewController.swift; sourceTree = ""; }; @@ -3453,10 +3420,8 @@ 11B35396831B92AAC156DF1D /* NftCollectionViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionViewModel.swift; sourceTree = ""; }; 11B35399E91DA7AAF4104C8F /* CreateAccountAdvancedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateAccountAdvancedViewController.swift; sourceTree = ""; }; 11B353A0B705D8EABC5B6827 /* EnabledWallet_v_0_10.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnabledWallet_v_0_10.swift; sourceTree = ""; }; - 11B353A1CC274EDBF8A67DEA /* MarketAdvancedSearchResultViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketAdvancedSearchResultViewController.swift; sourceTree = ""; }; 11B353A64E88BD68714D4D07 /* RestoreViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreViewController.swift; sourceTree = ""; }; 11B353B02ADF5EC5CC83FB33 /* SendHandlerFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendHandlerFactory.swift; sourceTree = ""; }; - 11B353B060BDF272932D3522 /* MarketListMarketFieldDecorator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketListMarketFieldDecorator.swift; sourceTree = ""; }; 11B353B4C04282FDBB1B6563 /* Guide.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Guide.swift; sourceTree = ""; }; 11B353BA87FDCB1BCBA92E61 /* InputStackView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputStackView.swift; sourceTree = ""; }; 11B353C09FE554834C760777 /* SelectorModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorModule.swift; sourceTree = ""; }; @@ -3465,7 +3430,6 @@ 11B353E1284B381BE56AC663 /* NumPadView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumPadView.swift; sourceTree = ""; }; 11B353E80D544DAF20B12B56 /* AboutModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutModule.swift; sourceTree = ""; }; 11B353F1E3B5875396F03E0D /* CoinSelectService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinSelectService.swift; sourceTree = ""; }; - 11B353F8063B95C6571AA517 /* MultiSwapConfirmationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiSwapConfirmationView.swift; sourceTree = ""; }; 11B353FA8AE18587D516754B /* BlockchainSettingRecordStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockchainSettingRecordStorage.swift; sourceTree = ""; }; 11B3540B41309A446C1DDB83 /* CoinAnalyticsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinAnalyticsViewController.swift; sourceTree = ""; }; 11B3540BDD94203AFD41C6C7 /* SendEvmService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendEvmService.swift; sourceTree = ""; }; @@ -3477,7 +3441,6 @@ 11B3542B6FE4B4F0C0B65369 /* FeeData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeeData.swift; sourceTree = ""; }; 11B3542B81C83F855FB3CD6C /* TonOutgoingTransactionRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TonOutgoingTransactionRecord.swift; sourceTree = ""; }; 11B3543968337A40168D3EB0 /* MarkdownParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownParser.swift; sourceTree = ""; }; - 11B3543F4D196A47EFE3E6F7 /* MarketHeaderCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketHeaderCell.swift; sourceTree = ""; }; 11B3544AC69419F31F20F34E /* TransactionFilterViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionFilterViewModel.swift; sourceTree = ""; }; 11B35450456BE5E3EE8F7391 /* Faq.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Faq.swift; sourceTree = ""; }; 11B354506A9B41DCD49B2807 /* UnlockModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnlockModule.swift; sourceTree = ""; }; @@ -3517,28 +3480,23 @@ 11B355267E1A6678B7B5FCF1 /* AddTokenModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddTokenModule.swift; sourceTree = ""; }; 11B3552D3F84BA594EFE964C /* MarkdownBlockQuoteCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownBlockQuoteCell.swift; sourceTree = ""; }; 11B3553967AFF40F6A9A611A /* CoinPageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinPageView.swift; sourceTree = ""; }; - 11B3554159E6E5B7C1E71F04 /* MarketOverviewService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewService.swift; sourceTree = ""; }; 11B35542A7D7FE1BDC2E73E2 /* AccountType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountType.swift; sourceTree = ""; }; 11B355436F62829DBE3C92B4 /* CellComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CellComponent.swift; sourceTree = ""; }; 11B3554BC96C9C24C24CC2B0 /* DuressModeSelectView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuressModeSelectView.swift; sourceTree = ""; }; 11B35564351D59D37278C723 /* ExtendedKeyService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtendedKeyService.swift; sourceTree = ""; }; 11B35577CFC2384E3A454329 /* EnabledWallet_v_0_20.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnabledWallet_v_0_20.swift; sourceTree = ""; }; 11B3557DF76CFEBE7DA50D81 /* BottomGradientWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BottomGradientWrapper.swift; sourceTree = ""; }; - 11B3557E5ACDC89EF79C8C0C /* MarketListMarketPairDecorator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketListMarketPairDecorator.swift; sourceTree = ""; }; 11B35588D5C27AD3673DEE2F /* AppIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppIcon.swift; sourceTree = ""; }; 11B3558ACECAA1C886FA82C0 /* RecoveryPhraseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseViewController.swift; sourceTree = ""; }; 11B3558D624AF040E9D102DF /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 11B35592753D3F2A9CCA5809 /* UnlinkWatchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnlinkWatchViewController.swift; sourceTree = ""; }; 11B355949F6D268EF1977DC9 /* ManageAccountViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageAccountViewModel.swift; sourceTree = ""; }; - 11B355A2FFA369C4E89DFF53 /* ISendConfirmationData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ISendConfirmationData.swift; sourceTree = ""; }; + 11B355A2FFA369C4E89DFF53 /* ISendData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ISendData.swift; sourceTree = ""; }; 11B355ABE6F12B78194DDEDD /* WalletSorter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletSorter.swift; sourceTree = ""; }; 11B355ABE89C2793563829BD /* Date.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; 11B355B546A9CA0324F2F0AE /* AmountInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AmountInputView.swift; sourceTree = ""; }; 11B355B6EFCAB40DC5ACEA3D /* MultiSelectorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiSelectorViewController.swift; sourceTree = ""; }; - 11B355BEB95969D89B3F8876 /* MarketListViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketListViewModel.swift; sourceTree = ""; }; 11B355C1E3C922BAE804AAF9 /* WalletConnectSessionStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectSessionStorage.swift; sourceTree = ""; }; - 11B355C615D9FE4290671D5D /* MarketTopPairsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketTopPairsModule.swift; sourceTree = ""; }; - 11B355D1DB2F95F1183FF2F8 /* CoinPriceListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinPriceListView.swift; sourceTree = ""; }; 11B355D5EFD2B74DE15F0C2A /* FaqModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaqModule.swift; sourceTree = ""; }; 11B355DF40EB498107EDAA4A /* BrandFooterCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrandFooterCell.swift; sourceTree = ""; }; 11B355E86612AEE00ED19CFE /* BackupManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupManager.swift; sourceTree = ""; }; @@ -3552,7 +3510,6 @@ 11B35614C6E244926AF48701 /* Account.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = ""; }; 11B35615F3ECB5D6E467B49A /* ReceiveSelectCoinService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveSelectCoinService.swift; sourceTree = ""; }; 11B35625BCC4536F39B151F0 /* RestorePrivateKeyViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestorePrivateKeyViewModel.swift; sourceTree = ""; }; - 11B3562819DF141457837340 /* MarketWatchlistToggleService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketWatchlistToggleService.swift; sourceTree = ""; }; 11B3562F83BCEE2720B1C23F /* ITransactionService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ITransactionService.swift; sourceTree = ""; }; 11B356300F9A6A12C29450E7 /* DateFormatterCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateFormatterCache.swift; sourceTree = ""; }; 11B3563ED22080EE222848A5 /* MarkdownImageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownImageCell.swift; sourceTree = ""; }; @@ -3565,9 +3522,7 @@ 11B356671FA76C7DEDA50B94 /* SwapApproveConfirmationModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapApproveConfirmationModule.swift; sourceTree = ""; }; 11B3566B18FBFBA85D98D824 /* EnabledWalletCacheManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnabledWalletCacheManager.swift; sourceTree = ""; }; 11B3566DC3A97A5CC3E2C729 /* BalancePrimaryValueManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalancePrimaryValueManager.swift; sourceTree = ""; }; - 11B3566FE007887C3528583C /* MarketWatchlistViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketWatchlistViewModel.swift; sourceTree = ""; }; 11B3567314F1A1DF8D1B2910 /* TransactionFilterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionFilterView.swift; sourceTree = ""; }; - 11B35683F0E48309E8298427 /* SendModuleNew.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendModuleNew.swift; sourceTree = ""; }; 11B356861F703A5A5C6630B6 /* LitecoinAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LitecoinAdapter.swift; sourceTree = ""; }; 11B3568F6FAF721301DEC188 /* FormCautionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormCautionView.swift; sourceTree = ""; }; 11B35690912F374FEE910193 /* NftCollectionMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionMetadata.swift; sourceTree = ""; }; @@ -3575,7 +3530,6 @@ 11B3569F2E6BD5E9CBCFCA1F /* Token.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = ""; }; 11B356A734526DECD9606A66 /* AccountRecord_v_0_36.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountRecord_v_0_36.swift; sourceTree = ""; }; 11B356B9F833E1AEE0D6D589 /* CexDepositService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexDepositService.swift; sourceTree = ""; }; - 11B356BEB2B4DFC3E9C950C5 /* MarketAdvancedSearchViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketAdvancedSearchViewModel.swift; sourceTree = ""; }; 11B356D15E318829D9C7F5F1 /* EvmBlockchainManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EvmBlockchainManager.swift; sourceTree = ""; }; 11B356D5A5F32E88FEC7629D /* AddTokenViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddTokenViewController.swift; sourceTree = ""; }; 11B356D6300E64A1982BC9EB /* UIAlertController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIAlertController.swift; sourceTree = ""; }; @@ -3592,10 +3546,8 @@ 11B35708A630D70385F34A8B /* NftCollectionModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionModule.swift; sourceTree = ""; }; 11B35711A471C5A45DD87108 /* EvmNetworkViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EvmNetworkViewController.swift; sourceTree = ""; }; 11B3572105A456CCDD63E94D /* SecondaryButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecondaryButtonStyle.swift; sourceTree = ""; }; - 11B357229D5E717F2051F0AC /* MarketCategoryService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketCategoryService.swift; sourceTree = ""; }; 11B3572B7C2F16CD51F37FF0 /* UIImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; 11B3572F134D41A670EE9244 /* CexWithdrawNetwork.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexWithdrawNetwork.swift; sourceTree = ""; }; - 11B3573427BEEC59D4B978FA /* MultiSwapConfirmationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiSwapConfirmationViewModel.swift; sourceTree = ""; }; 11B35736BA15E54066036D54 /* BackupMnemonicWordCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupMnemonicWordCell.swift; sourceTree = ""; }; 11B3573A58F426A72669A948 /* BlockchainSettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockchainSettingsViewModel.swift; sourceTree = ""; }; 11B3573AF91C82342639A9B1 /* InputSecondaryCircleButtonWrapperView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputSecondaryCircleButtonWrapperView.swift; sourceTree = ""; }; @@ -3613,7 +3565,6 @@ 11B3576C0D8464F74D44EE92 /* BinanceWithdrawHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BinanceWithdrawHandler.swift; sourceTree = ""; }; 11B3576F224007FD4154EBE8 /* LockoutManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockoutManager.swift; sourceTree = ""; }; 11B3576FCFC9394BA37975FC /* BackupVerifyWordsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupVerifyWordsViewModel.swift; sourceTree = ""; }; - 11B35770F0C72E1CD3F99985 /* MarketTopService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketTopService.swift; sourceTree = ""; }; 11B357736B8C29DF38F5DCBA /* AlertViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertViewController.swift; sourceTree = ""; }; 11B35779E6353B98B298FF29 /* CurrentDateProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrentDateProvider.swift; sourceTree = ""; }; 11B3577A9294A3EE662872D7 /* TonIncomingTransactionRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TonIncomingTransactionRecord.swift; sourceTree = ""; }; @@ -3638,7 +3589,6 @@ 11B357D222B4819BE881E182 /* WalletTokenListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletTokenListViewController.swift; sourceTree = ""; }; 11B357D7156B86181DD0C6D4 /* TabHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabHeaderView.swift; sourceTree = ""; }; 11B357D89546EBA13B01A1ED /* TransactionsViewItemFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionsViewItemFactory.swift; sourceTree = ""; }; - 11B357E05A8AF5608ECF5D5F /* TopPlatformHeaderCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopPlatformHeaderCell.swift; sourceTree = ""; }; 11B357E9508BF369BDFF7753 /* MarkdownListItemCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownListItemCell.swift; sourceTree = ""; }; 11B357EC69F650DCA696F48D /* CexAsset.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexAsset.swift; sourceTree = ""; }; 11B357EEC98939F9C7AA3271 /* ReceiveDerivationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveDerivationViewModel.swift; sourceTree = ""; }; @@ -3646,19 +3596,16 @@ 11B357F15913DDAE69C9B0E0 /* RestoreSettingsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreSettingsManager.swift; sourceTree = ""; }; 11B357F2FAE27C9739CAE5C7 /* CoinPriceListEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinPriceListEntry.swift; sourceTree = ""; }; 11B357F4747A6B256C31EC7C /* PoolGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PoolGroup.swift; sourceTree = ""; }; - 11B357FD9D760D9671A3DF24 /* SendViewModelNew.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendViewModelNew.swift; sourceTree = ""; }; + 11B357FD9D760D9671A3DF24 /* PreSendViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreSendViewModel.swift; sourceTree = ""; }; 11B35801399EA004F5A2A1F7 /* MarkdownService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownService.swift; sourceTree = ""; }; 11B3580953728946194D1187 /* NftCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionViewController.swift; sourceTree = ""; }; 11B3580D6EDF1BB135965CC5 /* ReceiveSelectorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveSelectorViewController.swift; sourceTree = ""; }; - 11B3580ECB328146E94D4359 /* CoinTreasuriesService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinTreasuriesService.swift; sourceTree = ""; }; 11B358145A0D9F93ACBC0301 /* CreateAccountModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateAccountModule.swift; sourceTree = ""; }; - 11B3582259AD3A0C55CF6D2C /* MarketOverviewTopPlatformsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewTopPlatformsService.swift; sourceTree = ""; }; 11B35822E26E7298100CD69D /* LogRecordStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogRecordStorage.swift; sourceTree = ""; }; 11B35828C8D50D0A5B915B2A /* TransactionsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionsModule.swift; sourceTree = ""; }; 11B3583528958D290AD3CE0C /* BackupMnemonicWordsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupMnemonicWordsCell.swift; sourceTree = ""; }; 11B3583932F270503C1DF3F0 /* AdapterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdapterFactory.swift; sourceTree = ""; }; 11B35847887E070CA535F890 /* UserDefaultsStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsStorage.swift; sourceTree = ""; }; - 11B3584888F2DB8CCFAA90DF /* MarketCategoryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketCategoryViewController.swift; sourceTree = ""; }; 11B3584D2C3754A605975D6C /* SecondaryCircleButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecondaryCircleButtonStyle.swift; sourceTree = ""; }; 11B35850DF16D11D45C44A60 /* CexDepositNetworkSelectService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexDepositNetworkSelectService.swift; sourceTree = ""; }; 11B358556C8FC5368E14D81E /* AccountRecordStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountRecordStorage.swift; sourceTree = ""; }; @@ -3676,13 +3623,11 @@ 11B3589893A8995F86F08B1C /* BtcBlockchainSettingsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BtcBlockchainSettingsModule.swift; sourceTree = ""; }; 11B3589B8D488ED1F6912287 /* SelectorButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorButtonStyle.swift; sourceTree = ""; }; 11B358A102692DA01F91413D /* BaseUniswapV2MultiSwapProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseUniswapV2MultiSwapProvider.swift; sourceTree = ""; }; - 11B358A22655004017228F65 /* CoinRankModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinRankModule.swift; sourceTree = ""; }; 11B358A294479046C42D2E6B /* PublicKeysService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PublicKeysService.swift; sourceTree = ""; }; 11B358A78367D108DD529C1B /* MarkdownHeader3Cell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownHeader3Cell.swift; sourceTree = ""; }; 11B358B22BAF021E8FA028BF /* RecipientAddressCautionCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecipientAddressCautionCell.swift; sourceTree = ""; }; 11B358B8D6DFEAEDE84D53DE /* ReceiveSelectCoinViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveSelectCoinViewController.swift; sourceTree = ""; }; 11B358C2B0271F74A958BD90 /* NftAssetMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAssetMetadata.swift; sourceTree = ""; }; - 11B358C7505D0DE60CD03B22 /* MarketAdvancedSearchResultService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketAdvancedSearchResultService.swift; sourceTree = ""; }; 11B358C7DF6F82875527031E /* RestoreSelectModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreSelectModule.swift; sourceTree = ""; }; 11B358CA18471A93188933B4 /* DashAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashAdapter.swift; sourceTree = ""; }; 11B358D98E1FBA6909D352DA /* FaqRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaqRepository.swift; sourceTree = ""; }; @@ -3706,19 +3651,16 @@ 11B35935EF1B2237E0289669 /* BaseTransactionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTransactionsViewModel.swift; sourceTree = ""; }; 11B3593FBD158050C9FEF6B9 /* Misc.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Misc.swift; sourceTree = ""; }; 11B3594CBF3EA39A848D22EB /* EditDuressPasscodeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditDuressPasscodeViewModel.swift; sourceTree = ""; }; - 11B359575A4E090B236E84C7 /* CoinRankViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinRankViewModel.swift; sourceTree = ""; }; 11B35957968B4D79EC406D4D /* BottomSheetViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BottomSheetViewController.swift; sourceTree = ""; }; 11B3595BAA550B6BEC8C3F72 /* LaunchScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LaunchScreen.swift; sourceTree = ""; }; 11B3595BAB29E195A1317DD1 /* Blockchain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Blockchain.swift; sourceTree = ""; }; 11B35962622F74F89FD32D2B /* EvmAddressViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EvmAddressViewModel.swift; sourceTree = ""; }; 11B359636E1AA1BC72CF7B11 /* PoolProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PoolProvider.swift; sourceTree = ""; }; - 11B3596381A93F3A3D2575D6 /* MarketListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketListViewController.swift; sourceTree = ""; }; 11B35968D12AAAC828AFE955 /* PrimaryButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrimaryButtonStyle.swift; sourceTree = ""; }; 11B35968F5DE9FDA6EC26FCD /* TextInputCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextInputCell.swift; sourceTree = ""; }; 11B359697FC3E92D4111ED5D /* String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 11B3596ECFEECF17ADB3BAEF /* RestoreService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreService.swift; sourceTree = ""; }; 11B359706510588C2E7D448B /* BaseCurrencySettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseCurrencySettingsViewModel.swift; sourceTree = ""; }; - 11B359715B07FD5316D72A07 /* MarketTopPairsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketTopPairsViewModel.swift; sourceTree = ""; }; 11B35977188C93500A2CC6B0 /* InputCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputCell.swift; sourceTree = ""; }; 11B3597A2B0B529BE97F85C8 /* WalletHeaderCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletHeaderCell.swift; sourceTree = ""; }; 11B3597E2B288ECD850C1DFE /* PasteInputCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasteInputCell.swift; sourceTree = ""; }; @@ -3726,7 +3668,6 @@ 11B359852B313E849499BC19 /* NftAssetRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAssetRecord.swift; sourceTree = ""; }; 11B3598A8D7D1A8D5E17BE15 /* RestoreMnemonicViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreMnemonicViewModel.swift; sourceTree = ""; }; 11B3598D3D2FCEB51E0A0760 /* TextDropDownAndSettingsHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextDropDownAndSettingsHeaderView.swift; sourceTree = ""; }; - 11B3598FB2653DB1DC1429CA /* MarketAdvancedSearchResultModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketAdvancedSearchResultModule.swift; sourceTree = ""; }; 11B35995983865EED8599DB0 /* MultiSwapTokenSelectView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiSwapTokenSelectView.swift; sourceTree = ""; }; 11B35995DE2AAD2186441E38 /* CoinMajorHoldersModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinMajorHoldersModule.swift; sourceTree = ""; }; 11B35995E0D358AC4DA2FA74 /* RestoreBinanceModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreBinanceModule.swift; sourceTree = ""; }; @@ -3740,7 +3681,7 @@ 11B359BBFCD82C3C6DC06F96 /* FeeRateProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeeRateProvider.swift; sourceTree = ""; }; 11B359C5AF7EE92A5756CCFF /* CoinManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinManager.swift; sourceTree = ""; }; 11B359C62F476065C11EE049 /* TextDropDownAndSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextDropDownAndSettingsView.swift; sourceTree = ""; }; - 11B359C9E2EFD391FB848618 /* SendConfirmField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendConfirmField.swift; sourceTree = ""; }; + 11B359C9E2EFD391FB848618 /* SendField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendField.swift; sourceTree = ""; }; 11B359CC2E1E7853CD1547B3 /* LogoHeaderCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogoHeaderCell.swift; sourceTree = ""; }; 11B359CE35C7483CCB956D13 /* CoinAnalyticsHoldersCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinAnalyticsHoldersCell.swift; sourceTree = ""; }; 11B359D1A38D53951CEE6F84 /* ReceiveViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveViewController.swift; sourceTree = ""; }; @@ -3759,13 +3700,10 @@ 11B359FE71F5DE6AAD2BA3D8 /* NftMetadataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftMetadataManager.swift; sourceTree = ""; }; 11B359FF2DB6F840D867FD2F /* BottomSheetModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BottomSheetModule.swift; sourceTree = ""; }; 11B35A05B93CB243B6404C4A /* WelcomeTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeTextView.swift; sourceTree = ""; }; - 11B35A0AF4D03160AF66D1D9 /* MarketOverviewCategoryService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewCategoryService.swift; sourceTree = ""; }; 11B35A0F912218FEC2A196C0 /* CoinInvestorsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinInvestorsViewController.swift; sourceTree = ""; }; 11B35A10404D5E085E482CC7 /* SetPasscodeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetPasscodeView.swift; sourceTree = ""; }; - 11B35A12A3B7218DF597C172 /* MarketAdvancedSearchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketAdvancedSearchViewController.swift; sourceTree = ""; }; 11B35A1AE56A94BEB52AC4D1 /* StorageMigrator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageMigrator.swift; sourceTree = ""; }; 11B35A1C200EC15159154E3F /* ShortcutInputCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShortcutInputCell.swift; sourceTree = ""; }; - 11B35A1E2AE3DC240D5B785E /* CoinRankViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinRankViewController.swift; sourceTree = ""; }; 11B35A296048CDD27A26FE9E /* EvmAccountManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EvmAccountManager.swift; sourceTree = ""; }; 11B35A296A67CE158347A785 /* SendTonService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendTonService.swift; sourceTree = ""; }; 11B35A2C34C3D62CCA5BFFB5 /* GuidesRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GuidesRepository.swift; sourceTree = ""; }; @@ -3796,21 +3734,18 @@ 11B35AA43C4832521D428799 /* ListSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListSection.swift; sourceTree = ""; }; 11B35AAAC675987369F2DA1B /* BinanceAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BinanceAdapter.swift; sourceTree = ""; }; 11B35AAE4114A56DF13ECF0F /* StackViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackViewCell.swift; sourceTree = ""; }; - 11B35AB1D0CE5D8ECE7DDF65 /* MarketOverviewTopPairsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewTopPairsViewModel.swift; sourceTree = ""; }; 11B35AB755E196D299B81BFB /* CoinMajorHoldersViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinMajorHoldersViewController.swift; sourceTree = ""; }; 11B35ABC3E6C990E3BFA0A7B /* CexWithdrawViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexWithdrawViewController.swift; sourceTree = ""; }; 11B35ABF8159065957CD3EF8 /* SubscriptionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionManager.swift; sourceTree = ""; }; 11B35AC2D01DF06DC50EAC6A /* HighlightedTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HighlightedTextView.swift; sourceTree = ""; }; 11B35AD211091A7C8619CEA2 /* CexAssetRecordStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexAssetRecordStorage.swift; sourceTree = ""; }; - 11B35AD24681D0A122E6A3C5 /* SendDataNew.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendDataNew.swift; sourceTree = ""; }; - 11B35ADBD038830223A8375D /* CoinPriceListMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinPriceListMode.swift; sourceTree = ""; }; + 11B35AD24681D0A122E6A3C5 /* SendData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendData.swift; sourceTree = ""; }; 11B35ADF518A2F98FF673B4B /* CoinAuditsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinAuditsViewModel.swift; sourceTree = ""; }; - 11B35ADF9BC4D149F86F23E4 /* MarketFilteredListService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketFilteredListService.swift; sourceTree = ""; }; 11B35AE5785634316A1A5DA8 /* WalletBlockchainElementService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletBlockchainElementService.swift; sourceTree = ""; }; 11B35AFE2C95FF73F75652D8 /* ChartView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartView.swift; sourceTree = ""; }; 11B35B0879F715C0777919AA /* WatchlistWidget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchlistWidget.swift; sourceTree = ""; }; 11B35B0A0EC524FBC663BEA5 /* CexDepositViewItemFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexDepositViewItemFactory.swift; sourceTree = ""; }; - 11B35B0F9BC5C4CDBC5B041D /* SendEvmHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendEvmHandler.swift; sourceTree = ""; }; + 11B35B0F9BC5C4CDBC5B041D /* EvmSendHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EvmSendHandler.swift; sourceTree = ""; }; 11B35B106BD8E4DBD67B7700 /* BaseTransactionsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTransactionsService.swift; sourceTree = ""; }; 11B35B109B4F60753BEC5078 /* ReceiveAddressService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveAddressService.swift; sourceTree = ""; }; 11B35B143F359BE790EC392B /* EvmAddressService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EvmAddressService.swift; sourceTree = ""; }; @@ -3840,14 +3775,13 @@ 11B35B6F5261FF3F9ECBC02E /* PasscodeLockState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasscodeLockState.swift; sourceTree = ""; }; 11B35B70808A7D2484859EFD /* FaqViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaqViewController.swift; sourceTree = ""; }; 11B35B7E25636164A4B65CEC /* InputRowModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputRowModifier.swift; sourceTree = ""; }; - 11B35B91B5EAF2E193FDC04E /* SendConfirmationNewView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendConfirmationNewView.swift; sourceTree = ""; }; + 11B35B91B5EAF2E193FDC04E /* SendView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendView.swift; sourceTree = ""; }; 11B35B968B299A67FC7FEAE3 /* WalletConnectManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectManager.swift; sourceTree = ""; }; 11B35B96D2BC5994AC8EC794 /* MainModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainModule.swift; sourceTree = ""; }; 11B35B9BA734A13A0ADC507E /* TransactionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionsViewModel.swift; sourceTree = ""; }; 11B35B9F4421EE65B8B09370 /* StatRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatRecord.swift; sourceTree = ""; }; 11B35BAA4EA85B4A3A173498 /* RowButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowButtonStyle.swift; sourceTree = ""; }; 11B35BAABF1F6A9EFF769C47 /* NftCollectionOverviewViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionOverviewViewController.swift; sourceTree = ""; }; - 11B35BB370AE2C896BB9F877 /* TopPlatformViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopPlatformViewController.swift; sourceTree = ""; }; 11B35BB3B8928864A742C83E /* ReceiveAddressModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveAddressModule.swift; sourceTree = ""; }; 11B35BB7206DA0EDBB43C814 /* BalanceCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalanceCell.swift; sourceTree = ""; }; 11B35BBC5BBCC258824A80F3 /* CexDepositNetworkSelectModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexDepositNetworkSelectModule.swift; sourceTree = ""; }; @@ -3866,14 +3800,13 @@ 11B35BEA44ADC0D844330FB7 /* WCSignEthereumTransactionRequestViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WCSignEthereumTransactionRequestViewController.swift; sourceTree = ""; }; 11B35BEEB24CDB82D3F4E7C0 /* NftAddressMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAddressMetadata.swift; sourceTree = ""; }; 11B35BEEC0AB0B09C7E4209A /* LaunchScreenManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LaunchScreenManager.swift; sourceTree = ""; }; - 11B35BEF6F80BDF166173819 /* SendConfirmationNewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendConfirmationNewViewModel.swift; sourceTree = ""; }; + 11B35BEF6F80BDF166173819 /* SendViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendViewModel.swift; sourceTree = ""; }; 11B35BF766EAC97E74CD620D /* AlertTitleCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertTitleCell.swift; sourceTree = ""; }; 11B35BFA410F267D3A9B8E43 /* KeychainStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = ""; }; 11B35BFAAAE3B1357B5CE944 /* CoinPageService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinPageService.swift; sourceTree = ""; }; 11B35BFB11BC4B9FF7D53B8D /* CoinMarketsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinMarketsView.swift; sourceTree = ""; }; 11B35C09B59EF5DEB6D7EB07 /* FaqViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaqViewModel.swift; sourceTree = ""; }; 11B35C13937F82D36C823205 /* MainBadgeService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainBadgeService.swift; sourceTree = ""; }; - 11B35C19608F6A314CF1F0C5 /* MarketAdvancedSearchService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketAdvancedSearchService.swift; sourceTree = ""; }; 11B35C1DF4F98D814CFE3951 /* OpenSeaNftProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenSeaNftProvider.swift; sourceTree = ""; }; 11B35C227EDC2D4ED188A0FC /* CexDepositNetworkSelectViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexDepositNetworkSelectViewController.swift; sourceTree = ""; }; 11B35C2397749C5654830540 /* EvmNftAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EvmNftAdapter.swift; sourceTree = ""; }; @@ -3886,11 +3819,9 @@ 11B35C4D6F474C2EB3687EB4 /* AccountFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountFactory.swift; sourceTree = ""; }; 11B35C4F17D4CC8E89F7DC3B /* NftCollectionOverviewService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionOverviewService.swift; sourceTree = ""; }; 11B35C4FFB99D10A8F343E9C /* LanguageManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LanguageManager.swift; sourceTree = ""; }; - 11B35C5CA7497C540FFC5D39 /* MarketTopPairsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketTopPairsViewController.swift; sourceTree = ""; }; 11B35C6498078B1AFF406256 /* CoinMajorHoldersService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinMajorHoldersService.swift; sourceTree = ""; }; 11B35C6DF4DEE25B8B4B2E28 /* TonAddressParserItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TonAddressParserItem.swift; sourceTree = ""; }; 11B35C6E5282F55B88042F8D /* WalletTokenListViewItemFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletTokenListViewItemFactory.swift; sourceTree = ""; }; - 11B35C7B8BA65E9AA3BB7AFB /* FavoriteCoinRecordStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteCoinRecordStorage.swift; sourceTree = ""; }; 11B35C7CCC41913AA8D36CBC /* WalletCexElementService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletCexElementService.swift; sourceTree = ""; }; 11B35C7DCF9B7894F7600623 /* LaunchModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LaunchModule.swift; sourceTree = ""; }; 11B35C7F043B6C41E53D43BC /* SendEvmTransactionService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendEvmTransactionService.swift; sourceTree = ""; }; @@ -3946,12 +3877,9 @@ 11B35DA9FF23D110A042EDD6 /* NftMetadataSyncer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftMetadataSyncer.swift; sourceTree = ""; }; 11B35DB358405198CF67F11D /* MnemonicInputCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MnemonicInputCell.swift; sourceTree = ""; }; 11B35DB5445B83B51C69D7AE /* TokenTransactionsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenTransactionsService.swift; sourceTree = ""; }; - 11B35DB992C240A4CF24938A /* MarketCategoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketCategoryView.swift; sourceTree = ""; }; 11B35DBDADDA8D4F9D88C7AA /* RecipientAddressInputCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecipientAddressInputCell.swift; sourceTree = ""; }; 11B35DC48EEBE1160676B269 /* ManageAccountService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageAccountService.swift; sourceTree = ""; }; 11B35DC72F0D8DBBCCE2F988 /* MarkdownVisitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownVisitor.swift; sourceTree = ""; }; - 11B35DCB7125B0046592414B /* MarketAdvancedSearchModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketAdvancedSearchModule.swift; sourceTree = ""; }; - 11B35DCCC2D8CD00EF6A9A77 /* MarketOverviewMetricsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewMetricsCell.swift; sourceTree = ""; }; 11B35DCDDACF2BB1E0748ABB /* RestoreSelectViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreSelectViewModel.swift; sourceTree = ""; }; 11B35DDC338BFE2832C07360 /* TransactionTokenSelectView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionTokenSelectView.swift; sourceTree = ""; }; 11B35DDE879F1628BB2CE523 /* WidgetProd.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = WidgetProd.entitlements; sourceTree = ""; }; @@ -3963,7 +3891,7 @@ 11B35DFA83DA24A00D73EA7D /* RestoreSettingRecord_v_0_25.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreSettingRecord_v_0_25.swift; sourceTree = ""; }; 11B35DFBFBF34277E7FC3325 /* ActivateSubscriptionViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivateSubscriptionViewModel.swift; sourceTree = ""; }; 11B35E1B9C559545FC3E6226 /* ReceiveTokenViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveTokenViewModel.swift; sourceTree = ""; }; - 11B35E255F6CA21FFA9E6B42 /* FavoriteCoinRecord_v_0_22.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteCoinRecord_v_0_22.swift; sourceTree = ""; }; + 11B35E255F6CA21FFA9E6B42 /* FavoriteCoinRecord_v_0_38.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteCoinRecord_v_0_38.swift; sourceTree = ""; }; 11B35E298D53B8A2C2684119 /* AppWidgetConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppWidgetConstants.swift; sourceTree = ""; }; 11B35E2ACF02E2C35EFAE9FA /* NftMetadataService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftMetadataService.swift; sourceTree = ""; }; 11B35E2D539ACED30C947F2C /* RestoreBinanceService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreBinanceService.swift; sourceTree = ""; }; @@ -3986,7 +3914,6 @@ 11B35E9B9C7A88B7584507DF /* AccountRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountRecord.swift; sourceTree = ""; }; 11B35E9E1D262629CD843F7E /* ManageAccountModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageAccountModule.swift; sourceTree = ""; }; 11B35EA2F51B9257D036D3E6 /* CoinInvestorsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinInvestorsModule.swift; sourceTree = ""; }; - 11B35EAECC2236EB081241B4 /* SendAmountViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendAmountViewModel.swift; sourceTree = ""; }; 11B35EB9BA551F2F1AF7739D /* TermsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TermsManager.swift; sourceTree = ""; }; 11B35EBD933DD3C9E72F1CA8 /* EnabledWallet_v_0_25.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnabledWallet_v_0_25.swift; sourceTree = ""; }; 11B35EC03BB5316524050518 /* CexWithdrawNetworkRaw.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexWithdrawNetworkRaw.swift; sourceTree = ""; }; @@ -4005,7 +3932,6 @@ 11B35EFB45ECC2D403CA6C89 /* ValueFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueFormatter.swift; sourceTree = ""; }; 11B35F007444A766AF8CD20D /* EvmLabelStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EvmLabelStorage.swift; sourceTree = ""; }; 11B35F0240E56EF4591D1C8F /* WalletTokenListService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletTokenListService.swift; sourceTree = ""; }; - 11B35F08C14B3F0D978E2E7F /* CoinTreasuriesModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinTreasuriesModule.swift; sourceTree = ""; }; 11B35F0C950836EA4166CEC5 /* SingleCoinPriceProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleCoinPriceProvider.swift; sourceTree = ""; }; 11B35F13BAFE57D363B9684F /* EvmLabelManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EvmLabelManager.swift; sourceTree = ""; }; 11B35F24FEE8233477BCDA18 /* CexWithdrawConfirmModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexWithdrawConfirmModule.swift; sourceTree = ""; }; @@ -4027,8 +3953,6 @@ 11B35F98E89F83A30870F404 /* ActiveAccount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActiveAccount.swift; sourceTree = ""; }; 11B35F99E093B7DDB24D39C9 /* SetPasscodeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetPasscodeViewModel.swift; sourceTree = ""; }; 11B35F9B75F6663FAFCA3177 /* TonAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TonAdapter.swift; sourceTree = ""; }; - 11B35F9BA41AC15436A4B977 /* DropdownSortHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropdownSortHeaderView.swift; sourceTree = ""; }; - 11B35F9DA79410E7B9C1B0F8 /* MarketTopModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketTopModule.swift; sourceTree = ""; }; 11B35FA360A91FDE3EB0B85C /* RateAppManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RateAppManager.swift; sourceTree = ""; }; 11B35FA70D9570CB2708E1CA /* BackupVerifyWordsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupVerifyWordsService.swift; sourceTree = ""; }; 11B35FA71AA140CD3764C6BC /* SyncerStateStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncerStateStorage.swift; sourceTree = ""; }; @@ -4041,17 +3965,13 @@ 11B35FF02BBEDAEF446D0610 /* ModuleUnlockView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModuleUnlockView.swift; sourceTree = ""; }; 11B35FF3390A7BB8E040620D /* PlaceholderViewNew.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaceholderViewNew.swift; sourceTree = ""; }; 11B35FF539B93A4C61AD1D00 /* CoinInvestorsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinInvestorsViewModel.swift; sourceTree = ""; }; - 179E7048A730489634E27043 /* FavoriteCoinRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteCoinRecord.swift; sourceTree = ""; }; 1A56404C1C16B85434117DB7 /* AppStatusModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppStatusModule.swift; sourceTree = ""; }; 1A5640528EFD15137E218EA3 /* MainSettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainSettingsViewModel.swift; sourceTree = ""; }; - 1A5640B4F6298D9F326C5EDE /* MarketOverviewTopCoinsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewTopCoinsViewModel.swift; sourceTree = ""; }; 1A5641572B6E46E18B52A6A9 /* SecuritySettingsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecuritySettingsModule.swift; sourceTree = ""; }; 1A5641679DC88BE355F0F3A0 /* Development.template.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Development.template.xcconfig; sourceTree = ""; }; 1A56417C27A95B429D9F2912 /* MarkdownContentProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownContentProvider.swift; sourceTree = ""; }; - 1A5641A724199908970CFB54 /* MarketNftTopCollectionsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketNftTopCollectionsViewController.swift; sourceTree = ""; }; 1A5641CDB00EF52E18BF70F3 /* AppVersionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppVersionManager.swift; sourceTree = ""; }; 1A5641E505FE004F601943C4 /* PerformanceTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PerformanceTableViewCell.swift; sourceTree = ""; }; - 1A564206FEC56546760B9BEA /* MarketOverviewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewViewModel.swift; sourceTree = ""; }; 1A564215DD6F0D54C1F6C4F7 /* AcademyMarkdownConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AcademyMarkdownConfig.swift; sourceTree = ""; }; 1A56422C196B48931CDE1445 /* SendTransactionError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendTransactionError.swift; sourceTree = ""; }; 1A564293D88587642800717B /* FilterHeaderCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterHeaderCell.swift; sourceTree = ""; }; @@ -4060,7 +3980,6 @@ 1A56432704A2E7A9BE78497B /* ReleaseNotesMarkdownConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReleaseNotesMarkdownConfig.swift; sourceTree = ""; }; 1A56433D5D39CCA995F97777 /* Development.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Development.xcconfig; sourceTree = ""; }; 1A564370A637B30D34776F2A /* Production.template.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = file.template; path = Production.template.xcconfig; sourceTree = ""; }; - 1A5643A672A508BC4CBCABDD /* NftCollectionsMultiSortHeaderViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionsMultiSortHeaderViewModel.swift; sourceTree = ""; }; 1A5644074A3ACB1DFB63FF92 /* ReleaseNotesService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReleaseNotesService.swift; sourceTree = ""; }; 1A56443BF752CB6537E45F5A /* BlockchainSettingsStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockchainSettingsStorage.swift; sourceTree = ""; }; 1A56444EB2F32DB662981653 /* TitledHighlightedDescriptionCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TitledHighlightedDescriptionCell.swift; sourceTree = ""; }; @@ -4068,15 +3987,10 @@ 1A56446DB62F52AC4C3C2C30 /* SecuritySettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecuritySettingsViewModel.swift; sourceTree = ""; }; 1A56447C12D91108517ED217 /* UIDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIDevice.swift; sourceTree = ""; }; 1A5644A21F9FEC4E2A7B0860 /* PlaceholderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaceholderView.swift; sourceTree = ""; }; - 1A5644E4694DBB0E6E0B10CC /* BaseMarketOverviewTopListDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseMarketOverviewTopListDataSource.swift; sourceTree = ""; }; 1A564504718E7D19F379A9F7 /* AdapterError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdapterError.swift; sourceTree = ""; }; 1A56450DA6DF97C9E1FFE987 /* BitcoinBaseAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BitcoinBaseAdapter.swift; sourceTree = ""; }; - 1A564555A67E4DC1DC935A04 /* MarketListWatchViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketListWatchViewModel.swift; sourceTree = ""; }; 1A564580B3F739DAC59C623F /* DeepLinkManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeepLinkManager.swift; sourceTree = ""; }; 1A56458C7C29504755A09E00 /* LaunchService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LaunchService.swift; sourceTree = ""; }; - 1A56459D85D4859D8A0F4D5A /* MarketTopPlatformsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketTopPlatformsModule.swift; sourceTree = ""; }; - 1A5645B1C5FD344967B1F4B7 /* MarketOverviewCategoryCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewCategoryCell.swift; sourceTree = ""; }; - 1A5646218714BA81DE9B5631 /* MarketOverviewTopCoinsDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewTopCoinsDataSource.swift; sourceTree = ""; }; 1A5646483957D74946973BEE /* PerformanceContentCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PerformanceContentCollectionViewCell.swift; sourceTree = ""; }; 1A56469A2F3EAAEDECFB4034 /* JailbreakService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JailbreakService.swift; sourceTree = ""; }; 1A5646B5D68A302515565030 /* BalanceErrorService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalanceErrorService.swift; sourceTree = ""; }; @@ -4085,65 +3999,46 @@ 1A5646D49060C3EFF06D0479 /* App.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; 1A564702FB246F315983743E /* BalanceErrorViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalanceErrorViewModel.swift; sourceTree = ""; }; 1A564730E8F235240D62124B /* HighlightedDescriptionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HighlightedDescriptionView.swift; sourceTree = ""; }; - 1A56477F6FC71270AD53A3AE /* MarketOverviewTopPlatformsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewTopPlatformsViewModel.swift; sourceTree = ""; }; 1A5647ACB8A65C250F62E07D /* AdapterState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdapterState.swift; sourceTree = ""; }; 1A5647AD7481B36F20D4DDF9 /* MainSettingsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainSettingsModule.swift; sourceTree = ""; }; - 1A5647FA18CC69113ECB6581 /* MarketOverviewGlobalDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewGlobalDataSource.swift; sourceTree = ""; }; 1A564814721244F4D4D87557 /* ReachabilityViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReachabilityViewModel.swift; sourceTree = ""; }; 1A56485B094980B68B0A86AE /* ReadMoreTextCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadMoreTextCell.swift; sourceTree = ""; }; 1A564872B7C5F76D8CE55A8B /* BinanceAddressParserItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BinanceAddressParserItem.swift; sourceTree = ""; }; 1A564879AD72301AAB78F8F5 /* MainSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainSettingsViewController.swift; sourceTree = ""; }; 1A5648911028181BB1462CFE /* TraitCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TraitCell.swift; sourceTree = ""; }; 1A56489DE231CDDCA75CAEB3 /* AppError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; }; - 1A5648E7534C2E7F16C4A2D4 /* MarketTopPlatformsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketTopPlatformsService.swift; sourceTree = ""; }; 1A5648F3AB070B0ACB98C7EE /* Production.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Production.xcconfig; sourceTree = ""; }; 1A564920282E84A6E7EE05EB /* SendEthereumErrorCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendEthereumErrorCell.swift; sourceTree = ""; }; - 1A564995DE20E52E8E0F1E6A /* MarketTopPlatformsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketTopPlatformsViewModel.swift; sourceTree = ""; }; - 1A5649C0BD100768C726B4FB /* TopPlatformService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopPlatformService.swift; sourceTree = ""; }; - 1A5649C48B3AABC56D2512ED /* MarketOverviewGlobalViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewGlobalViewModel.swift; sourceTree = ""; }; 1A5649C6FFC694CD18A8B39A /* AddressUri.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddressUri.swift; sourceTree = ""; }; - 1A5649E41FE690AF0A712426 /* MarketNftTopCollectionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketNftTopCollectionsViewModel.swift; sourceTree = ""; }; 1A564A144576DB93334E1682 /* ScanQrViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScanQrViewController.swift; sourceTree = ""; }; 1A564A1B86DF22E86F0BB442 /* TopPlatformMarketCapFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopPlatformMarketCapFetcher.swift; sourceTree = ""; }; 1A564A55E5866D6081EA6F69 /* EnabledWallet_v_0_13.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnabledWallet_v_0_13.swift; sourceTree = ""; }; 1A564A6A5C4F3080690AE93F /* ConvertedError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConvertedError.swift; sourceTree = ""; }; - 1A564A6D161EAD22626332C1 /* MarketOverviewCategoryDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewCategoryDataSource.swift; sourceTree = ""; }; 1A564AB0B646F7A92DD188F2 /* BalanceErrorModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalanceErrorModule.swift; sourceTree = ""; }; - 1A564ADD13E597F423249CA3 /* MarketOverviewCategoryViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewCategoryViewModel.swift; sourceTree = ""; }; 1A564AFF2709E27114985A8D /* AppVersionStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppVersionStorage.swift; sourceTree = ""; }; 1A564B1C051AF2C87C670563 /* FilterHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterHeaderView.swift; sourceTree = ""; }; - 1A564B44985D1169593F202C /* MarketOverviewTopPlatformsDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewTopPlatformsDataSource.swift; sourceTree = ""; }; 1A564B73ABBEFC58A13D501E /* PerformanceSideCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PerformanceSideCollectionViewCell.swift; sourceTree = ""; }; 1A564B7C35C5B235D2BBAC2C /* AppVersion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppVersion.swift; sourceTree = ""; }; 1A564BB88BF3ED779F8C21DC /* BlurManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlurManager.swift; sourceTree = ""; }; 1A564BCC9DD29DB5455669A5 /* HighlightedDescriptionBaseView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HighlightedDescriptionBaseView.swift; sourceTree = ""; }; - 1A564BDA5600859626D99BB4 /* TopPlatformModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopPlatformModule.swift; sourceTree = ""; }; 1A564BE241BFC5DD59D0FB7C /* BtcRestoreMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BtcRestoreMode.swift; sourceTree = ""; }; 1A564BF724C69C237E309C07 /* BitcoinCashAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BitcoinCashAdapter.swift; sourceTree = ""; }; 1A564C46FB773A67E29D9D32 /* BlockchainSettingRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockchainSettingRecord.swift; sourceTree = ""; }; 1A564C4B0D13BC7214419A3E /* GradientClippingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GradientClippingView.swift; sourceTree = ""; }; - 1A564C4DB4A57CCF2C5EFB78 /* MarketTopPlatformsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketTopPlatformsViewController.swift; sourceTree = ""; }; - 1A564C5CC7EC339C3113869D /* MarketListTopPlatformDecorator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketListTopPlatformDecorator.swift; sourceTree = ""; }; 1A564CB28708314AE0A69424 /* TitledHighlightedDescriptionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TitledHighlightedDescriptionView.swift; sourceTree = ""; }; - 1A564CC5878BF33B8CE1F339 /* MarketListNftCollectionDecorator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketListNftCollectionDecorator.swift; sourceTree = ""; }; 1A564CE10FD5FEC14EF38BD8 /* PrivacyPolicyViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivacyPolicyViewController.swift; sourceTree = ""; }; 1A564CE56CEC73B78C9DB6B5 /* BalanceErrorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalanceErrorViewController.swift; sourceTree = ""; }; 1A564CF35E7A07E96B704ADA /* PlaceholderViewModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaceholderViewModule.swift; sourceTree = ""; }; - 1A564D12426BCA027C67377E /* MarketNftTopCollectionsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketNftTopCollectionsService.swift; sourceTree = ""; }; 1A564D2011D7A4E80B2B7B92 /* JailbreakTestManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JailbreakTestManager.swift; sourceTree = ""; }; 1A564D5E55767404ED6C88E0 /* Decimal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Decimal.swift; sourceTree = ""; }; - 1A564D661F3AE561D7FE9FAA /* MarketOverviewTopCoinsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewTopCoinsService.swift; sourceTree = ""; }; 1A564D73251DBFA0CFE34D12 /* MainSettingsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainSettingsService.swift; sourceTree = ""; }; 1A564D7B1F36B1C4AB4CBF3A /* BasePerformanceCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasePerformanceCollectionViewCell.swift; sourceTree = ""; }; 1A564D8AD6D160F27C021F48 /* ReachabilityService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReachabilityService.swift; sourceTree = ""; }; - 1A564D8F8A8A63BC9BEAAD56 /* TopPlatformsMultiSortHeaderViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopPlatformsMultiSortHeaderViewModel.swift; sourceTree = ""; }; 1A564DEB9782FF55EFFD8CCA /* DeepLinkService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeepLinkService.swift; sourceTree = ""; }; - 1A564E5282C3C22DA85141AF /* MarketNftTopCollectionsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketNftTopCollectionsModule.swift; sourceTree = ""; }; 1A564E6A6B7E18C287A1D77D /* TransactionDataSortMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionDataSortMode.swift; sourceTree = ""; }; 1A564E86CCEAD0F9956664D4 /* ThemeActionSheetController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeActionSheetController.swift; sourceTree = ""; }; 1A564EA6F1CCDF88F78351F8 /* ThemeSearchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeSearchViewController.swift; sourceTree = ""; }; 1A564EA8CD67010BFFC57AAB /* DashAddressParserItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashAddressParserItem.swift; sourceTree = ""; }; - 1A564EDF0FD6A1D1575D1EFB /* MarketOverviewGlobalService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewGlobalService.swift; sourceTree = ""; }; 1A564FF31C5E879781A2D5E0 /* TraitsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TraitsCell.swift; sourceTree = ""; }; 2FA5D02D8F5C2AE32C6FF923 /* KitCleaner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KitCleaner.swift; sourceTree = ""; }; 2FA5D0F4DBA8FC1F917AA95B /* SendSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendSettingsViewController.swift; sourceTree = ""; }; @@ -4214,7 +4109,6 @@ 2FA5DF48D44BB409FB94CED9 /* UnknownSwapTransactionRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnknownSwapTransactionRecord.swift; sourceTree = ""; }; 2FA5DF5B8B69876A526CCDDD /* BinanceChainOutgoingTransactionRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BinanceChainOutgoingTransactionRecord.swift; sourceTree = ""; }; 2FA5DFE7C4A198C235B2EAB5 /* TransactionAdapterManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAdapterManager.swift; sourceTree = ""; }; - 3AB682BC25BADD97002197A5 /* MarketOverviewModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewModule.swift; sourceTree = ""; }; 3AF9F617253EF555000626A8 /* ZcashTransactionPool.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZcashTransactionPool.swift; sourceTree = ""; }; 3AF9F618253EF555000626A8 /* ZcashAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZcashAdapter.swift; sourceTree = ""; }; 3AF9F619253EF555000626A8 /* ZcashTransactionWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZcashTransactionWrapper.swift; sourceTree = ""; }; @@ -4234,15 +4128,11 @@ 50EED191218822FA00E200AD /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = ""; }; 50EED192218822FA00E200AD /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; 58AAA024EB2E850DD3277419 /* SwapStepCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapStepCell.swift; sourceTree = ""; }; - 58AAA02D981360FF0CC50A19 /* MarketGlobalMetricViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketGlobalMetricViewController.swift; sourceTree = ""; }; 58AAA0A9EA8A2210522F38EE /* MarketGlobalFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketGlobalFetcher.swift; sourceTree = ""; }; 58AAA0B8ECE5854FAB9362AC /* CoinOverviewViewItemFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinOverviewViewItemFactory.swift; sourceTree = ""; }; 58AAA0D499D632E44F7BE172 /* OneInchSettingsDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OneInchSettingsDataSource.swift; sourceTree = ""; }; - 58AAA0ED0FCFACF791EC865C /* FavoritesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoritesManager.swift; sourceTree = ""; }; - 58AAA11651E3CE29A461BF42 /* MarketPostViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketPostViewModel.swift; sourceTree = ""; }; 58AAA1233617C06AC975285A /* SwapApproveAmountView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapApproveAmountView.swift; sourceTree = ""; }; 58AAA13C7C5B258310BA61AF /* CoinChartService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinChartService.swift; sourceTree = ""; }; - 58AAA15F4FA7B9EC091EDFF3 /* MarketSingleSortHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketSingleSortHeaderView.swift; sourceTree = ""; }; 58AAA16C7E337511638808E5 /* DebugInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugInteractor.swift; sourceTree = ""; }; 58AAA16E4AB334B67FFD891A /* LockDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockDelegate.swift; sourceTree = ""; }; 58AAA18F732998DCAA76E47C /* UniswapSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UniswapSettings.swift; sourceTree = ""; }; @@ -4251,9 +4141,7 @@ 58AAA246206AF337B91809BE /* BitcoinAddressParserItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BitcoinAddressParserItem.swift; sourceTree = ""; }; 58AAA24B6EB7103CB072A53D /* FeeSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeeSlider.swift; sourceTree = ""; }; 58AAA251C8ADF1AE43EAF65F /* SwapApproveModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapApproveModule.swift; sourceTree = ""; }; - 58AAA2521E8F8845D96AB865 /* MarketOverviewHeaderCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewHeaderCell.swift; sourceTree = ""; }; 58AAA25A23A8EBB537DD56C2 /* AddressResolutionProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddressResolutionProvider.swift; sourceTree = ""; }; - 58AAA263DAB58FD63E6A9351 /* MarketPostViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketPostViewController.swift; sourceTree = ""; }; 58AAA27BACBAFC7A19DC36B6 /* UniswapSettingsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UniswapSettingsModule.swift; sourceTree = ""; }; 58AAA2B7F82F42442ED27A67 /* DownloadService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadService.swift; sourceTree = ""; }; 58AAA312DD0792117182B64E /* Eip20Adapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Eip20Adapter.swift; sourceTree = ""; }; @@ -4263,18 +4151,13 @@ 58AAA3C555FBFB5423CCF8E0 /* FeeSliderValueView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeeSliderValueView.swift; sourceTree = ""; }; 58AAA4022C5803D2440F43C7 /* DebugPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugPresenter.swift; sourceTree = ""; }; 58AAA422A0530C0C07E19F2F /* StepBadgeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepBadgeView.swift; sourceTree = ""; }; - 58AAA42A6EB5242006547A92 /* MarketPostModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketPostModule.swift; sourceTree = ""; }; 58AAA43491E0E4F17D020455 /* SwapApproveViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapApproveViewModel.swift; sourceTree = ""; }; 58AAA444C885BCC354F1B7B3 /* CoinPageMarkdownParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinPageMarkdownParser.swift; sourceTree = ""; }; - 58AAA4A4F31EAB9164B33299 /* MarketTvlSortHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketTvlSortHeaderView.swift; sourceTree = ""; }; - 58AAA50A504CFA74CA19A415 /* MarketMetricView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketMetricView.swift; sourceTree = ""; }; - 58AAA51AD262FBDC3D69EEF8 /* MarketSingleSortHeaderViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketSingleSortHeaderViewModel.swift; sourceTree = ""; }; 58AAA54E84B5C1B68644AE46 /* SimpleSheetTitleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleSheetTitleView.swift; sourceTree = ""; }; 58AAA55A4A6A97C25F84034F /* CoinChartFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinChartFactory.swift; sourceTree = ""; }; 58AAA5BF9EE200DAA24AD42A /* SwapConfirmationAmountCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapConfirmationAmountCell.swift; sourceTree = ""; }; 58AAA5C1F206B755B477A30B /* ChartModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartModule.swift; sourceTree = ""; }; 58AAA656A81B2C12F618FB44 /* BaseEvmAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseEvmAdapter.swift; sourceTree = ""; }; - 58AAA657E30EBD52A5E06ACF /* MarketPostService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketPostService.swift; sourceTree = ""; }; 58AAA681BF5F2CBDCD0D8898 /* SwapApproveViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapApproveViewController.swift; sourceTree = ""; }; 58AAA6CBFBB0EA959466977D /* CoinPageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinPageViewModel.swift; sourceTree = ""; }; 58AAA6FB0AF97333CD9D007F /* InfoSeparatorHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InfoSeparatorHeaderView.swift; sourceTree = ""; }; @@ -4285,13 +4168,9 @@ 58AAA75A0580C45CC08D89E8 /* OneInchSettingsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OneInchSettingsService.swift; sourceTree = ""; }; 58AAA765922F6668954C238E /* MetricChartViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetricChartViewModel.swift; sourceTree = ""; }; 58AAA76748B32A4D8FCD765A /* UniswapSettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UniswapSettingsViewModel.swift; sourceTree = ""; }; - 58AAA775FE9B46DA2910F508 /* MarketGlobalMetricModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketGlobalMetricModule.swift; sourceTree = ""; }; 58AAA78B50AEDADC56B9DEBD /* OneInchSettingsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OneInchSettingsModule.swift; sourceTree = ""; }; - 58AAA78BB269FEBB430092A3 /* MarketTvlSortHeaderViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketTvlSortHeaderViewModel.swift; sourceTree = ""; }; 58AAA7A94D25C20240FD75C6 /* PaymentRequestAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentRequestAddress.swift; sourceTree = ""; }; - 58AAA7B3EA0C8B9FDEC41837 /* MarketGlobalMetricService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketGlobalMetricService.swift; sourceTree = ""; }; 58AAA7C8532511E4BCD9C8D9 /* SwapAllowanceCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapAllowanceCell.swift; sourceTree = ""; }; - 58AAA7D27615D192FBC5486E /* MarketWatchlistModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketWatchlistModule.swift; sourceTree = ""; }; 58AAA7D7F06F7C044DF9CE0A /* MarketGlobalModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketGlobalModule.swift; sourceTree = ""; }; 58AAA80C6D2024281A5FA3E5 /* SwapCoinCardViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapCoinCardViewModel.swift; sourceTree = ""; }; 58AAA83833C970A2DC467715 /* InfoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InfoViewController.swift; sourceTree = ""; }; @@ -4302,23 +4181,17 @@ 58AAA8AD6794AD2AAF462B7B /* CoinCardModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinCardModule.swift; sourceTree = ""; }; 58AAA8B7E47FF3B010851E58 /* Global.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Global.swift; sourceTree = ""; }; 58AAA8CB22CEF84A71CF044F /* MetricChartService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetricChartService.swift; sourceTree = ""; }; - 58AAA8E1106E31D68FD9181D /* MarketGlobalTvlMetricViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketGlobalTvlMetricViewController.swift; sourceTree = ""; }; 58AAA8F53B1A92A391DB1CFF /* CoinChartViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinChartViewModel.swift; sourceTree = ""; }; - 58AAA8FDCCC09B609C7D0FEA /* MarketViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketViewController.swift; sourceTree = ""; }; 58AAA905C4D766F902C08B65 /* DebugRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugRouter.swift; sourceTree = ""; }; 58AAA9828D8742CD3D9995D9 /* SwapSwitchCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapSwitchCell.swift; sourceTree = ""; }; 58AAA9A4761030BE9F60C85E /* UdnAddressParserItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UdnAddressParserItem.swift; sourceTree = ""; }; - 58AAA9B26F62DB74FF3830D5 /* MarketListTvlDecorator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketListTvlDecorator.swift; sourceTree = ""; }; 58AAA9BBAB97C2D21A83956C /* CoinSelectViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinSelectViewController.swift; sourceTree = ""; }; 58AAA9D55F97CE089EC67766 /* LastBlockInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LastBlockInfo.swift; sourceTree = ""; }; 58AAA9D5D29115C5F435CF1B /* ChartConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartConfiguration.swift; sourceTree = ""; }; 58AAA9E19A578FD13792D2B7 /* AdditionalDataView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdditionalDataView.swift; sourceTree = ""; }; 58AAA9F2F3A6F483B0D67E18 /* DataStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataStatus.swift; sourceTree = ""; }; - 58AAAA1B62A6A1A278BE06AA /* MarketWatchlistViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketWatchlistViewController.swift; sourceTree = ""; }; - 58AAAA3930D3CB65CC545658 /* GradientPercentCircle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GradientPercentCircle.swift; sourceTree = ""; }; 58AAAA6EA2DAD95D2CB417FD /* DebugViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugViewController.swift; sourceTree = ""; }; 58AAAA86B59A4D59F08EB334 /* SwapSlippageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapSlippageViewModel.swift; sourceTree = ""; }; - 58AAAA9DFF1F23B0B8A8CEAD /* MarketViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketViewModel.swift; sourceTree = ""; }; 58AAAAA7456EB952A3E5A53F /* AddressService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddressService.swift; sourceTree = ""; }; 58AAAAC62C5B05463D339BCC /* EvmAddressParserItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EvmAddressParserItem.swift; sourceTree = ""; }; 58AAAAD2AA132E9B13726D8B /* MetricChartViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetricChartViewController.swift; sourceTree = ""; }; @@ -4326,7 +4199,6 @@ 58AAAB39CAE1453B9ED024E4 /* SwapConfirmationModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapConfirmationModule.swift; sourceTree = ""; }; 58AAAB692B7C326319D186E4 /* CoinSelectViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinSelectViewModel.swift; sourceTree = ""; }; 58AAAB934A3F1B6490245F1D /* MetricChartModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetricChartModule.swift; sourceTree = ""; }; - 58AAABDFE887324FC10AC290 /* MarketWatchlistService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketWatchlistService.swift; sourceTree = ""; }; 58AAAC2D03916C80C8DC5FE5 /* FeeSliderWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeeSliderWrapper.swift; sourceTree = ""; }; 58AAAC5B00009B199A687EF3 /* EvmKitManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EvmKitManager.swift; sourceTree = ""; }; 58AAAC5B35D17E82D59F7183 /* SwapConfirmationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapConfirmationViewController.swift; sourceTree = ""; }; @@ -4338,20 +4210,15 @@ 58AAAD02C2210D9DA14D1E21 /* UniswapSettingsDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UniswapSettingsDataSource.swift; sourceTree = ""; }; 58AAAD40C9A99F0EDEFCAD14 /* AddressUriParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddressUriParser.swift; sourceTree = ""; }; 58AAAD7AC450FEF913E5417F /* InfoModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InfoModule.swift; sourceTree = ""; }; - 58AAAD81E45666E783B8B2EA /* MarketGlobalDefiMetricService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketGlobalDefiMetricService.swift; sourceTree = ""; }; - 58AAADBB7B760C189AD6032F /* MarketGlobalTvlMetricService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketGlobalTvlMetricService.swift; sourceTree = ""; }; 58AAADD445F174344AFB6AAC /* DebugModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugModule.swift; sourceTree = ""; }; 58AAADE7CE3F004B908CEDA1 /* SwapApproveService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapApproveService.swift; sourceTree = ""; }; 58AAAE153E9683CB79DCF857 /* SelectorButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorButton.swift; sourceTree = ""; }; 58AAAE622FCAB8C2400A3149 /* GradientLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GradientLayer.swift; sourceTree = ""; }; - 58AAAEA0582FFB81EB6C6263 /* MarketGlobalTvlFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketGlobalTvlFetcher.swift; sourceTree = ""; }; 58AAAEB2257174A64DE5E51B /* SwapViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapViewModel.swift; sourceTree = ""; }; 58AAAF4E4A24283A9DF0191F /* OneInchFeeService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OneInchFeeService.swift; sourceTree = ""; }; 58AAAF740AE2BEBD7CEBD563 /* AmountDecimalParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AmountDecimalParser.swift; sourceTree = ""; }; 58AAAFB203A455CB53996F97 /* SwapCoinCardCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapCoinCardCell.swift; sourceTree = ""; }; - 58AAAFB549AE163AD4F920DD /* MarketOverviewViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketOverviewViewController.swift; sourceTree = ""; }; 58AAAFBDA192A490C33DBB95 /* UITextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITextView.swift; sourceTree = ""; }; - 58AAAFE702C2E51EEE209C56 /* MarketListDefiDecorator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketListDefiDecorator.swift; sourceTree = ""; }; 58AAAFF25BF263B5EC4188F7 /* SwapDeadlineViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapDeadlineViewModel.swift; sourceTree = ""; }; 58AAAFF6E494F623AD62AF95 /* UniswapSettingsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UniswapSettingsService.swift; sourceTree = ""; }; 6B146A932A52A69400648C10 /* ChartIndicatorSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartIndicatorSettingsViewController.swift; sourceTree = ""; }; @@ -4362,10 +4229,21 @@ 6B29071B2AF0CB8A006157D6 /* WalletConnectAppShowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectAppShowView.swift; sourceTree = ""; }; 6B29071D2AF0CB8A006157D6 /* WidgetCoinAppShowModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WidgetCoinAppShowModule.swift; sourceTree = ""; }; 6B29071E2AF0CB8A006157D6 /* EventHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventHandler.swift; sourceTree = ""; }; + 6B5F5E0D2C0C65F700E03EB2 /* MarketPlatformViewNew.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketPlatformViewNew.swift; sourceTree = ""; }; + 6B5F5E102C0C660900E03EB2 /* MarketPlatformViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketPlatformViewModel.swift; sourceTree = ""; }; + 6B5F5E142C0DDD7100E03EB2 /* RankView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankView.swift; sourceTree = ""; }; + 6B5F5E172C0DDD8700E03EB2 /* RankViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankViewModel.swift; sourceTree = ""; }; + 6B8BD39D2C11B959003ADE10 /* TextFieldAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldAlert.swift; sourceTree = ""; }; 6BA5117C2BCFA06F00CB5A54 /* FirstAppearModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstAppearModifier.swift; sourceTree = ""; }; 6BAAF3442B9B245C00EFE5B2 /* ShimmerEffect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShimmerEffect.swift; sourceTree = ""; }; 6BAAF3452B9B245C00EFE5B2 /* SlideButtonStyling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SlideButtonStyling.swift; sourceTree = ""; }; 6BAAF3462B9B245C00EFE5B2 /* SlideButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SlideButton.swift; sourceTree = ""; }; + 6BB14F6A2BF49E7100E879B2 /* WalletButtonHiddenManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletButtonHiddenManager.swift; sourceTree = ""; }; + 6BB14F712BFE550600E879B2 /* MarketEtfFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketEtfFetcher.swift; sourceTree = ""; }; + 6BB14F742C01D04200E879B2 /* CheckBoxUiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckBoxUiView.swift; sourceTree = ""; }; + 6BB14F792C05FAB600E879B2 /* MarketTvlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketTvlView.swift; sourceTree = ""; }; + 6BB14F7A2C05FAB600E879B2 /* MarketTvlViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketTvlViewModel.swift; sourceTree = ""; }; + 6BB14F7F2C06F19300E879B2 /* DefiCoin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefiCoin.swift; sourceTree = ""; }; 6BCD52F72A161F4100993F20 /* BackupCloudModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupCloudModule.swift; sourceTree = ""; }; 6BCD52F92A161F4100993F20 /* ICloudBackupTermsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ICloudBackupTermsViewModel.swift; sourceTree = ""; }; 6BCD52FA2A161F4100993F20 /* ICloudBackupTermsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ICloudBackupTermsViewController.swift; sourceTree = ""; }; @@ -4532,7 +4410,6 @@ ABC9A89B361346FFDB04E48A /* UniswapV3Service.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UniswapV3Service.swift; sourceTree = ""; }; ABC9A8A353E491AAD3EDA120 /* ContactBookContactViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactBookContactViewController.swift; sourceTree = ""; }; ABC9A8B3C65C9F0285483160 /* SendZcashViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendZcashViewController.swift; sourceTree = ""; }; - ABC9A8B6A5C590B23C6F83C3 /* MarketWatchlistDecorator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketWatchlistDecorator.swift; sourceTree = ""; }; ABC9A8C0A9FBFB57996E8A8C /* UnspentOutputsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnspentOutputsCell.swift; sourceTree = ""; }; ABC9A8C6A291BACCADF0E443 /* UnspentOutputsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnspentOutputsViewModel.swift; sourceTree = ""; }; ABC9A8CE84FA36438BE4D6B5 /* FileManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = ""; }; @@ -4574,7 +4451,7 @@ ABC9AACC40370E1E0CFC7639 /* IndicatorAdviceCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IndicatorAdviceCell.swift; sourceTree = ""; }; ABC9AAD55B8932EE75E3C037 /* SwapInputModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapInputModule.swift; sourceTree = ""; }; ABC9AAEA86EF9D14503A4791 /* BackupCrypto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupCrypto.swift; sourceTree = ""; }; - ABC9AAF2ADD900F32D87C7BE /* SendViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendViewModel.swift; sourceTree = ""; }; + ABC9AAF2ADD900F32D87C7BE /* SendViewModelOld.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendViewModelOld.swift; sourceTree = ""; }; ABC9AB001077F4001611DFFC /* BackupAppViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupAppViewModel.swift; sourceTree = ""; }; ABC9AB05E1D374DB3454F9B2 /* WCEthereumTransactionPayload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WCEthereumTransactionPayload.swift; sourceTree = ""; }; ABC9AB0A37663BC3F17C7A81 /* FileStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileStorage.swift; sourceTree = ""; }; @@ -4635,7 +4512,6 @@ ABC9ADA345301F29B947F281 /* RestoreFileConfigurationModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreFileConfigurationModule.swift; sourceTree = ""; }; ABC9ADB77831DCB474B24C8A /* SendFeeService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendFeeService.swift; sourceTree = ""; }; ABC9ADC0B58A4ECA7EB76CCB /* BaseAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseAnimation.swift; sourceTree = ""; }; - ABC9ADC1A3B17225B6CC0869 /* MarketCategoryMarketCapFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketCategoryMarketCapFetcher.swift; sourceTree = ""; }; ABC9ADCE6C6975672E185951 /* MultiSwapSlippageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiSwapSlippageView.swift; sourceTree = ""; }; ABC9ADE822BC024F9B798211 /* BottomGradientHolder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BottomGradientHolder.swift; sourceTree = ""; }; ABC9ADF114FCFABEA148AF04 /* SendTimeLockErrorService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendTimeLockErrorService.swift; sourceTree = ""; }; @@ -4699,6 +4575,7 @@ D023D2692A24CD16004F65B0 /* BaseTronAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTronAdapter.swift; sourceTree = ""; }; D023D26C2A24CD4F004F65B0 /* TronKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TronKitManager.swift; sourceTree = ""; }; D023D2702A25CF61004F65B0 /* TronAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TronAdapter.swift; sourceTree = ""; }; + D02447D82C09FA5200A04BBC /* CoinTreasuriesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinTreasuriesView.swift; sourceTree = ""; }; D02A67B0272A7460009B2C1C /* TweetCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TweetCell.swift; sourceTree = ""; }; D02A67B1272A7460009B2C1C /* TweetsProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TweetsProvider.swift; sourceTree = ""; }; D02A67B3272A7460009B2C1C /* TwitterText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TwitterText.swift; sourceTree = ""; }; @@ -4709,11 +4586,14 @@ D02A67B9272A7460009B2C1C /* TweetsPageResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TweetsPageResponse.swift; sourceTree = ""; }; D02A67BA272A7460009B2C1C /* CoinTweetsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinTweetsService.swift; sourceTree = ""; }; D02A67BB272A7460009B2C1C /* CoinTweetsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinTweetsModule.swift; sourceTree = ""; }; + D033289E2BF6199600BBB364 /* InfoNewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoNewView.swift; sourceTree = ""; }; + D03F74812BF76D0A004FBCFA /* GasPriceData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GasPriceData.swift; sourceTree = ""; }; D04D98E9268055A2001A3135 /* TransactionRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionRecord.swift; sourceTree = ""; }; D04DD5402B68C0EF00219B87 /* HighlightedTiltledTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightedTiltledTextView.swift; sourceTree = ""; }; D0532CBD2B149DEE0015DF40 /* WatchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchViewModel.swift; sourceTree = ""; }; D0532CC02B149E110015DF40 /* WatchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchViewController.swift; sourceTree = ""; }; D0532CC32B149E450015DF40 /* WatchService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchService.swift; sourceTree = ""; }; + D054DAE22BE5123F0040B7C9 /* InitialTransactionSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialTransactionSettings.swift; sourceTree = ""; }; D05E968C2A25D6C6002CCD71 /* Trc20Adapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trc20Adapter.swift; sourceTree = ""; }; D05E968F2A261D82002CCD71 /* TronTransactionAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TronTransactionAdapter.swift; sourceTree = ""; }; D05E96922A261DC1002CCD71 /* TronTransactionConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TronTransactionConverter.swift; sourceTree = ""; }; @@ -4734,6 +4614,9 @@ D07157E02A2DDA17006F141F /* SendTronViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTronViewModel.swift; sourceTree = ""; }; D07157E32A2DDAA6006F141F /* SendTronViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTronViewController.swift; sourceTree = ""; }; D0740B1A2B87585000B085F9 /* ResendBitcoinService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResendBitcoinService.swift; sourceTree = ""; }; + D084F6BD2BEB94F700407FA4 /* OutputSelectView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputSelectView2.swift; sourceTree = ""; }; + D084F6C02BEB951C00407FA4 /* OutputSelectorViewModel2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputSelectorViewModel2.swift; sourceTree = ""; }; + D086A9152BF4D08400462024 /* SendParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendParameters.swift; sourceTree = ""; }; D087626E29815DAD00E6FFD4 /* ChooseWatchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChooseWatchViewController.swift; sourceTree = ""; }; D087626F29815DAD00E6FFD4 /* ChooseWatchViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChooseWatchViewModel.swift; sourceTree = ""; }; D090AF60297D725D00699916 /* TransparentIconButtonView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransparentIconButtonView.swift; sourceTree = ""; }; @@ -4742,10 +4625,15 @@ D09200BF293F21700091981A /* RestoreNonStandardViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreNonStandardViewController.swift; sourceTree = ""; }; D09200C0293F21710091981A /* RestoreNonStandardViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreNonStandardViewModel.swift; sourceTree = ""; }; D09200C1293F21720091981A /* RestoreNonStandardModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreNonStandardModule.swift; sourceTree = ""; }; + D092C58A2C12DE3D0060D915 /* PriceChangeManager+HsTimePeriod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PriceChangeManager+HsTimePeriod.swift"; sourceTree = ""; }; + D09C5C622C076C0E00E6909E /* CoinIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinIconView.swift; sourceTree = ""; }; D09D768B2A2E066E004311E6 /* SendTronConfirmationModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTronConfirmationModule.swift; sourceTree = ""; }; D09D768D2A2E06D6004311E6 /* SendTronConfirmationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTronConfirmationViewController.swift; sourceTree = ""; }; D09D76902A2E0753004311E6 /* SendTronConfirmationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTronConfirmationViewModel.swift; sourceTree = ""; }; D09D76932A2E07BD004311E6 /* SendTronConfirmationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTronConfirmationService.swift; sourceTree = ""; }; + D0A6902A2C00ACF600E59296 /* CautionDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CautionDataSource.swift; sourceTree = ""; }; + D0A6902D2C04969300E59296 /* CautionDataSourceViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CautionDataSourceViewModel.swift; sourceTree = ""; }; + D0A690332C05D01C00E59296 /* UIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageView.swift; sourceTree = ""; }; D0A980A82B5E3C0900127AF4 /* StepChangeButtonsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepChangeButtonsView.swift; sourceTree = ""; }; D0A980AE2B60E73F00127AF4 /* LegacyFeeSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyFeeSettingsView.swift; sourceTree = ""; }; D0C2260F2A66A3BC007101F7 /* PersonalSupportModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonalSupportModule.swift; sourceTree = ""; }; @@ -4754,6 +4642,13 @@ D0C226182A66A703007101F7 /* PersonalSupportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonalSupportViewModel.swift; sourceTree = ""; }; D0D5BCBB2976CB9F00587FDB /* PasswordInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordInputView.swift; sourceTree = ""; }; D0D5BCBF2976D3B300587FDB /* PsswordInputCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PsswordInputCell.swift; sourceTree = ""; }; + D0DEFF022BD1253B004C9DF0 /* BitcoinFeeSettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BitcoinFeeSettingsViewModel.swift; sourceTree = ""; }; + D0DEFF032BD1253B004C9DF0 /* BitcoinFeeSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BitcoinFeeSettingsView.swift; sourceTree = ""; }; + D0DEFF082BD1257E004C9DF0 /* BitcoinTransactionService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BitcoinTransactionService.swift; sourceTree = ""; }; + D0DEFF092BD1257F004C9DF0 /* BitcoinFeeData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BitcoinFeeData.swift; sourceTree = ""; }; + D0E5E84E2BE22172005080A4 /* BitcoinSendHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitcoinSendHandler.swift; sourceTree = ""; }; + D0E5E8512BE260C8005080A4 /* BitcoinPreSendHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitcoinPreSendHandler.swift; sourceTree = ""; }; + D0E5E8542BE38AA2005080A4 /* TronSendHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TronSendHandler.swift; sourceTree = ""; }; D0E659BA2B875003000D8981 /* ResendBitcoinViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResendBitcoinViewModel.swift; sourceTree = ""; }; D0EE557D2934B0D60027AAD3 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/InfoPlist.strings"; sourceTree = ""; }; D0EE557E2934B0D60027AAD3 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; @@ -4761,6 +4656,12 @@ D0F132A32B6B98F500C7310E /* RbfViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RbfViewModel.swift; sourceTree = ""; }; D0F132A62B6B990500C7310E /* RbfDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RbfDataSource.swift; sourceTree = ""; }; D0F9F5162B99857700C3190A /* FeeSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeeSettings.swift; sourceTree = ""; }; + D311DA1B2BD114B00013DB8F /* MarketView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketView.swift; sourceTree = ""; }; + D311DA1E2BD115240013DB8F /* MarketGlobalViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketGlobalViewModel.swift; sourceTree = ""; }; + D311DA212BD23C230013DB8F /* MarketAdvancedSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketAdvancedSearchView.swift; sourceTree = ""; }; + D311DA242BD23C890013DB8F /* ScrollableTabHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollableTabHeaderView.swift; sourceTree = ""; }; + D31369852BEA187E00BA6B5B /* ZcashSendHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZcashSendHandler.swift; sourceTree = ""; }; + D31369882BEA188D00BA6B5B /* ZcashPreSendHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZcashPreSendHandler.swift; sourceTree = ""; }; D31C4759238BF175008CB818 /* MnemonicDerivation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MnemonicDerivation.swift; sourceTree = ""; }; D31C475A238BF175008CB818 /* FeeRateState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeeRateState.swift; sourceTree = ""; }; D3285F4520BD158E00644076 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -4768,6 +4669,24 @@ D3285F5120BD158F00644076 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D3373D9420BEC7B30082BC4A /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; D3373DB120C52F640082BC4A /* LaunchScreen.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LaunchScreen.xib; sourceTree = ""; }; + D3384D082BFCB43800515664 /* MarketWatchlistSignalsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketWatchlistSignalsView.swift; sourceTree = ""; }; + D3384D0B2BFCB8F000515664 /* MarketWatchlistSignalBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketWatchlistSignalBadge.swift; sourceTree = ""; }; + D3384D0F2BFDCBDE00515664 /* MarketEtfView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketEtfView.swift; sourceTree = ""; }; + D3384D122BFDCBE700515664 /* MarketEtfViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketEtfViewModel.swift; sourceTree = ""; }; + D3384D152BFDEF6800515664 /* Etf.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Etf.swift; sourceTree = ""; }; + D3384D192BFF0CAF00515664 /* MarketMarketCapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketMarketCapView.swift; sourceTree = ""; }; + D3384D1C2BFF0CB800515664 /* MarketMarketCapViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketMarketCapViewModel.swift; sourceTree = ""; }; + D3384D202BFF0CCA00515664 /* MarketVolumeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketVolumeView.swift; sourceTree = ""; }; + D3384D232BFF0CD100515664 /* MarketVolumeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketVolumeViewModel.swift; sourceTree = ""; }; + D3384D4D2C07020300515664 /* PriceChangeMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceChangeMode.swift; sourceTree = ""; }; + D3384D502C0703B400515664 /* PriceChangeModeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceChangeModeManager.swift; sourceTree = ""; }; + D3402AED2BF5D58B003BF6F8 /* WatchlistViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchlistViewModel.swift; sourceTree = ""; }; + D3402AF02BF5D59D003BF6F8 /* WatchlistModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchlistModifier.swift; sourceTree = ""; }; + D3402AF62BF71C11003BF6F8 /* WatchlistManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchlistManager.swift; sourceTree = ""; }; + D34903162BE8DF48005F147B /* BinanceSendHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinanceSendHandler.swift; sourceTree = ""; }; + D34903192BE8DF5F005F147B /* BinancePreSendHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinancePreSendHandler.swift; sourceTree = ""; }; + D34A29B42BFB4AE200F63036 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/AppWidget.strings; sourceTree = ""; }; + D34A29B52BFB4E3200F63036 /* WatchlistSortBy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchlistSortBy.swift; sourceTree = ""; }; D34E941B21F86C3500AD8E90 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/InfoPlist.strings; sourceTree = ""; }; D34E941C21F86C3500AD8E90 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; }; D350DDAF2AE2526E00CF1989 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; @@ -4780,7 +4699,6 @@ D350DDC72AE27E4900CF1989 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/AppWidget.strings; sourceTree = ""; }; D350DDC92AE27E4A00CF1989 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/AppWidget.strings; sourceTree = ""; }; D350DDCA2AE2818A00CF1989 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/AppWidget.intentdefinition; sourceTree = ""; }; - D350DDCC2AE2819B00CF1989 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/AppWidget.strings; sourceTree = ""; }; D35B518821942E7A00504FBA /* TermsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsViewController.swift; sourceTree = ""; }; D368F5662844F2C400F79777 /* AppIconAlternate.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = AppIconAlternate.xcassets; sourceTree = ""; }; D36DE0AA272FD612000BC916 /* SwapViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapViewController.swift; sourceTree = ""; }; @@ -4805,10 +4723,31 @@ D36DE0F3272FD92E000BC916 /* SwapSelectProviderModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapSelectProviderModule.swift; sourceTree = ""; }; D36DE0F4272FD92F000BC916 /* SwapSelectProviderService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapSelectProviderService.swift; sourceTree = ""; }; D36DE0F5272FD92F000BC916 /* SwapSelectProviderViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapSelectProviderViewModel.swift; sourceTree = ""; }; + D36E50832BF75B6900C361BD /* WatchlistTimePeriod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchlistTimePeriod.swift; sourceTree = ""; }; + D36E50892BF76FA700C361BD /* WatchlistEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchlistEntry.swift; sourceTree = ""; }; + D36E508C2BF76FB400C361BD /* WatchlistProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchlistProvider.swift; sourceTree = ""; }; + D36E50922BF7852D00C361BD /* CoinListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinListView.swift; sourceTree = ""; }; + D3833AD62BEE1A7900ACECFB /* MarketWatchlistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketWatchlistView.swift; sourceTree = ""; }; + D3833AD92BEE1A8300ACECFB /* MarketWatchlistViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketWatchlistViewModel.swift; sourceTree = ""; }; + D3833ADD2BEE3FE000ACECFB /* MarketPlatformsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketPlatformsView.swift; sourceTree = ""; }; + D3833AE02BEE3FE800ACECFB /* MarketPlatformsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketPlatformsViewModel.swift; sourceTree = ""; }; + D3833AE92BEE4CAA00ACECFB /* TopPlatform.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopPlatform.swift; sourceTree = ""; }; + D3833AF12BF20B8600ACECFB /* MarketPairsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketPairsView.swift; sourceTree = ""; }; + D3833AF42BF20B8D00ACECFB /* MarketPairsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketPairsViewModel.swift; sourceTree = ""; }; + D3833AF72BF2181800ACECFB /* MarketPair.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketPair.swift; sourceTree = ""; }; + D3833AFB2BF335C700ACECFB /* MarketNewsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketNewsView.swift; sourceTree = ""; }; + D3833AFE2BF335D100ACECFB /* MarketNewsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketNewsViewModel.swift; sourceTree = ""; }; + D3833B012BF38A8000ACECFB /* MarketTabViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketTabViewModel.swift; sourceTree = ""; }; + D3833B042BF4AFB800ACECFB /* MarqueeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarqueeView.swift; sourceTree = ""; }; D38405CE218317DF007D50AD /* Unstoppable D.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Unstoppable D.app"; sourceTree = BUILT_PRODUCTS_DIR; }; D38406BE21831B3D007D50AD /* Unstoppable.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Unstoppable.app; sourceTree = BUILT_PRODUCTS_DIR; }; D38406C3218327B1007D50AD /* AppIcon.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = AppIcon.xcassets; sourceTree = ""; }; D38406C821832968007D50AD /* AppIconDev.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = AppIconDev.xcassets; sourceTree = ""; }; + D389BC452C0DCF4100724504 /* Advice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Advice.swift; sourceTree = ""; }; + D389BC482C0DDA8F00724504 /* HsTimePeriod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HsTimePeriod.swift; sourceTree = ""; }; + D389BC4B2C0DDCF500724504 /* MarketAdvancedSearchBlockchainsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketAdvancedSearchBlockchainsView.swift; sourceTree = ""; }; + D389BC4E2C0DEF1800724504 /* MarketAdvancedSearchResultsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketAdvancedSearchResultsView.swift; sourceTree = ""; }; + D389BC512C0DEF2200724504 /* MarketAdvancedSearchResultsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketAdvancedSearchResultsViewModel.swift; sourceTree = ""; }; D3948EF02ADA846400FAE566 /* WidgetExtension Dev.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "WidgetExtension Dev.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; D3948EF12ADA846400FAE566 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; D3948EF32ADA846400FAE566 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; @@ -4819,15 +4758,33 @@ D3948F192ADA88D800FAE566 /* IntentExtension Dev.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "IntentExtension Dev.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; D3948F1C2ADA88D900FAE566 /* IntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentHandler.swift; sourceTree = ""; }; D3948F1E2ADA88D900FAE566 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D3A580872BE4DAA2003953F4 /* EvmSendData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmSendData.swift; sourceTree = ""; }; + D3A5808A2BE4DB11003953F4 /* WalletConnectSendHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnectSendHandler.swift; sourceTree = ""; }; + D3A580932BE8AA80003953F4 /* BitcoinSendSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitcoinSendSettingsView.swift; sourceTree = ""; }; + D3A580962BE8AA90003953F4 /* BitcoinSendSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitcoinSendSettingsViewModel.swift; sourceTree = ""; }; D3B476A321D0D64E008B0C3E /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = ""; }; D3B476A421D0D64E008B0C3E /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; D3B476A521D0D659008B0C3E /* zh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh; path = zh.lproj/InfoPlist.strings; sourceTree = ""; }; D3B476A621D0D659008B0C3E /* zh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh; path = zh.lproj/Localizable.strings; sourceTree = ""; }; D3B62A8820CA40DC005A9F80 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; D3B62A9B20CA73A0005A9F80 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + D3B73E262BDBC6120067429D /* IPreSendHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPreSendHandler.swift; sourceTree = ""; }; + D3B73E292BDBC61D0067429D /* EvmPreSendHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmPreSendHandler.swift; sourceTree = ""; }; + D3B73E2C2BDF6B6D0067429D /* MultiSwapSendHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSwapSendHandler.swift; sourceTree = ""; }; + D3B73E2F2BDFC5580067429D /* PriceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceRow.swift; sourceTree = ""; }; D3BA25912ADFAD7C002B13EA /* WidgetExtension Prod.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "WidgetExtension Prod.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; D3BA25A52ADFAF23002B13EA /* IntentExtension Prod.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "IntentExtension Prod.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; D3BC257F2B0B5E1E0092F682 /* TonKitKmm.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = TonKitKmm.xcframework; sourceTree = ""; }; + D3D13A5E2C0D9DCB002484BC /* MarketAdvancedSearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketAdvancedSearchViewModel.swift; sourceTree = ""; }; + D3DB51982BD63D680091BBDB /* MarketSearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketSearchViewModel.swift; sourceTree = ""; }; + D3DB519B2BD685180091BBDB /* RedactedModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactedModifier.swift; sourceTree = ""; }; + D3DB519E2BD6854A0091BBDB /* MarketSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketSearchView.swift; sourceTree = ""; }; + D3DB51A12BD6857E0091BBDB /* MarketGlobalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketGlobalView.swift; sourceTree = ""; }; + D3DB51A42BD685B40091BBDB /* MarketTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketTabView.swift; sourceTree = ""; }; + D3DB51A72BD787490091BBDB /* MarketCoinsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketCoinsView.swift; sourceTree = ""; }; + D3DB51AA2BD787A00091BBDB /* MarketCoinsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketCoinsViewModel.swift; sourceTree = ""; }; + D3DB51AE2BD7AF860091BBDB /* DiffText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffText.swift; sourceTree = ""; }; + D3DB51B12BD912A00091BBDB /* MarketInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketInfo.swift; sourceTree = ""; }; D3DD671B2BC3BB9300EC7F78 /* BaseEvmMultiSwapQuote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseEvmMultiSwapQuote.swift; sourceTree = ""; }; D3DD671E2BC3BD1200EC7F78 /* BaseUniswapMultiSwapQuote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseUniswapMultiSwapQuote.swift; sourceTree = ""; }; D3DD67212BC3BD5C00EC7F78 /* BaseEvmMultiSwapConfirmationQuote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseEvmMultiSwapConfirmationQuote.swift; sourceTree = ""; }; @@ -4839,6 +4796,12 @@ D3DD67332BC3CC2100EC7F78 /* ThorChainMultiSwapBtcQuote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThorChainMultiSwapBtcQuote.swift; sourceTree = ""; }; D3DD67362BC3CC2C00EC7F78 /* ThorChainMultiSwapBtcConfirmationQuote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThorChainMultiSwapBtcConfirmationQuote.swift; sourceTree = ""; }; D3DD67392BC3CFF300EC7F78 /* BaseSendBtcData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseSendBtcData.swift; sourceTree = ""; }; + D3F9B0242BE38AF1009FFA95 /* RegularSendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegularSendView.swift; sourceTree = ""; }; + D3F9B02A2BE3A9A1009FFA95 /* MultiSwapSendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSwapSendView.swift; sourceTree = ""; }; + D3F9B0302BE3B39D009FFA95 /* EvmDecoration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmDecoration.swift; sourceTree = ""; }; + D3F9B0332BE3B3A7009FFA95 /* EvmDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmDecorator.swift; sourceTree = ""; }; + D3F9B0362BE3B5AA009FFA95 /* WalletConnectSendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnectSendView.swift; sourceTree = ""; }; + D3F9B0392BE3BB36009FFA95 /* WalletConnectSendViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnectSendViewModel.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -4855,7 +4818,6 @@ 6BDA29AB29D6F37C003847ED /* ECashKit in Frameworks */, D3604E8C28F03DBF0066C366 /* BitcoinCashKit in Frameworks */, D3993DC428F429AA008720FB /* UnstoppableDomainsResolution in Frameworks */, - D3BC25802B0B5E1E0092F682 /* TonKitKmm.xcframework in Frameworks */, D3604E9E28F03DC00066C366 /* BinanceChainKit in Frameworks */, D023D2652A24B9DC004F65B0 /* TronKit in Frameworks */, D3C187BC2907CFC200FE1900 /* Checkpoints in Frameworks */, @@ -4886,6 +4848,7 @@ D3604E9C28F03DC00066C366 /* FeeRateKit in Frameworks */, D3C187E2290FD00E00FE1900 /* ComponentKit in Frameworks */, D339A93F29126D2A00B895BE /* HsCryptoKit in Frameworks */, + 6BBCE4A32BDA419200ABBD55 /* Web3Wallet in Frameworks */, D08C93B12B91E3B400A7D1D5 /* Hodler in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4934,6 +4897,7 @@ D3604E8228F03C6B0066C366 /* FeeRateKit in Frameworks */, D3C187D2290FCF3D00FE1900 /* ComponentKit in Frameworks */, D339A93D29126D0F00B895BE /* HsCryptoKit in Frameworks */, + 6BBCE4A52BDA419B00ABBD55 /* Web3Wallet in Frameworks */, D08C93AF2B91E39E00A7D1D5 /* Hodler in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4981,17 +4945,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 11B35022BB7BE673C5108390 /* MarketTopPairs */ = { - isa = PBXGroup; - children = ( - 11B3557E5ACDC89EF79C8C0C /* MarketListMarketPairDecorator.swift */, - 11B35C5CA7497C540FFC5D39 /* MarketTopPairsViewController.swift */, - 11B359715B07FD5316D72A07 /* MarketTopPairsViewModel.swift */, - 11B355C615D9FE4290671D5D /* MarketTopPairsModule.swift */, - ); - path = MarketTopPairs; - sourceTree = ""; - }; 11B35043F81AA7646DFDDBBC /* CoinInvestors */ = { isa = PBXGroup; children = ( @@ -5003,18 +4956,6 @@ path = CoinInvestors; sourceTree = ""; }; - 11B3508440D2593C95D34386 /* MarketCategory */ = { - isa = PBXGroup; - children = ( - 11B350CAB1C54A2CAA4C76F6 /* MarketCategoryModule.swift */, - 11B357229D5E717F2051F0AC /* MarketCategoryService.swift */, - 11B352AC4F5BE70D055293D7 /* MarketCategoryViewModel.swift */, - ABC9ADC1A3B17225B6CC0869 /* MarketCategoryMarketCapFetcher.swift */, - 11B3584888F2DB8CCFAA90DF /* MarketCategoryViewController.swift */, - ); - path = MarketCategory; - sourceTree = ""; - }; 11B350A2E7EE706CB00F6E34 /* BackupVerifyWords */ = { isa = PBXGroup; children = ( @@ -5231,7 +5172,6 @@ 11B3506CB3D780A00F4BBBBE /* AccountRecord_v_0_20.swift */, 11B35B2C6C103AFF4CCC6E91 /* CoinRecord_v19.swift */, 1A5646C5FEE5A60A658B0180 /* AppVersion_v_0_20.swift */, - 11B35E255F6CA21FFA9E6B42 /* FavoriteCoinRecord_v_0_22.swift */, 11B35CB98A27269A510F40EE /* SyncMode_v_0_24.swift */, 11B3526A40F07F6C8E77BEF9 /* BlockchainSettingRecord_v_0_24.swift */, 11B356E4E27F5C12FC3859D1 /* CustomToken.swift */, @@ -5241,6 +5181,7 @@ 11B3509AC90AEDF72F5989C6 /* EnabledWallet_v_0_34.swift */, 11B3528DDD55DDA1BAC2BADB /* ActiveAccount_v_0_36.swift */, 11B356A734526DECD9606A66 /* AccountRecord_v_0_36.swift */, + 11B35E255F6CA21FFA9E6B42 /* FavoriteCoinRecord_v_0_38.swift */, ); path = Deprecated; sourceTree = ""; @@ -5268,6 +5209,7 @@ ABC9AF26FDCB363793BF66E1 /* Integer.swift */, ABC9A776346AF62265896CA1 /* CellElement.swift */, 11B353356496AA219686B993 /* UIWindow.swift */, + D0A690332C05D01C00E59296 /* UIImageView.swift */, ); path = Extensions; sourceTree = ""; @@ -5285,16 +5227,6 @@ path = Pool; sourceTree = ""; }; - 11B352D7FBB7101D346C9CCF /* TopPairs */ = { - isa = PBXGroup; - children = ( - 11B350465C489A233625E8F2 /* MarketOverviewTopPairsDataSource.swift */, - 11B35AB1D0CE5D8ECE7DDF65 /* MarketOverviewTopPairsViewModel.swift */, - 11B352BDC42A2F717AFAE7BD /* MarketOverviewTopPairsService.swift */, - ); - path = TopPairs; - sourceTree = ""; - }; 11B352FE5BF3F643F4A9CD99 /* Cells */ = { isa = PBXGroup; children = ( @@ -5351,6 +5283,15 @@ D00DAE442B626C2900F48E1D /* GasPrice.swift */, ABC9A448ABC30B93088DE978 /* Binding.swift */, 11B35ED0A8819AB7EA27D368 /* StatExtensions.swift */, + D3DB51B12BD912A00091BBDB /* MarketInfo.swift */, + D3833AE92BEE4CAA00ACECFB /* TopPlatform.swift */, + D3833AF72BF2181800ACECFB /* MarketPair.swift */, + D086A9152BF4D08400462024 /* SendParameters.swift */, + D3384D152BFDEF6800515664 /* Etf.swift */, + 6BB14F7F2C06F19300E879B2 /* DefiCoin.swift */, + D389BC452C0DCF4100724504 /* Advice.swift */, + D389BC482C0DDA8F00724504 /* HsTimePeriod.swift */, + D092C58A2C12DE3D0060D915 /* PriceChangeManager+HsTimePeriod.swift */, ); path = Extensions; sourceTree = ""; @@ -5390,16 +5331,6 @@ path = UserInterface; sourceTree = ""; }; - 11B353B7A5F3850C328543E8 /* MarketTop */ = { - isa = PBXGroup; - children = ( - 11B35F9DA79410E7B9C1B0F8 /* MarketTopModule.swift */, - 11B35770F0C72E1CD3F99985 /* MarketTopService.swift */, - 11B3505AD2C1640DEAD8CFFC /* MarketTopViewController.swift */, - ); - path = MarketTop; - sourceTree = ""; - }; 11B353F19C7361EF351410B1 /* NftCollection */ = { isa = PBXGroup; children = ( @@ -5437,14 +5368,6 @@ path = Alert; sourceTree = ""; }; - 11B3546A0ED6C1FCC3C5A097 /* FilteredList */ = { - isa = PBXGroup; - children = ( - 11B35ADF9BC4D149F86F23E4 /* MarketFilteredListService.swift */, - ); - path = FilteredList; - sourceTree = ""; - }; 11B3546A6E6F3CC013C9FF44 /* SwiftUI */ = { isa = PBXGroup; children = ( @@ -5491,6 +5414,13 @@ ABC9A07015564A3BE995B0D7 /* Recipient */, ABC9A49FE1A041CAC3B6C1E9 /* Alert */, 6BA5117C2BCFA06F00CB5A54 /* FirstAppearModifier.swift */, + D311DA242BD23C890013DB8F /* ScrollableTabHeaderView.swift */, + D3DB519B2BD685180091BBDB /* RedactedModifier.swift */, + D3DB51AE2BD7AF860091BBDB /* DiffText.swift */, + D3B73E2F2BDFC5580067429D /* PriceRow.swift */, + D3833B042BF4AFB800ACECFB /* MarqueeView.swift */, + 6BB14F742C01D04200E879B2 /* CheckBoxUiView.swift */, + D09C5C622C076C0E00E6909E /* CoinIconView.swift */, ); path = SwiftUI; sourceTree = ""; @@ -5574,6 +5504,9 @@ 11B35196B818E6069195BAF1 /* KeychainManager.swift */, 11B3505A43D9C2787B3BD153 /* PasscodeLockManager.swift */, 11B359980AA45D6B44151D7A /* StatManager.swift */, + 6BB14F6A2BF49E7100E879B2 /* WalletButtonHiddenManager.swift */, + D3402AF62BF71C11003BF6F8 /* WatchlistManager.swift */, + D3384D502C0703B400515664 /* PriceChangeModeManager.swift */, ); path = Managers; sourceTree = ""; @@ -5626,6 +5559,7 @@ 11B354AB3C3435F2D5F39EE6 /* Modules */ = { isa = PBXGroup; children = ( + D311DA1A2BCD28C10013DB8F /* SendNew */, 11B35344CEA8CF35257A6975 /* Launch */, 11B35597F6C8570E3C7ABFBF /* Main */, 11B355696B08867B698F84CD /* Wallet */, @@ -5652,7 +5586,6 @@ 11B350F4F8AA86BB84CF5CF2 /* SendEvmTransaction */, 11B35D070AAF3E6A8ED8EC83 /* SendEvm */, D07157D72A2DD7A8006F141F /* SendTron */, - 58AAA119CAEF1B4621D71DF8 /* Favorites */, 58AAAC89F3E7CD9F799B89D7 /* Coin */, 11B3581D97669C8038485FAE /* CreateAccount */, 11B35FEAD9772F1E11F3D767 /* RestoreAccount */, @@ -5697,22 +5630,11 @@ 11B35E2F913B2546A041D0AE /* NoPasscode */, 11B35B779976BD7D47FFD40C /* MultiSwap */, D0118E462B7C99F700D55CE6 /* ResendBitcoin */, + D033289D2BF6196E00BBB364 /* InfoNew */, ); path = Modules; sourceTree = ""; }; - 11B354B9064677BDA5946B48 /* Ranks */ = { - isa = PBXGroup; - children = ( - 11B3513AC6560B9C37C342F3 /* CoinRankService.swift */, - 11B359575A4E090B236E84C7 /* CoinRankViewModel.swift */, - 11B35A1E2AE3DC240D5B785E /* CoinRankViewController.swift */, - 11B358A22655004017228F65 /* CoinRankModule.swift */, - 11B351664970D7EA1F7B50C7 /* CoinRankHeaderView.swift */, - ); - path = Ranks; - sourceTree = ""; - }; 11B354D11B72A988BA1D8F59 /* CoinMarkets */ = { isa = PBXGroup; children = ( @@ -5852,7 +5774,6 @@ children = ( 11B357F2FAE27C9739CAE5C7 /* CoinPriceListEntry.swift */, 11B35EE1C1F555F4160AC201 /* CoinPriceListProvider.swift */, - 11B355D1DB2F95F1183FF2F8 /* CoinPriceListView.swift */, ); path = CoinPriceList; sourceTree = ""; @@ -5908,7 +5829,6 @@ 11B35450456BE5E3EE8F7391 /* Faq.swift */, 11B35B176A5FDEBBE94D307E /* BitcoinCashCoinType.swift */, 11B352D393EDFE4F015B0DEA /* Address.swift */, - 179E7048A730489634E27043 /* FavoriteCoinRecord.swift */, 11B35F98E89F83A30870F404 /* ActiveAccount.swift */, 11B3546480B733000550BEB6 /* RestoreSettingRecord.swift */, 11B3502637A858E6DDF9471B /* EvmSyncSource.swift */, @@ -5960,6 +5880,9 @@ 11B35B6F5261FF3F9ECBC02E /* PasscodeLockState.swift */, 11B35743A66A4653A3C2FDBF /* Stats.swift */, 11B35B9F4421EE65B8B09370 /* StatRecord.swift */, + D36E50832BF75B6900C361BD /* WatchlistTimePeriod.swift */, + D34A29B52BFB4E3200F63036 /* WatchlistSortBy.swift */, + D3384D4D2C07020300515664 /* PriceChangeMode.swift */, ); path = Models; sourceTree = ""; @@ -5985,17 +5908,6 @@ path = Cells; sourceTree = ""; }; - 11B35677CF7976A3B13AAA6E /* MarketAdvancedSearch */ = { - isa = PBXGroup; - children = ( - 11B35DCB7125B0046592414B /* MarketAdvancedSearchModule.swift */, - 11B35C19608F6A314CF1F0C5 /* MarketAdvancedSearchService.swift */, - 11B356BEB2B4DFC3E9C950C5 /* MarketAdvancedSearchViewModel.swift */, - 11B35A12A3B7218DF597C172 /* MarketAdvancedSearchViewController.swift */, - ); - path = MarketAdvancedSearch; - sourceTree = ""; - }; 11B356791A9FB33F6AF7409E /* Passcode */ = { isa = PBXGroup; children = ( @@ -6015,7 +5927,6 @@ 11B357889F003A0B33D9DF27 /* PriceChangeType.swift */, 11B357A5569EAC7D20CD40B2 /* ValueFormatter.swift */, 11B3558D624AF040E9D102DF /* Extensions.swift */, - 11B35ADBD038830223A8375D /* CoinPriceListMode.swift */, 11B35CBAB0A2DA60C3E9A22B /* WidgetConfig.swift */, ); path = Misc; @@ -6048,31 +5959,6 @@ path = Address; sourceTree = ""; }; - 11B356D1F8AA459AEEF579D7 /* SendConfirmation */ = { - isa = PBXGroup; - children = ( - 11B35B91B5EAF2E193FDC04E /* SendConfirmationNewView.swift */, - 11B35BEF6F80BDF166173819 /* SendConfirmationNewViewModel.swift */, - 11B35B0F9BC5C4CDBC5B041D /* SendEvmHandler.swift */, - 11B35D8B730D82D948B27210 /* ISendHandler.swift */, - 11B35613DBD19C0B86D83B49 /* BaseSendEvmData.swift */, - 11B355A2FFA369C4E89DFF53 /* ISendConfirmationData.swift */, - 11B35AD24681D0A122E6A3C5 /* SendDataNew.swift */, - 11B355166E437B7ADB8B8EBA /* ValueLevel.swift */, - 11B359C9E2EFD391FB848618 /* SendConfirmField.swift */, - 11B353B02ADF5EC5CC83FB33 /* SendHandlerFactory.swift */, - 11B3542B6FE4B4F0C0B65369 /* FeeData.swift */, - 11B3562F83BCEE2720B1C23F /* ITransactionService.swift */, - 11B3564FC860C383807BFBE3 /* EvmTransactionService.swift */, - 11B352D4978C011048EC985F /* TransactionServiceFactory.swift */, - 11B3514BFFAE3CE09F4DB2EA /* TransactionSettings.swift */, - 11B35B1EC5A29D1B77C1BCB6 /* EvmFeeData.swift */, - 11B359F3E7E0DB7A55C079CD /* EvmFeeEstimator.swift */, - D3DD67392BC3CFF300EC7F78 /* BaseSendBtcData.swift */, - ); - path = SendConfirmation; - sourceTree = ""; - }; 11B356E7705CB0B09907CAE5 /* ManageAccounts */ = { isa = PBXGroup; children = ( @@ -6142,7 +6028,6 @@ 11B35763ED14419B9EE4C6F9 /* EnabledWalletStorage.swift */, 11B358556C8FC5368E14D81E /* AccountRecordStorage.swift */, 11B35822E26E7298100CD69D /* LogRecordStorage.swift */, - 11B35C7B8BA65E9AA3BB7AFB /* FavoriteCoinRecordStorage.swift */, 11B355C1E3C922BAE804AAF9 /* WalletConnectSessionStorage.swift */, 11B354C4B46DF1A50103F026 /* ActiveAccountStorage.swift */, 11B350DD8FFDB14904D23AE0 /* RestoreSettingsStorage.swift */, @@ -6342,18 +6227,6 @@ path = Ton; sourceTree = ""; }; - 11B359040ACA861F4788E983 /* SwiftUI */ = { - isa = PBXGroup; - children = ( - 11B35EAECC2236EB081241B4 /* SendAmountViewModel.swift */, - 11B352436A876FC59DF41C78 /* SendAmountView.swift */, - 11B35683F0E48309E8298427 /* SendModuleNew.swift */, - 11B353355FF7FBE72BF60981 /* SendView.swift */, - 11B357FD9D760D9671A3DF24 /* SendViewModelNew.swift */, - ); - path = SwiftUI; - sourceTree = ""; - }; 11B3592A7B1E091728CA81B4 /* BirthdayInput */ = { isa = PBXGroup; children = ( @@ -6438,18 +6311,6 @@ path = RestoreSettings; sourceTree = ""; }; - 11B35AAFE626B8D1806D8960 /* MarketList */ = { - isa = PBXGroup; - children = ( - 11B3562819DF141457837340 /* MarketWatchlistToggleService.swift */, - 11B3596381A93F3A3D2575D6 /* MarketListViewController.swift */, - 11B355BEB95969D89B3F8876 /* MarketListViewModel.swift */, - 11B353B060BDF272932D3522 /* MarketListMarketFieldDecorator.swift */, - 1A564555A67E4DC1DC935A04 /* MarketListWatchViewModel.swift */, - ); - path = MarketList; - sourceTree = ""; - }; 11B35AC73BD1A5E8D91C19E9 /* CoinToggle */ = { isa = PBXGroup; children = ( @@ -6533,20 +6394,18 @@ 11B357975CCFB31CCEF29F97 /* IMultiSwapProvider.swift */, 11B35062C72B1D98A2A4EDA9 /* MultiSwapQuotesView.swift */, 11B35140CD5BF8B1C26A6278 /* MultiSwapButtonState.swift */, - 11B353F8063B95C6571AA517 /* MultiSwapConfirmationView.swift */, 11B350910284BA2BF694FA17 /* MultiSwapSettingStorage.swift */, 11B35733E1033052E6CA46C3 /* Providers */, 11B35E1144E3EF63ACEE8F75 /* TokenSelect */, - D0B462F12B5A470F0027815D /* FeeSettings */, ABC9A8880D69657A7187D71D /* AddressView */, - 11B3573427BEEC59D4B978FA /* MultiSwapConfirmationViewModel.swift */, ABC9AA67148C02EABEA43AE2 /* Approve */, - 11B356D1F8AA459AEEF579D7 /* SendConfirmation */, 11B350F532661482B6170F92 /* IMultiSwapQuote.swift */, 11B35293738048B01671B637 /* IMultiSwapConfirmationQuote.swift */, 11B35D0D43137223A01FC2DA /* MultiSwapMainField.swift */, 11B358DD4F0C5E4E5A4EEBA4 /* MultiSwapPreSwapStep.swift */, 11B35EDC73170179EA8F4CBE /* MultiSwapModule.swift */, + D3B73E2C2BDF6B6D0067429D /* MultiSwapSendHandler.swift */, + D3F9B02A2BE3A9A1009FFA95 /* MultiSwapSendView.swift */, ); path = MultiSwap; sourceTree = ""; @@ -6743,16 +6602,6 @@ path = ManageAccount; sourceTree = ""; }; - 11B35D945123DFF27A66FFC5 /* MarketAdvancedSearchResults */ = { - isa = PBXGroup; - children = ( - 11B3598FB2653DB1DC1429CA /* MarketAdvancedSearchResultModule.swift */, - 11B358C7505D0DE60CD03B22 /* MarketAdvancedSearchResultService.swift */, - 11B353A1CC274EDBF8A67DEA /* MarketAdvancedSearchResultViewController.swift */, - ); - path = MarketAdvancedSearchResults; - sourceTree = ""; - }; 11B35DC1E21C6740F9C49143 /* PrivateKeys */ = { isa = PBXGroup; children = ( @@ -6914,10 +6763,8 @@ 11B35F5F70D13CDD22955832 /* Treasuries */ = { isa = PBXGroup; children = ( - 11B35F08C14B3F0D978E2E7F /* CoinTreasuriesModule.swift */, - 11B3580ECB328146E94D4359 /* CoinTreasuriesService.swift */, 11B3522CBA84677E00D44983 /* CoinTreasuriesViewModel.swift */, - 11B351E253E310F1738EBE13 /* CoinTreasuriesViewController.swift */, + D02447D82C09FA5200A04BBC /* CoinTreasuriesView.swift */, ); path = Treasuries; sourceTree = ""; @@ -7002,17 +6849,6 @@ path = BalanceError; sourceTree = ""; }; - 1A56441B781D474BC4F6FA85 /* Category */ = { - isa = PBXGroup; - children = ( - 11B35A0AF4D03160AF66D1D9 /* MarketOverviewCategoryService.swift */, - 1A564ADD13E597F423249CA3 /* MarketOverviewCategoryViewModel.swift */, - 1A564A6D161EAD22626332C1 /* MarketOverviewCategoryDataSource.swift */, - 1A5645B1C5FD344967B1F4B7 /* MarketOverviewCategoryCell.swift */, - ); - path = Category; - sourceTree = ""; - }; 1A564533BEABADF4DC5F8A25 /* Main */ = { isa = PBXGroup; children = ( @@ -7025,29 +6861,6 @@ path = Main; sourceTree = ""; }; - 1A56459494751BD5A4416ECB /* MarketTopPlatforms */ = { - isa = PBXGroup; - children = ( - 1A56459D85D4859D8A0F4D5A /* MarketTopPlatformsModule.swift */, - 1A5648E7534C2E7F16C4A2D4 /* MarketTopPlatformsService.swift */, - 1A564995DE20E52E8E0F1E6A /* MarketTopPlatformsViewModel.swift */, - 1A564C4DB4A57CCF2C5EFB78 /* MarketTopPlatformsViewController.swift */, - 1A564D8F8A8A63BC9BEAAD56 /* TopPlatformsMultiSortHeaderViewModel.swift */, - 1A564C5CC7EC339C3113869D /* MarketListTopPlatformDecorator.swift */, - ); - path = MarketTopPlatforms; - sourceTree = ""; - }; - 1A5645A040DB85EC7ED82369 /* TopCoins */ = { - isa = PBXGroup; - children = ( - 1A564D661F3AE561D7FE9FAA /* MarketOverviewTopCoinsService.swift */, - 1A5640B4F6298D9F326C5EDE /* MarketOverviewTopCoinsViewModel.swift */, - 1A5646218714BA81DE9B5631 /* MarketOverviewTopCoinsDataSource.swift */, - ); - path = TopCoins; - sourceTree = ""; - }; 1A56463750A2B2C557B399E7 /* Security */ = { isa = PBXGroup; children = ( @@ -7058,26 +6871,6 @@ path = Security; sourceTree = ""; }; - 1A5646D7F9188134B80B17F2 /* TopPlatforms */ = { - isa = PBXGroup; - children = ( - 11B3582259AD3A0C55CF6D2C /* MarketOverviewTopPlatformsService.swift */, - 1A56477F6FC71270AD53A3AE /* MarketOverviewTopPlatformsViewModel.swift */, - 1A564B44985D1169593F202C /* MarketOverviewTopPlatformsDataSource.swift */, - ); - path = TopPlatforms; - sourceTree = ""; - }; - 1A5646DA41E1060F8643C9C0 /* GlobalMarket */ = { - isa = PBXGroup; - children = ( - 1A564EDF0FD6A1D1575D1EFB /* MarketOverviewGlobalService.swift */, - 1A5649C48B3AABC56D2512ED /* MarketOverviewGlobalViewModel.swift */, - 1A5647FA18CC69113ECB6581 /* MarketOverviewGlobalDataSource.swift */, - ); - path = GlobalMarket; - sourceTree = ""; - }; 1A5646EF1FF04ABE198C069D /* Placeholder */ = { isa = PBXGroup; children = ( @@ -7097,19 +6890,6 @@ path = ScanQr; sourceTree = ""; }; - 1A56485634872CA2E6CFD6EE /* TopPlatform */ = { - isa = PBXGroup; - children = ( - 1A564BDA5600859626D99BB4 /* TopPlatformModule.swift */, - 1A5649C0BD100768C726B4FB /* TopPlatformService.swift */, - 1A564A1B86DF22E86F0BB442 /* TopPlatformMarketCapFetcher.swift */, - 11B3527F1528AA697AAA6E61 /* TopPlatformViewModel.swift */, - 11B357E05A8AF5608ECF5D5F /* TopPlatformHeaderCell.swift */, - 11B35BB370AE2C896BB9F877 /* TopPlatformViewController.swift */, - ); - path = TopPlatform; - sourceTree = ""; - }; 1A5648A0CFE0BED4703D9794 /* Privacy */ = { isa = PBXGroup; children = ( @@ -7147,19 +6927,6 @@ path = AppStatus; sourceTree = ""; }; - 1A564F308577D43053000DE8 /* MarketNftTopCollections */ = { - isa = PBXGroup; - children = ( - 1A564E5282C3C22DA85141AF /* MarketNftTopCollectionsModule.swift */, - 1A564D12426BCA027C67377E /* MarketNftTopCollectionsService.swift */, - 1A5649E41FE690AF0A712426 /* MarketNftTopCollectionsViewModel.swift */, - 1A5641A724199908970CFB54 /* MarketNftTopCollectionsViewController.swift */, - 1A564CC5878BF33B8CE1F339 /* MarketListNftCollectionDecorator.swift */, - 1A5643A672A508BC4CBCABDD /* NftCollectionsMultiSortHeaderViewModel.swift */, - ); - path = MarketNftTopCollections; - sourceTree = ""; - }; 2FA5D04441C8926DBEB858AF /* InputOutputOrder */ = { isa = PBXGroup; children = ( @@ -7396,25 +7163,6 @@ path = Address; sourceTree = ""; }; - 58AAA113A485122CE8DBE7B4 /* MarketPosts */ = { - isa = PBXGroup; - children = ( - 58AAA42A6EB5242006547A92 /* MarketPostModule.swift */, - 58AAA657E30EBD52A5E06ACF /* MarketPostService.swift */, - 58AAA11651E3CE29A461BF42 /* MarketPostViewModel.swift */, - 58AAA263DAB58FD63E6A9351 /* MarketPostViewController.swift */, - ); - path = MarketPosts; - sourceTree = ""; - }; - 58AAA119CAEF1B4621D71DF8 /* Favorites */ = { - isa = PBXGroup; - children = ( - 58AAA0ED0FCFACF791EC865C /* FavoritesManager.swift */, - ); - path = Favorites; - sourceTree = ""; - }; 58AAA1A28E8F61C815C86131 /* Info */ = { isa = PBXGroup; children = ( @@ -7494,22 +7242,6 @@ path = Fiat; sourceTree = ""; }; - 58AAA3EE50F55E115BCF90B6 /* Views */ = { - isa = PBXGroup; - children = ( - 58AAAA3930D3CB65CC545658 /* GradientPercentCircle.swift */, - 58AAA50A504CFA74CA19A415 /* MarketMetricView.swift */, - 11B3503B9A985B4835FDB03D /* MarketMultiSortHeaderView.swift */, - 11B350FAB6F1A6E1FCFACB2F /* MarketMultiSortHeaderViewModel.swift */, - 58AAA15F4FA7B9EC091EDFF3 /* MarketSingleSortHeaderView.swift */, - 58AAA51AD262FBDC3D69EEF8 /* MarketSingleSortHeaderViewModel.swift */, - 58AAA78BB269FEBB430092A3 /* MarketTvlSortHeaderViewModel.swift */, - 58AAA4A4F31EAB9164B33299 /* MarketTvlSortHeaderView.swift */, - 11B35F9BA41AC15436A4B977 /* DropdownSortHeaderView.swift */, - ); - path = Views; - sourceTree = ""; - }; 58AAA5B7985B3E7423CFD479 /* OneInch */ = { isa = PBXGroup; children = ( @@ -7519,26 +7251,6 @@ path = OneInch; sourceTree = ""; }; - 58AAA5C5DA041F3A46A6B241 /* MarketOverview */ = { - isa = PBXGroup; - children = ( - 1A5646DA41E1060F8643C9C0 /* GlobalMarket */, - 1A5645A040DB85EC7ED82369 /* TopCoins */, - 1A56441B781D474BC4F6FA85 /* Category */, - 1A5646D7F9188134B80B17F2 /* TopPlatforms */, - 3AB682BC25BADD97002197A5 /* MarketOverviewModule.swift */, - 1A564206FEC56546760B9BEA /* MarketOverviewViewModel.swift */, - 1A5644E4694DBB0E6E0B10CC /* BaseMarketOverviewTopListDataSource.swift */, - 58AAAFB549AE163AD4F920DD /* MarketOverviewViewController.swift */, - 58AAA2521E8F8845D96AB865 /* MarketOverviewHeaderCell.swift */, - 11B35DCCC2D8CD00EF6A9A77 /* MarketOverviewMetricsCell.swift */, - 11B3554159E6E5B7C1E71F04 /* MarketOverviewService.swift */, - 11B35DB992C240A4CF24938A /* MarketCategoryView.swift */, - 11B352D7FBB7101D346C9CCF /* TopPairs */, - ); - path = MarketOverview; - sourceTree = ""; - }; 58AAA7A159BA985AD4DB5515 /* Views */ = { isa = PBXGroup; children = ( @@ -7559,15 +7271,6 @@ path = CoinChart; sourceTree = ""; }; - 58AAA8784275769B6BEF45F5 /* MarketGlobal */ = { - isa = PBXGroup; - children = ( - 58AAA0A9EA8A2210522F38EE /* MarketGlobalFetcher.swift */, - 58AAA7D7F06F7C044DF9CE0A /* MarketGlobalModule.swift */, - ); - path = MarketGlobal; - sourceTree = ""; - }; 58AAA88E3D2A68E24195C0C3 /* ViewModels */ = { isa = PBXGroup; children = ( @@ -7593,26 +7296,23 @@ 58AAA9EB9618EBC895D0B123 /* Market */ = { isa = PBXGroup; children = ( - 11B35AAFE626B8D1806D8960 /* MarketList */, - 58AAA5C5DA041F3A46A6B241 /* MarketOverview */, - 58AAAAF9DC549506AF5B4C6D /* MarketWatchlist */, - 11B35677CF7976A3B13AAA6E /* MarketAdvancedSearch */, - 11B35D945123DFF27A66FFC5 /* MarketAdvancedSearchResults */, - 58AAA8784275769B6BEF45F5 /* MarketGlobal */, - 58AAA113A485122CE8DBE7B4 /* MarketPosts */, - 11B3508440D2593C95D34386 /* MarketCategory */, - 11B353B7A5F3850C328543E8 /* MarketTop */, - 58AAAC5B31C9DD58D57A3EA9 /* MarketGlobalMetric */, - 58AAA3EE50F55E115BCF90B6 /* Views */, - 1A564F308577D43053000DE8 /* MarketNftTopCollections */, - 1A56459494751BD5A4416ECB /* MarketTopPlatforms */, - 1A56485634872CA2E6CFD6EE /* TopPlatform */, + D3EE4A462C0EEE7100E40C97 /* Tab */, + D3EE4A452C0EEE6500E40C97 /* Global */, + D3EE4A442C0EEE5700E40C97 /* Search */, + D3EE4A432C0EE43000E40C97 /* Fetchers */, + D3D13A5D2C0D9D9A002484BC /* AdvancedSearch */, + 6BB14F782C05FAA600E879B2 /* Tvl */, + D3384D1F2BFF0CBD00515664 /* Volume */, + D3384D182BFF0C9900515664 /* MarketCap */, + D3384D0E2BFDCBD100515664 /* Etf */, + D3833AFA2BF335B800ACECFB /* News */, + D3833AF02BF20B7200ACECFB /* Pairs */, + D3833AEC2BF1F0AC00ACECFB /* Platform */, + D3833ADC2BEE3FC200ACECFB /* Platforms */, + D3833AD52BEE1A2900ACECFB /* Watchlist */, + D3DB51AD2BD7A9740091BBDB /* Coins */, 58AAA3173853A16B2433AEC0 /* MarketModule.swift */, - 58AAAA9DFF1F23B0B8A8CEAD /* MarketViewModel.swift */, - 58AAA8FDCCC09B609C7D0FEA /* MarketViewController.swift */, - 11B3543F4D196A47EFE3E6F7 /* MarketHeaderCell.swift */, - 11B3546A0ED6C1FCC3C5A097 /* FilteredList */, - 11B35022BB7BE673C5108390 /* MarketTopPairs */, + D311DA1B2BD114B00013DB8F /* MarketView.swift */, ); path = Market; sourceTree = ""; @@ -7643,18 +7343,6 @@ path = OneInch; sourceTree = ""; }; - 58AAAAF9DC549506AF5B4C6D /* MarketWatchlist */ = { - isa = PBXGroup; - children = ( - 58AAA7D27615D192FBC5486E /* MarketWatchlistModule.swift */, - 58AAABDFE887324FC10AC290 /* MarketWatchlistService.swift */, - 11B3566FE007887C3528583C /* MarketWatchlistViewModel.swift */, - 58AAAA1B62A6A1A278BE06AA /* MarketWatchlistViewController.swift */, - ABC9A8B6A5C590B23C6F83C3 /* MarketWatchlistDecorator.swift */, - ); - path = MarketWatchlist; - sourceTree = ""; - }; 58AAAB6A314C9C062F5707AB /* Debug */ = { isa = PBXGroup; children = ( @@ -7675,30 +7363,10 @@ path = DoubleSpendInfo; sourceTree = ""; }; - 58AAAC23B9A9A22F8C485881 /* DefiCap */ = { - isa = PBXGroup; - children = ( - 58AAAD81E45666E783B8B2EA /* MarketGlobalDefiMetricService.swift */, - 58AAAFE702C2E51EEE209C56 /* MarketListDefiDecorator.swift */, - ); - path = DefiCap; - sourceTree = ""; - }; - 58AAAC5B31C9DD58D57A3EA9 /* MarketGlobalMetric */ = { - isa = PBXGroup; - children = ( - 58AAA7B3EA0C8B9FDEC41837 /* MarketGlobalMetricService.swift */, - 58AAA775FE9B46DA2910F508 /* MarketGlobalMetricModule.swift */, - 58AAA02D981360FF0CC50A19 /* MarketGlobalMetricViewController.swift */, - 58AAAC23B9A9A22F8C485881 /* DefiCap */, - 58AAADC5A7211A14D2E41B6C /* TvlInDefi */, - ); - path = MarketGlobalMetric; - sourceTree = ""; - }; 58AAAC89F3E7CD9F799B89D7 /* Coin */ = { isa = PBXGroup; children = ( + 6B5F5E132C0DDD6000E03EB2 /* Rank */, D0D6933E270B28240077AF17 /* CoinOverview */, 11B354D11B72A988BA1D8F59 /* CoinMarkets */, 11B35C53664AC4D47BE3EDCA /* Analytics */, @@ -7715,7 +7383,6 @@ 58AAA353DAC061C2123948FC /* CoinPageViewController.swift */, 58AAA55A4A6A97C25F84034F /* CoinChartFactory.swift */, 58AAA444C885BCC354F1B7B3 /* CoinPageMarkdownParser.swift */, - 11B354B9064677BDA5946B48 /* Ranks */, ABC9A04E5F5F2817B1E287A2 /* Indicators */, 11B3529DC8E74672659515B8 /* CoinPageViewModelNew.swift */, 11B3553967AFF40F6A9A611A /* CoinPageView.swift */, @@ -7723,17 +7390,6 @@ path = Coin; sourceTree = ""; }; - 58AAADC5A7211A14D2E41B6C /* TvlInDefi */ = { - isa = PBXGroup; - children = ( - 58AAA8E1106E31D68FD9181D /* MarketGlobalTvlMetricViewController.swift */, - 58AAADBB7B760C189AD6032F /* MarketGlobalTvlMetricService.swift */, - 58AAA9B26F62DB74FF3830D5 /* MarketListTvlDecorator.swift */, - 58AAAEA0582FFB81EB6C6263 /* MarketGlobalTvlFetcher.swift */, - ); - path = TvlInDefi; - sourceTree = ""; - }; 58AAAED208A8D02F3BC8B828 /* Adapters */ = { isa = PBXGroup; children = ( @@ -7799,6 +7455,24 @@ path = WidgetCoinAppShowWorker; sourceTree = ""; }; + 6B5F5E132C0DDD6000E03EB2 /* Rank */ = { + isa = PBXGroup; + children = ( + 6B5F5E142C0DDD7100E03EB2 /* RankView.swift */, + 6B5F5E172C0DDD8700E03EB2 /* RankViewModel.swift */, + ); + path = Rank; + sourceTree = ""; + }; + 6BB14F782C05FAA600E879B2 /* Tvl */ = { + isa = PBXGroup; + children = ( + 6BB14F792C05FAB600E879B2 /* MarketTvlView.swift */, + 6BB14F7A2C05FAB600E879B2 /* MarketTvlViewModel.swift */, + ); + path = Tvl; + sourceTree = ""; + }; 6BCD52F62A161F4100993F20 /* ICloud */ = { isa = PBXGroup; children = ( @@ -8086,6 +7760,8 @@ children = ( ABC9A22311B6AA64B7D93CB4 /* DataSourceChain.swift */, ABC9AFF00631C853B04007AC /* WalletTokenBalance */, + D0A6902A2C00ACF600E59296 /* CautionDataSource.swift */, + D0A6902D2C04969300E59296 /* CautionDataSourceViewModel.swift */, ); path = DataSources; sourceTree = ""; @@ -8140,6 +7816,7 @@ ABC9AB43B35D4DE04BDB5734 /* AlertView.swift */, ABC9A77CEB528EACD0244E73 /* ActionSheetModifier.swift */, ABC9AA428B57E9309477EC96 /* TransparentFullScreenCover.swift */, + 6B8BD39D2C11B959003ADE10 /* TextFieldAlert.swift */, ); path = Alert; sourceTree = ""; @@ -8181,7 +7858,6 @@ ABC9AD43BE81A12A368EDF16 /* Platforms */, ABC9AB6B4E6060E3A4D28409 /* MemoInput */, 2FA5D1C5F34C09BA38AC37B2 /* Settings */, - 11B359040ACA861F4788E983 /* SwiftUI */, ABC9AEFF9518D63C47B10844 /* UnspentOutputs */, ); path = Send; @@ -8570,7 +8246,7 @@ ABC9A6E3A891AD336A1A5326 /* Zcash */, ABC9AD1F2311CC6425CF9D90 /* SendModule.swift */, ABC9A48552CF0C90E22686A9 /* SendBaseService.swift */, - ABC9AAF2ADD900F32D87C7BE /* SendViewModel.swift */, + ABC9AAF2ADD900F32D87C7BE /* SendViewModelOld.swift */, ABC9ABE97578DC667CBDC11A /* BaseSendViewController.swift */, 11B35805423FC0A1C859B08E /* Ton */, ); @@ -8849,6 +8525,14 @@ path = Views; sourceTree = ""; }; + D033289D2BF6196E00BBB364 /* InfoNew */ = { + isa = PBXGroup; + children = ( + D033289E2BF6199600BBB364 /* InfoNewView.swift */, + ); + path = InfoNew; + sourceTree = ""; + }; D04D98ED268061EF001A3135 /* Bitcoin */ = { isa = PBXGroup; children = ( @@ -8969,6 +8653,8 @@ D0B462F12B5A470F0027815D /* FeeSettings */ = { isa = PBXGroup; children = ( + D0DEFF032BD1253B004C9DF0 /* BitcoinFeeSettingsView.swift */, + D0DEFF022BD1253B004C9DF0 /* BitcoinFeeSettingsViewModel.swift */, D06A171A2BA1B1BC0081E312 /* FeeSettingsViewHelper.swift */, ABC9ABCA9E26E2B6A5E0D43E /* Eip1559FeeSettingsView.swift */, ABC9A8E02C3486492E3B12F2 /* Eip1559FeeSettingsViewModel.swift */, @@ -9046,6 +8732,58 @@ path = Transactions; sourceTree = ""; }; + D311DA1A2BCD28C10013DB8F /* SendNew */ = { + isa = PBXGroup; + children = ( + D0B462F12B5A470F0027815D /* FeeSettings */, + 11B355166E437B7ADB8B8EBA /* ValueLevel.swift */, + 11B3542B6FE4B4F0C0B65369 /* FeeData.swift */, + D0DEFF092BD1257F004C9DF0 /* BitcoinFeeData.swift */, + D0DEFF082BD1257E004C9DF0 /* BitcoinTransactionService.swift */, + 11B35B91B5EAF2E193FDC04E /* SendView.swift */, + 11B35BEF6F80BDF166173819 /* SendViewModel.swift */, + 11B35B0F9BC5C4CDBC5B041D /* EvmSendHandler.swift */, + 11B35D8B730D82D948B27210 /* ISendHandler.swift */, + 11B35613DBD19C0B86D83B49 /* BaseSendEvmData.swift */, + 11B355A2FFA369C4E89DFF53 /* ISendData.swift */, + 11B359C9E2EFD391FB848618 /* SendField.swift */, + 11B353B02ADF5EC5CC83FB33 /* SendHandlerFactory.swift */, + 11B3562F83BCEE2720B1C23F /* ITransactionService.swift */, + 11B3564FC860C383807BFBE3 /* EvmTransactionService.swift */, + 11B352D4978C011048EC985F /* TransactionServiceFactory.swift */, + 11B3514BFFAE3CE09F4DB2EA /* TransactionSettings.swift */, + 11B35B1EC5A29D1B77C1BCB6 /* EvmFeeData.swift */, + 11B359F3E7E0DB7A55C079CD /* EvmFeeEstimator.swift */, + D3DD67392BC3CFF300EC7F78 /* BaseSendBtcData.swift */, + 11B35AD24681D0A122E6A3C5 /* SendData.swift */, + 11B353355FF7FBE72BF60981 /* PreSendView.swift */, + 11B357FD9D760D9671A3DF24 /* PreSendViewModel.swift */, + D3B73E262BDBC6120067429D /* IPreSendHandler.swift */, + D3B73E292BDBC61D0067429D /* EvmPreSendHandler.swift */, + D0E5E84E2BE22172005080A4 /* BitcoinSendHandler.swift */, + D0E5E8512BE260C8005080A4 /* BitcoinPreSendHandler.swift */, + D3F9B0242BE38AF1009FFA95 /* RegularSendView.swift */, + D3F9B0302BE3B39D009FFA95 /* EvmDecoration.swift */, + D3F9B0332BE3B3A7009FFA95 /* EvmDecorator.swift */, + D3F9B0362BE3B5AA009FFA95 /* WalletConnectSendView.swift */, + D3F9B0392BE3BB36009FFA95 /* WalletConnectSendViewModel.swift */, + D3A580872BE4DAA2003953F4 /* EvmSendData.swift */, + D3A5808A2BE4DB11003953F4 /* WalletConnectSendHandler.swift */, + D0E5E8542BE38AA2005080A4 /* TronSendHandler.swift */, + D054DAE22BE5123F0040B7C9 /* InitialTransactionSettings.swift */, + D3A580932BE8AA80003953F4 /* BitcoinSendSettingsView.swift */, + D3A580962BE8AA90003953F4 /* BitcoinSendSettingsViewModel.swift */, + D34903162BE8DF48005F147B /* BinanceSendHandler.swift */, + D34903192BE8DF5F005F147B /* BinancePreSendHandler.swift */, + D31369852BEA187E00BA6B5B /* ZcashSendHandler.swift */, + D31369882BEA188D00BA6B5B /* ZcashPreSendHandler.swift */, + D084F6BD2BEB94F700407FA4 /* OutputSelectView2.swift */, + D084F6C02BEB951C00407FA4 /* OutputSelectorViewModel2.swift */, + D03F74812BF76D0A004FBCFA /* GasPriceData.swift */, + ); + path = SendNew; + sourceTree = ""; + }; D3285F3920BD158E00644076 = { isa = PBXGroup; children = ( @@ -9093,9 +8831,95 @@ path = UnstoppableWallet; sourceTree = ""; }; + D3384D0E2BFDCBD100515664 /* Etf */ = { + isa = PBXGroup; + children = ( + D3384D0F2BFDCBDE00515664 /* MarketEtfView.swift */, + D3384D122BFDCBE700515664 /* MarketEtfViewModel.swift */, + ); + path = Etf; + sourceTree = ""; + }; + D3384D182BFF0C9900515664 /* MarketCap */ = { + isa = PBXGroup; + children = ( + D3384D192BFF0CAF00515664 /* MarketMarketCapView.swift */, + D3384D1C2BFF0CB800515664 /* MarketMarketCapViewModel.swift */, + ); + path = MarketCap; + sourceTree = ""; + }; + D3384D1F2BFF0CBD00515664 /* Volume */ = { + isa = PBXGroup; + children = ( + D3384D202BFF0CCA00515664 /* MarketVolumeView.swift */, + D3384D232BFF0CD100515664 /* MarketVolumeViewModel.swift */, + ); + path = Volume; + sourceTree = ""; + }; + D36E50882BF7656E00C361BD /* Watchlist */ = { + isa = PBXGroup; + children = ( + D36E50892BF76FA700C361BD /* WatchlistEntry.swift */, + D36E508C2BF76FB400C361BD /* WatchlistProvider.swift */, + ); + path = Watchlist; + sourceTree = ""; + }; + D3833AD52BEE1A2900ACECFB /* Watchlist */ = { + isa = PBXGroup; + children = ( + D3402AF02BF5D59D003BF6F8 /* WatchlistModifier.swift */, + D3402AED2BF5D58B003BF6F8 /* WatchlistViewModel.swift */, + D3833AD62BEE1A7900ACECFB /* MarketWatchlistView.swift */, + D3833AD92BEE1A8300ACECFB /* MarketWatchlistViewModel.swift */, + D3384D082BFCB43800515664 /* MarketWatchlistSignalsView.swift */, + D3384D0B2BFCB8F000515664 /* MarketWatchlistSignalBadge.swift */, + ); + path = Watchlist; + sourceTree = ""; + }; + D3833ADC2BEE3FC200ACECFB /* Platforms */ = { + isa = PBXGroup; + children = ( + D3833ADD2BEE3FE000ACECFB /* MarketPlatformsView.swift */, + D3833AE02BEE3FE800ACECFB /* MarketPlatformsViewModel.swift */, + ); + path = Platforms; + sourceTree = ""; + }; + D3833AEC2BF1F0AC00ACECFB /* Platform */ = { + isa = PBXGroup; + children = ( + 6B5F5E0D2C0C65F700E03EB2 /* MarketPlatformViewNew.swift */, + 6B5F5E102C0C660900E03EB2 /* MarketPlatformViewModel.swift */, + ); + path = Platform; + sourceTree = ""; + }; + D3833AF02BF20B7200ACECFB /* Pairs */ = { + isa = PBXGroup; + children = ( + D3833AF12BF20B8600ACECFB /* MarketPairsView.swift */, + D3833AF42BF20B8D00ACECFB /* MarketPairsViewModel.swift */, + ); + path = Pairs; + sourceTree = ""; + }; + D3833AFA2BF335B800ACECFB /* News */ = { + isa = PBXGroup; + children = ( + D3833AFB2BF335C700ACECFB /* MarketNewsView.swift */, + D3833AFE2BF335D100ACECFB /* MarketNewsViewModel.swift */, + ); + path = News; + sourceTree = ""; + }; D3948EF52ADA846400FAE566 /* Widget */ = { isa = PBXGroup; children = ( + D36E50882BF7656E00C361BD /* Watchlist */, D3948EF62ADA846400FAE566 /* AppWidgetBundle.swift */, D3948EFA2ADA846800FAE566 /* Assets.xcassets */, D3948EFC2ADA846800FAE566 /* Info.plist */, @@ -9108,6 +8932,7 @@ 11B35B0879F715C0777919AA /* WatchlistWidget.swift */, 11B35E298D53B8A2C2684119 /* AppWidgetConstants.swift */, D350DDAF2AE2526E00CF1989 /* Localizable.xcstrings */, + D36E50922BF7852D00C361BD /* CoinListView.swift */, ); path = Widget; sourceTree = ""; @@ -9121,6 +8946,65 @@ path = IntentExtension; sourceTree = ""; }; + D3D13A5D2C0D9D9A002484BC /* AdvancedSearch */ = { + isa = PBXGroup; + children = ( + D311DA212BD23C230013DB8F /* MarketAdvancedSearchView.swift */, + D3D13A5E2C0D9DCB002484BC /* MarketAdvancedSearchViewModel.swift */, + D389BC4B2C0DDCF500724504 /* MarketAdvancedSearchBlockchainsView.swift */, + D389BC4E2C0DEF1800724504 /* MarketAdvancedSearchResultsView.swift */, + D389BC512C0DEF2200724504 /* MarketAdvancedSearchResultsViewModel.swift */, + ); + path = AdvancedSearch; + sourceTree = ""; + }; + D3DB51AD2BD7A9740091BBDB /* Coins */ = { + isa = PBXGroup; + children = ( + D3DB51A72BD787490091BBDB /* MarketCoinsView.swift */, + D3DB51AA2BD787A00091BBDB /* MarketCoinsViewModel.swift */, + ); + path = Coins; + sourceTree = ""; + }; + D3EE4A432C0EE43000E40C97 /* Fetchers */ = { + isa = PBXGroup; + children = ( + 58AAA0A9EA8A2210522F38EE /* MarketGlobalFetcher.swift */, + 1A564A1B86DF22E86F0BB442 /* TopPlatformMarketCapFetcher.swift */, + 6BB14F712BFE550600E879B2 /* MarketEtfFetcher.swift */, + 58AAA7D7F06F7C044DF9CE0A /* MarketGlobalModule.swift */, + ); + path = Fetchers; + sourceTree = ""; + }; + D3EE4A442C0EEE5700E40C97 /* Search */ = { + isa = PBXGroup; + children = ( + D3DB51982BD63D680091BBDB /* MarketSearchViewModel.swift */, + D3DB519E2BD6854A0091BBDB /* MarketSearchView.swift */, + ); + path = Search; + sourceTree = ""; + }; + D3EE4A452C0EEE6500E40C97 /* Global */ = { + isa = PBXGroup; + children = ( + D311DA1E2BD115240013DB8F /* MarketGlobalViewModel.swift */, + D3DB51A12BD6857E0091BBDB /* MarketGlobalView.swift */, + ); + path = Global; + sourceTree = ""; + }; + D3EE4A462C0EEE7100E40C97 /* Tab */ = { + isa = PBXGroup; + children = ( + D3DB51A42BD685B40091BBDB /* MarketTabView.swift */, + D3833B012BF38A8000ACECFB /* MarketTabViewModel.swift */, + ); + path = Tab; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -9180,6 +9064,7 @@ D3E323C72AE7B8E400F73914 /* KeychainAccess */, 6B55E33C2AF26D7A00616B60 /* Starscream */, D08C93B02B91E3B400A7D1D5 /* Hodler */, + 6BBCE4A22BDA419200ABBD55 /* Web3Wallet */, ); productName = Wallet; productReference = D38405CE218317DF007D50AD /* Unstoppable D.app */; @@ -9241,6 +9126,7 @@ D3E323C92AE7B8F400F73914 /* KeychainAccess */, 6B55E33A2AF26D6400616B60 /* Starscream */, D08C93AE2B91E39E00A7D1D5 /* Hodler */, + 6BBCE4A42BDA419B00ABBD55 /* Web3Wallet */, ); productName = Wallet; productReference = D38406BE21831B3D007D50AD /* Unstoppable.app */; @@ -9511,12 +9397,14 @@ D3840583218317DF007D50AD /* BlurManager.swift in Sources */, D3840585218317DF007D50AD /* MainSettingsModule.swift in Sources */, D3840586218317DF007D50AD /* MainSettingsViewModel.swift in Sources */, + D3B73E312BDFC5580067429D /* PriceRow.swift in Sources */, D3840588218317DF007D50AD /* MainSettingsService.swift in Sources */, D3840589218317DF007D50AD /* MainSettingsViewController.swift in Sources */, D384058B218317DF007D50AD /* LanguageManager.swift in Sources */, D384058C218317DF007D50AD /* LocalStorage.swift in Sources */, D023D26B2A24CD16004F65B0 /* BaseTronAdapter.swift in Sources */, D384058F218317DF007D50AD /* SecuritySettingsModule.swift in Sources */, + D0DEFF052BD1253C004C9DF0 /* BitcoinFeeSettingsViewModel.swift in Sources */, D3840592218317DF007D50AD /* SecuritySettingsViewModel.swift in Sources */, D3840593218317DF007D50AD /* BitcoinAdapter.swift in Sources */, D384059A218317DF007D50AD /* KeyboardObservingViewController.swift in Sources */, @@ -9525,6 +9413,7 @@ D38405A1218317DF007D50AD /* Date.swift in Sources */, D38405A2218317DF007D50AD /* UIAlertController.swift in Sources */, D38405A5218317DF007D50AD /* DateHelper.swift in Sources */, + D3DB51A62BD685B40091BBDB /* MarketTabView.swift in Sources */, D02A67CC272A7460009B2C1C /* TwitterUsersResponse.swift in Sources */, D38405A6218317DF007D50AD /* SystemInfoManager.swift in Sources */, D38405A7218317DF007D50AD /* PermissionsHelper.swift in Sources */, @@ -9538,15 +9427,16 @@ 11B357FE4C2E1EC8E26ED68F /* StorageMigrator.swift in Sources */, 11B35406710372339144C92D /* AuthData.swift in Sources */, 5046E672259C491100A941E5 /* InfoSeparatorHeaderView.swift in Sources */, + D084F6BF2BEB94F700407FA4 /* OutputSelectView2.swift in Sources */, 3C7B99D3D6AE0B1C0D8E5F09 /* UrlManager.swift in Sources */, 58AAA3BE2D0DEF53CE6CEE97 /* EvmKitManager.swift in Sources */, + 6B5F5E0F2C0C65F700E03EB2 /* MarketPlatformViewNew.swift in Sources */, 58AAABE8E8374ED4211F610C /* Eip20Adapter.swift in Sources */, 58AAA4A377F356194AE08055 /* BaseEvmAdapter.swift in Sources */, 1A56415A4BB89B9156C6442D /* Decimal.swift in Sources */, 11B35D54818399B4BCE9F2C2 /* UnlinkViewController.swift in Sources */, D36DE0E5272FD887000BC916 /* OneInchService.swift in Sources */, 11B35F66D2561CD9555C8857 /* UnlinkModule.swift in Sources */, - 3A73FC9E258B1AF700FE4D34 /* MarketWatchlistModule.swift in Sources */, 11B35B99C84075296D6F26DE /* UnlinkViewModel.swift in Sources */, 11B35177B650540BCEA880B3 /* UnlinkService.swift in Sources */, 1A5645335BEED7A26D53A6B9 /* AddressUri.swift in Sources */, @@ -9555,13 +9445,14 @@ 1A56475828E9121D78E02D67 /* BitcoinCashAdapter.swift in Sources */, 11B352E00B6E0DF4F6D42486 /* DashAdapter.swift in Sources */, 11B359F074038D1507C23747 /* EnabledWallet.swift in Sources */, - 3A73FCB1258B1AFD00FE4D34 /* GradientPercentCircle.swift in Sources */, 11B358B0576F63BE43947DD5 /* Account.swift in Sources */, 11B35BEB439509EACB41AB06 /* AccountType.swift in Sources */, 11B35F663F7E12BFDDE3C88B /* AccountManager.swift in Sources */, + D3384D0D2BFCB8F000515664 /* MarketWatchlistSignalBadge.swift in Sources */, 11B353AA4AFFB020A68E09B6 /* AccountFactory.swift in Sources */, 3C7B9BAF807355796DCA80C4 /* WelcomeScreenViewController.swift in Sources */, 11B35BCD6D0462E31D7EBA06 /* BackupManager.swift in Sources */, + D0A6902F2C04969300E59296 /* CautionDataSourceViewModel.swift in Sources */, 11B358D1687049E5DACEBC96 /* AppManager.swift in Sources */, D31C4760238BF176008CB818 /* MnemonicDerivation.swift in Sources */, 11B356C6FEEFD7A1B854FB46 /* AccountRecord.swift in Sources */, @@ -9583,11 +9474,11 @@ D003297726CD2C67002EC21D /* TransactionLockInfo.swift in Sources */, 2FA5DB3B49EF898B63DD0F94 /* KitCleaner.swift in Sources */, 2FA5D98D7EE8F90744430243 /* TimeInterval.swift in Sources */, - 3AB682BF25BADD97002197A5 /* MarketOverviewModule.swift in Sources */, 11B3538BB9A4C913033D250F /* FeeCoinProvider.swift in Sources */, D02A67BD272A7460009B2C1C /* TweetCell.swift in Sources */, 11B3527D20636D21F0F45C80 /* CurrentDateProvider.swift in Sources */, 11B35DA5492B0C4EC7130A19 /* AppConfig.swift in Sources */, + D3833AF32BF20B8600ACECFB /* MarketPairsView.swift in Sources */, 11B350B22CCCFCD466BEB808 /* FeeRateProvider.swift in Sources */, 6BCD53172A161F4800993F20 /* BackupViewController.swift in Sources */, 11B35B3F384758B223A7218C /* MainSettingsFooterCell.swift in Sources */, @@ -9600,7 +9491,6 @@ 58AAAD3A237C3A2F46FD2509 /* DebugPresenter.swift in Sources */, 58AAA0BEEC3EF61DCB80C6BA /* DebugViewController.swift in Sources */, 58AAAD3AC2B87E6AAFE535D8 /* DebugLogger.swift in Sources */, - 3A73FCAE258B1AFD00FE4D34 /* MarketMetricView.swift in Sources */, 1A564EDBD3E4B37299E199B7 /* AppStatusModule.swift in Sources */, 11B3557CB2595D2884C94498 /* MultiTextMetricsView.swift in Sources */, 1A564F73B7FE144D39DEA34F /* UIDevice.swift in Sources */, @@ -9615,6 +9505,7 @@ 58AAA34F0F6195DF86596A41 /* ChartConfiguration.swift in Sources */, 58AAA75358DF98C1D7191B81 /* DoubleSpendInfoViewController.swift in Sources */, 58AAA747269D5AE1BBDDA2F7 /* LastBlockInfo.swift in Sources */, + D3384D252BFF0CD100515664 /* MarketVolumeViewModel.swift in Sources */, 58AAA4A4D0D7398E7184E7AB /* UITextView.swift in Sources */, 5039F973269C5A9B004711B8 /* ReleaseNotesViewController.swift in Sources */, 58AAA12167F3BC03D0FA55DF /* LockDelegate.swift in Sources */, @@ -9627,6 +9518,7 @@ 58AAA0C0DB5C901FE58F5F9B /* FeeSliderWrapper.swift in Sources */, 11B3545A8A2A23ADB5BA1E0A /* WelcomeTextView.swift in Sources */, 1A56424B5327D4ADFD728B0B /* BlockchainSettingRecord.swift in Sources */, + D3B73E282BDBC6120067429D /* IPreSendHandler.swift in Sources */, D3DD67292BC3BF4700EC7F78 /* OneInchMultiSwapQuote.swift in Sources */, 1A564CBB4708B7DAC6D6A2AD /* BlockchainSettingsStorage.swift in Sources */, 1A5646E67996AB355694A35E /* EnabledWallet_v_0_13.swift in Sources */, @@ -9642,13 +9534,13 @@ D36DE0C1272FD864000BC916 /* UniswapService.swift in Sources */, 11B35CB5A90FCD0B53D59140 /* AlertViewController.swift in Sources */, 11B35C8A76BB5C69263E9757 /* AlertModule.swift in Sources */, - 3A73FC7A258B1AE500FE4D34 /* MarketViewModel.swift in Sources */, 11B35383F24853BDDB27618A /* AlertPresenter.swift in Sources */, 11B35D835774B7F4E286BF32 /* AlertRouter.swift in Sources */, 11B35996BB3F179501DC0B08 /* BottomSheetTitleView.swift in Sources */, 1A5646F57C3677EBC462673C /* AppError.swift in Sources */, 1A564E0B7DB0060B9600FCB1 /* BalanceErrorViewController.swift in Sources */, 1A5643A263F288A4E83409FA /* BalanceErrorViewModel.swift in Sources */, + D03328A02BF6199600BBB364 /* InfoNewView.swift in Sources */, 1A564504E164177DD6EECFBA /* BalanceErrorService.swift in Sources */, 1A564168590F031AA453E1D1 /* BalanceErrorModule.swift in Sources */, 11B351FB99274553725754E4 /* GuidesModule.swift in Sources */, @@ -9690,7 +9582,6 @@ 11B3533941A80D693369E9C0 /* BrandFooterCell.swift in Sources */, 58AAAAF8C20AB0E0299A36B8 /* CoinSelectModule.swift in Sources */, 58AAA9AEFE1043B01BEC2D6A /* CoinSelectViewController.swift in Sources */, - 3A73FC75258B1ADB00FE4D34 /* MarketViewController.swift in Sources */, 58AAA8CECD9281330BE789B8 /* InfoViewController.swift in Sources */, 58AAA410C9996BA929E3CEEF /* InfoModule.swift in Sources */, D05E96982A262149002CCD71 /* TronTransactionRecord.swift in Sources */, @@ -9711,6 +9602,7 @@ 58AAA0D14CD9EDAE2DBF7540 /* SwapApproveViewController.swift in Sources */, 58AAAAD2D124AA0323629B0E /* SwapApproveModule.swift in Sources */, 58AAAFDBA357FC699C07C334 /* AdditionalDataWithErrorView.swift in Sources */, + D3B73E2B2BDBC61D0067429D /* EvmPreSendHandler.swift in Sources */, 58AAA9053CD38F13CD944E2A /* SwapApproveAmountView.swift in Sources */, 58AAAFE644C1B236B9714B47 /* CoinSelectViewModel.swift in Sources */, 50701ACF25B041E600EDE51B /* JailbreakTestManager.swift in Sources */, @@ -9719,6 +9611,7 @@ 11B355F32686B8689B4EC105 /* WalletConnectRequest.swift in Sources */, 6B2907222AF0CB8A006157D6 /* WalletConnectAppShowModule.swift in Sources */, D008CA5B267C8DDF00001E0A /* EvmIncomingTransactionRecord.swift in Sources */, + D0DEFF0B2BD1257F004C9DF0 /* BitcoinTransactionService.swift in Sources */, 11B35C2FBF875F81E13CC575 /* CoinService.swift in Sources */, 1A5647072B937BE4B69FFA1D /* SendEthereumErrorCell.swift in Sources */, 58AAAAE261DEB08128441641 /* AmountDecimalParser.swift in Sources */, @@ -9745,7 +9638,6 @@ D0A980B02B60E73F00127AF4 /* LegacyFeeSettingsView.swift in Sources */, D0C226142A66A3DB007101F7 /* PersonalSupportViewController.swift in Sources */, 11B35A4D9BD4B8C29FBAFACF /* AboutModule.swift in Sources */, - 3A73FC9C258B1AF700FE4D34 /* MarketWatchlistViewController.swift in Sources */, 11B35B086B0D62A9D7A10CD0 /* AddTokenViewModel.swift in Sources */, 11B35A42BF19B93C6005FBD9 /* AddTokenService.swift in Sources */, 11B35D550563934444558D15 /* AddTokenViewController.swift in Sources */, @@ -9769,10 +9661,8 @@ 11B3547B32F2E3458065F2EB /* AmountInputView.swift in Sources */, 11B354B34C4B6471F67F5471 /* InputPrefixWrapperView.swift in Sources */, D3DD67352BC3CC2100EC7F78 /* ThorChainMultiSwapBtcQuote.swift in Sources */, - 179E746F1E3D7BC613BD0AFC /* FavoriteCoinRecord.swift in Sources */, 11B35A42D28B8BC4CDA57D8E /* AccountRecord_v_0_19.swift in Sources */, - 58AAAA8975F5B63340672D00 /* MarketWatchlistService.swift in Sources */, - 11B35B970E8949F968960796 /* MarketListViewController.swift in Sources */, + 6B8BD39F2C11B959003ADE10 /* TextFieldAlert.swift in Sources */, D0E659BC2B875003000D8981 /* ResendBitcoinViewModel.swift in Sources */, 11B35B501A30615698B04C96 /* AddEvmTokenBlockchainService.swift in Sources */, 11B35E57C6406D2249A23E6F /* SendEvmTransactionService.swift in Sources */, @@ -9789,9 +9679,11 @@ 11B35E1A30BE0E0432B4A064 /* AmountInputViewModel.swift in Sources */, 11B3574A4715D307D9EFCF43 /* SendEvmConfirmationModule.swift in Sources */, 11B358C72B4E7F70331084AA /* SendEvmViewController.swift in Sources */, + D3DB51A32BD6857E0091BBDB /* MarketGlobalView.swift in Sources */, 11B35321C9FCFD1DFA4401A3 /* SendEvmService.swift in Sources */, 11B3564C551C6C76ECCE387D /* SendEvmViewModel.swift in Sources */, 11B35FF681C01782693B3C4A /* SendEvmConfirmationViewController.swift in Sources */, + D084F6C22BEB951C00407FA4 /* OutputSelectorViewModel2.swift in Sources */, 11B35249863079AB40D81F62 /* SendAvailableBalanceCell.swift in Sources */, 11B35AF2A7175246F8586F09 /* SendEvmModule.swift in Sources */, 11B35972FDF15D690466B792 /* SendAvailableBalanceViewModel.swift in Sources */, @@ -9804,7 +9696,6 @@ 1A564DC4E55C1BE1C4D4CDA9 /* HighlightedDescriptionBaseView.swift in Sources */, 1A564977D009E610D8D194AE /* TitledHighlightedDescriptionView.swift in Sources */, 1A5642F3C5C414F65A3CC59D /* TitledHighlightedDescriptionCell.swift in Sources */, - 58AAAEF20C684F912ED5D7AE /* FavoritesManager.swift in Sources */, 58AAAFEB4507E7459BED2F28 /* CoinPageModule.swift in Sources */, 58AAA691DC0544582BABF263 /* CoinChartService.swift in Sources */, 58AAABED534A95A11B332D44 /* CoinChartViewModel.swift in Sources */, @@ -9814,6 +9705,7 @@ 1A564D209D3AFA40F808C8FB /* PerformanceContentCollectionViewCell.swift in Sources */, 1A564E8CA0CFBE8B1E232B60 /* PerformanceTableViewCell.swift in Sources */, 1A564BAB51E12A8F37D870B5 /* PerformanceSideCollectionViewCell.swift in Sources */, + D3833B002BF335D100ACECFB /* MarketNewsViewModel.swift in Sources */, 58AAAA71882CB345D56BBA00 /* CoinChartFactory.swift in Sources */, 11B35DD9C17FDD3ED40BA321 /* CoinInvestorsModule.swift in Sources */, 11B358D913A404C1DA7D4E0E /* CoinInvestorsViewModel.swift in Sources */, @@ -9841,16 +9733,20 @@ 11B35D7BE3B34B0171EC90A8 /* ManageAccountsModule.swift in Sources */, 11B35AE0661AD6EA5844D50B /* ManageAccountsViewModel.swift in Sources */, 11B35DFFC539A1E72382C8F7 /* ManageAccountsService.swift in Sources */, + D3DB519D2BD685180091BBDB /* RedactedModifier.swift in Sources */, 11B35518B24EDB088463A7A4 /* ManageAccountModule.swift in Sources */, 11B3523E47942D2118DBC290 /* ManageAccountService.swift in Sources */, 11B356476D5E88F21C297B52 /* ManageAccountViewController.swift in Sources */, 11B350860CB79E9C5F032166 /* ManageAccountViewModel.swift in Sources */, 11B3563E71C4AC16DFE8AB76 /* ActiveAccount.swift in Sources */, 6BCD530D2A161F4100993F20 /* ICloudBackupNameViewModel.swift in Sources */, + D3833AF92BF2181800ACECFB /* MarketPair.swift in Sources */, 11B35056B69A06C8CFF3CBB6 /* BackupModule.swift in Sources */, + D349031B2BE8DF5F005F147B /* BinancePreSendHandler.swift in Sources */, D0A980AA2B5E3C0900127AF4 /* StepChangeButtonsView.swift in Sources */, 11B3598BE9C7A456A70B5DFD /* BackupVerifyWordsViewModel.swift in Sources */, 6BE8A0822ADE2FAB0012DE7F /* CurrencyManager.swift in Sources */, + D3833ADF2BEE3FE000ACECFB /* MarketPlatformsView.swift in Sources */, 11B35828C42630241AE8D0E0 /* BackupVerifyWordsService.swift in Sources */, 11B3534EF58DAC9E15DC49A5 /* BackupVerifyWordsViewController.swift in Sources */, 11B35E08C957B79CF373E9FB /* BackupVerifyWordsModule.swift in Sources */, @@ -9866,6 +9762,7 @@ D003297B26CD2E89002EC21D /* TransactionDateHeaderView.swift in Sources */, 11B35BF47E99D33FE20AA279 /* EnabledWallet_v_0_20.swift in Sources */, 11B3585AC6E5D92F98A71758 /* RestoreSettingRecord.swift in Sources */, + D3A5808C2BE4DB11003953F4 /* WalletConnectSendHandler.swift in Sources */, 11B357388B82489E53C13772 /* RestoreSettingsManager.swift in Sources */, 11B3527F2E2D46DC307E6D3D /* RestoreSettingsViewModel.swift in Sources */, D09200C3293F21720091981A /* RestoreNonStandardViewController.swift in Sources */, @@ -9876,7 +9773,6 @@ 11B354EFE4620A8E65D44335 /* WalletViewModel.swift in Sources */, 11B35E5DDFA437BD43717962 /* WalletViewController.swift in Sources */, 11B352C447693BA4688A9673 /* WalletModule.swift in Sources */, - 505E7F742897C6DA00229BF2 /* TopPlatformService.swift in Sources */, 11B35A5CCED22BDE4C93CE23 /* WalletBlockchainElementService.swift in Sources */, 11B350B3D287EE732007892B /* WalletCoinPriceService.swift in Sources */, 11B355EBC83D70F2D41D9217 /* WalletViewItemFactory.swift in Sources */, @@ -9896,6 +9792,7 @@ 1A5645DA5F609D469D12A6E1 /* AppVersionRecord.swift in Sources */, 1A56404471A7270782D6619F /* AppVersion_v_0_20.swift in Sources */, 1A564D86E3D7E200D9B81592 /* AppVersionStorage.swift in Sources */, + D389BC4A2C0DDA8F00724504 /* HsTimePeriod.swift in Sources */, 11B35E165D6681B849F9A934 /* TextFieldStackView.swift in Sources */, 11B3560586CBAB617211F003 /* Caution.swift in Sources */, 11B359413C92A83580A130B3 /* MnemonicInputCell.swift in Sources */, @@ -9907,14 +9804,15 @@ 58AAA900E2644527A2C78863 /* MetricChartService.swift in Sources */, 58AAAEDDF791C28174360A15 /* MetricChartFactory.swift in Sources */, 58AAA4B64068280C684EE5C1 /* MetricChartModule.swift in Sources */, + 6BB14F7C2C05FBAD00E879B2 /* MarketTvlViewModel.swift in Sources */, 58AAA48ED47FD19F368385FA /* MetricChartViewController.swift in Sources */, 58AAAE020611495B6294ED96 /* MarketGlobalModule.swift in Sources */, + D3DB51B32BD912A00091BBDB /* MarketInfo.swift in Sources */, 11B3598D4F303310AC88FE90 /* BaseCurrencySettingsModule.swift in Sources */, D023D26E2A24CD4F004F65B0 /* TronKitManager.swift in Sources */, - 58AAAAED41A83519EFB94237 /* MarketPostViewModel.swift in Sources */, D00267BD2A57E72700D6B2D5 /* ResendPasteInputView.swift in Sources */, - 58AAA3FE5DE72D3CEFFE4399 /* MarketPostService.swift in Sources */, 1A564CFD8F22A2F5FDB346EA /* JailbreakService.swift in Sources */, + D3833AFD2BF335C700ACECFB /* MarketNewsView.swift in Sources */, D05E96A72A2627E5002CCD71 /* TronOutgoingTransactionRecord.swift in Sources */, 58AAA097F8417B30693547FE /* CoinPageMarkdownParser.swift in Sources */, 1A564BB34D0EAA2E8BE8B498 /* DeepLinkManager.swift in Sources */, @@ -9930,6 +9828,7 @@ D02A67C9272A7460009B2C1C /* CoinTweetsViewController.swift in Sources */, D36DE0DF272FD887000BC916 /* OneInchViewModel.swift in Sources */, D02A67C6272A7460009B2C1C /* CoinTweetsViewModel.swift in Sources */, + 6BB14F732BFE550600E879B2 /* MarketEtfFetcher.swift in Sources */, D09200C6293F21720091981A /* RestoreNonStandardViewModel.swift in Sources */, 11B35F20127C070137781ED5 /* AddTokenModule.swift in Sources */, 11B35D722A70E8B4776AB5A8 /* AddBep2TokenBlockchainService.swift in Sources */, @@ -9938,6 +9837,7 @@ 58AAA0444AD1508917D349CF /* UniswapSettingsService.swift in Sources */, D0C2261A2A66A703007101F7 /* PersonalSupportViewModel.swift in Sources */, 58AAA52F2D68C5C4397C4D10 /* UniswapSettings.swift in Sources */, + D34903182BE8DF48005F147B /* BinanceSendHandler.swift in Sources */, 58AAA81C7A95558DBD560C90 /* OneInchSettings.swift in Sources */, 6BCD53092A161F4100993F20 /* ICloudBackupNameService.swift in Sources */, 58AAA5A70BBDBD3A9D572261 /* OneInchSettingsService.swift in Sources */, @@ -9949,7 +9849,9 @@ D0D5BCBD2976CB9F00587FDB /* PasswordInputView.swift in Sources */, 58AAA2CEB9DB7E34921D7778 /* SwapDeadlineViewModel.swift in Sources */, 58AAAF041A3FE53A28893E74 /* RecipientAddressViewModel.swift in Sources */, + D086A9172BF4D08400462024 /* SendParameters.swift in Sources */, D36DE0E2272FD887000BC916 /* OneInchProvider.swift in Sources */, + D3DB51A02BD6854A0091BBDB /* MarketSearchView.swift in Sources */, D36DE100272FD92F000BC916 /* SwapSelectProviderViewModel.swift in Sources */, 58AAAAEDC64AE5716BC07673 /* SwapSlippageViewModel.swift in Sources */, 58AAA56C780EF5C92C1D1A32 /* AddressResolutionProvider.swift in Sources */, @@ -9973,6 +9875,7 @@ 11B35448AE945A8647EF4856 /* SwitchAccountModule.swift in Sources */, 58AAA1AAD335F236D130FCBB /* SwapConfirmationModule.swift in Sources */, 58AAAA3F2EF03D83A5500228 /* SwapConfirmationViewController.swift in Sources */, + D0A6902C2C00ACF600E59296 /* CautionDataSource.swift in Sources */, 58AAA3A6458CB87F359F6366 /* SwapConfirmationAmountCell.swift in Sources */, 2FA5D584622BFDB5E3A55771 /* TransactionInfoModule.swift in Sources */, 2FA5D8B40E95A9F8617EB4CA /* TransactionInfoService.swift in Sources */, @@ -10016,51 +9919,22 @@ 11B3555B8D452B7F64815FAC /* WalletStorage.swift in Sources */, 11B35205EDD1A11067E1AC91 /* CoinManager.swift in Sources */, 11B35FB28152F8881369DD9D /* AdapterManager.swift in Sources */, + D3384D522C0703B400515664 /* PriceChangeModeManager.swift in Sources */, + D389BC532C0DEF2200724504 /* MarketAdvancedSearchResultsViewModel.swift in Sources */, 11B350388CD7F33B10BD3F4B /* AdapterFactory.swift in Sources */, D02A67D2272A7460009B2C1C /* TweetsPageResponse.swift in Sources */, 11B358362F756E91646878D0 /* CoinValue.swift in Sources */, 2FA5D7CDF884D2655E066C3E /* TransactionValue.swift in Sources */, 58AAA3F0AFD0D0F5FCD24DEF /* SelectorButton.swift in Sources */, - 58AAAF886ADA156E5559EE5B /* MarketOverviewViewController.swift in Sources */, - 58AAA1B716FCD40947F4F95C /* MarketOverviewHeaderCell.swift in Sources */, - 58AAAAEC33A43B54E8E4D3FB /* MarketPostViewController.swift in Sources */, D05E969B2A26278D002CCD71 /* TronApproveTransactionRecord.swift in Sources */, - 58AAA5EF1B46CAFB40139AD2 /* MarketPostModule.swift in Sources */, - 11B35EC2E4E5614FF64C7246 /* MarketMultiSortHeaderView.swift in Sources */, 2FA5D4C4D13E610DA6009C48 /* CoinOverviewViewModel.swift in Sources */, D05E969E2A2627AF002CCD71 /* TronContractCallTransactionRecord.swift in Sources */, 2FA5D72F396BC5D16C228112 /* CoinOverviewService.swift in Sources */, 2FA5DF327EC188AC29719179 /* CoinOverviewModule.swift in Sources */, 2FA5D0F37E1B46988A88DB29 /* CoinOverviewViewController.swift in Sources */, - 11B35CD199C820EC89FBB546 /* MarketListViewModel.swift in Sources */, - 11B356CF55DB1BE22071B24E /* MarketMultiSortHeaderViewModel.swift in Sources */, - 11B352E46C24498018071705 /* MarketCategoryService.swift in Sources */, - 11B35664B1EDEAB99B7B51AE /* MarketCategoryModule.swift in Sources */, - 11B358F9D6842ECD84E80752 /* MarketCategoryViewModel.swift in Sources */, - 11B35DFCEC1D363B160479EE /* MarketTopService.swift in Sources */, 6B2907262AF0CB8A006157D6 /* WalletConnectAppShowView.swift in Sources */, - 11B35A5A820C1BCC1A92E944 /* MarketTopViewController.swift in Sources */, - 11B35A8BB87C68ACF4594C99 /* MarketTopModule.swift in Sources */, - 11B35FF6D36153F372C16C32 /* MarketWatchlistViewModel.swift in Sources */, - 58AAA488935A7DE6CF7C592D /* MarketGlobalMetricService.swift in Sources */, - 58AAA7B0CC093B05F7487496 /* MarketGlobalMetricModule.swift in Sources */, - 58AAA937A06DD40BD9A64C71 /* MarketGlobalMetricViewController.swift in Sources */, - 58AAAB9BE155C6F7630BCE31 /* MarketSingleSortHeaderView.swift in Sources */, - 58AAA76E5789D2C9EAC9A2B6 /* MarketSingleSortHeaderViewModel.swift in Sources */, D00DAE462B626C2900F48E1D /* GasPrice.swift in Sources */, - 11B35C2ED09C6D5660BB1236 /* MarketWatchlistToggleService.swift in Sources */, - 11B35F28C21E228AB3158716 /* MarketOverviewMetricsCell.swift in Sources */, - 11B357FDC1C6BD6C39FE6853 /* MarketAdvancedSearchResultModule.swift in Sources */, - 11B35F8FB24AB02560A1D018 /* MarketAdvancedSearchResultViewController.swift in Sources */, - 11B35E255AF804AEE43FF46A /* MarketAdvancedSearchResultService.swift in Sources */, - 11B35D3E3B10A4E92FD01172 /* MarketAdvancedSearchViewController.swift in Sources */, - 11B35F173689829256427A34 /* MarketAdvancedSearchViewModel.swift in Sources */, D36DE0AC272FD612000BC916 /* SwapViewController.swift in Sources */, - 11B3505A12674A5FF36D3CC8 /* MarketAdvancedSearchService.swift in Sources */, - 11B359B11871F76B25426D58 /* MarketAdvancedSearchModule.swift in Sources */, - 11B350F36947CF278CDB436B /* MarketListMarketFieldDecorator.swift in Sources */, - 58AAACF322E073F1DDA1FBDC /* MarketTvlSortHeaderViewModel.swift in Sources */, - 58AAAF56EF620164D797D60F /* MarketTvlSortHeaderView.swift in Sources */, D087627729815DAE00E6FFD4 /* ChooseWatchViewModel.swift in Sources */, 11B358E508ECA92493A9A3FD /* CoinPageService.swift in Sources */, 11B35FBC1AFDCF0DB8362C88 /* CoinAnalyticsModule.swift in Sources */, @@ -10068,31 +9942,24 @@ 11B35D06AFC1BAC63F25D271 /* CoinAnalyticsService.swift in Sources */, D0118E4C2B7CC63300D55CE6 /* ResendBitcoinViewController.swift in Sources */, 11B35D5BB556A490C6E13BA9 /* CoinAnalyticsViewModel.swift in Sources */, + D389BC4D2C0DDCF500724504 /* MarketAdvancedSearchBlockchainsView.swift in Sources */, 2FA5DF894D21F43F29615EAE /* TweetAttachmentView.swift in Sources */, 2FA5D0A03A0F625BCF184F4B /* TweetPollView.swift in Sources */, 2FA5DCBB263A029AD6F4780E /* ReferencedTweetView.swift in Sources */, 11B3518BEA8865CADA5DA684 /* LaunchScreenManager.swift in Sources */, D07157DC2A2DD968006F141F /* SendTronModule.swift in Sources */, 11B35A82532EC55909EFBAD8 /* LaunchScreen.swift in Sources */, + 6BB14F762C01D04200E879B2 /* CheckBoxUiView.swift in Sources */, 11B35B925CE6EB25DF542611 /* CoinInvestorsService.swift in Sources */, - 11B3545B8A5568792A4C43D8 /* CoinTreasuriesModule.swift in Sources */, - 11B357ADA154348A3C1A987B /* CoinTreasuriesViewController.swift in Sources */, 6BCD53052A161F4100993F20 /* ICloudBackupTermsViewController.swift in Sources */, - 11B3526D1747C11291F2D998 /* CoinTreasuriesService.swift in Sources */, 11B354FA6F6BF59F64560590 /* CoinTreasuriesViewModel.swift in Sources */, - 11B35F3C9850408B9ADE0B16 /* DropdownSortHeaderView.swift in Sources */, 11B351A2A88DF7E8BEF88DB0 /* CoinReportsViewController.swift in Sources */, 11B35E48433E1F44C3D3886C /* CoinReportsModule.swift in Sources */, 11B356A19A721D3557D7213C /* CoinReportsViewModel.swift in Sources */, 11B3547D1AEE88F583C3E5E5 /* CoinReportsService.swift in Sources */, 11B35F6092E0950714E277E4 /* PostCell.swift in Sources */, - 58AAAEA69AA123E96665E2B7 /* MarketGlobalDefiMetricService.swift in Sources */, - 58AAAB25F7EDB3EAE26E690C /* MarketGlobalTvlMetricViewController.swift in Sources */, - 58AAA4C8FEC03BAFB1B4863E /* MarketGlobalTvlMetricService.swift in Sources */, - 58AAA60557D3A9E3AE7372E0 /* MarketListDefiDecorator.swift in Sources */, - 58AAAA07DC05EF7F912EA184 /* MarketListTvlDecorator.swift in Sources */, - 58AAAFC5FE754A2286161D16 /* MarketGlobalTvlFetcher.swift in Sources */, - 11B354283B8AC609B65AADDF /* FavoriteCoinRecord_v_0_22.swift in Sources */, + D054DAE42BE5123F0040B7C9 /* InitialTransactionSettings.swift in Sources */, + 11B354283B8AC609B65AADDF /* FavoriteCoinRecord_v_0_38.swift in Sources */, 6BA5117E2BCFA06F00CB5A54 /* FirstAppearModifier.swift in Sources */, 58AAAD1BFFE70A777DDF27A9 /* AddressParserChain.swift in Sources */, 58AAAB9E86439B6DE1D22538 /* EvmAddressParserItem.swift in Sources */, @@ -10112,6 +9979,7 @@ 1A56408B1A402BC99846F141 /* ZcashAddressParserItem.swift in Sources */, 11B35EE0660C0CE24235E4DF /* NftDatabaseStorage.swift in Sources */, 11B359F28FAF97AD4F7F6424 /* NftPriceRecord.swift in Sources */, + D3A580952BE8AA80003953F4 /* BitcoinSendSettingsView.swift in Sources */, 11B35D24B98C73BD43CCD80B /* NftAssetRecord.swift in Sources */, 11B351767B71D6C42A23C518 /* NftCollectionRecord.swift in Sources */, 11B35C8D53F838E7E5CA6EEC /* NftStorage.swift in Sources */, @@ -10125,6 +9993,7 @@ 1A564DBDAA43925DC8E39164 /* TraitsCell.swift in Sources */, 1A56441AD76BA80CB74902CB /* TraitCell.swift in Sources */, ABC9A9CF9BC0D1D10C291FDD /* UIViewController.swift in Sources */, + D092C58C2C12DE3D0060D915 /* PriceChangeManager+HsTimePeriod.swift in Sources */, ABC9A40EB6EC886116806130 /* WalletConnectSessionManager.swift in Sources */, 2FA5DC4BBDF469873662BDEE /* Eip1559EvmFeeViewModel.swift in Sources */, 2FA5D3C3D3AB41202B592640 /* Eip1559EvmFeeDataSource.swift in Sources */, @@ -10133,6 +10002,7 @@ 11B359C05619611CBCFC89AC /* EvmBlockchainManager.swift in Sources */, ABC9A2542EA47C2ED85C06B9 /* WalletConnectListViewController.swift in Sources */, ABC9A7A9E27CC5F93BE5018B /* WalletConnectListService.swift in Sources */, + D3F9B0382BE3B5AA009FFA95 /* WalletConnectSendView.swift in Sources */, ABC9A5C2E2976341520D2F6D /* WalletConnectListModule.swift in Sources */, ABC9ADCDC949C4C63D1260DE /* WalletConnectListViewModel.swift in Sources */, ABC9A85FAA53B6FB01F94171 /* WalletConnectScanQrViewModel.swift in Sources */, @@ -10148,10 +10018,11 @@ ABC9AE6D877341985A6F651F /* SendBitcoinAmountInputService.swift in Sources */, ABC9A62EF59AF658C1DAD36F /* SendAmountCautionService.swift in Sources */, ABC9AF77EF53B4A7B0C0E55A /* SendAmountCautionViewModel.swift in Sources */, - 1A564CC4790F0CED826C131F /* MarketOverviewViewModel.swift in Sources */, 11B359C198AA7A141522E5E9 /* EvmAccountManagerFactory.swift in Sources */, + 6BB14F7E2C05FBB000E879B2 /* MarketTvlView.swift in Sources */, 11B350918797E615D4FF6677 /* BlockchainSettingRecordStorage.swift in Sources */, 11B35B7132B99D12DC745064 /* BtcBlockchainSettingsModule.swift in Sources */, + 6B5F5E192C0DDD8700E03EB2 /* RankViewModel.swift in Sources */, 6B2907202AF0CB8A006157D6 /* WalletConnectAppShowService.swift in Sources */, 11B35F6B92C2FB142E522828 /* BtcBlockchainSettingsViewModel.swift in Sources */, 11B35CC0D8AC06CE594F84DA /* BtcBlockchainSettingsService.swift in Sources */, @@ -10163,13 +10034,14 @@ ABC9AE3D64AF3981A68D9913 /* SendConfirmationViewModel.swift in Sources */, 11B35DBC25EF36ABA6E13857 /* SectionsTableView.swift in Sources */, 11B35774CEE79A1FD5265FB0 /* EnabledWalletStorage.swift in Sources */, + D311DA1D2BD114B00013DB8F /* MarketView.swift in Sources */, 11B3503BF015EA47E1061122 /* AccountRecordStorage.swift in Sources */, 11B356A4B22FA16BE27AFAB1 /* LogRecordStorage.swift in Sources */, - 11B35968A3A43727ED6FB0B7 /* FavoriteCoinRecordStorage.swift in Sources */, 11B350354CCA9BDDC05A9CBA /* WalletConnectSessionStorage.swift in Sources */, 11B35890939A3326B352A0FB /* ActiveAccountStorage.swift in Sources */, 11B35AC33360F772120B9562 /* RestoreSettingsStorage.swift in Sources */, 11B35729848FDBC47F038553 /* AppVersionRecordStorage.swift in Sources */, + D3DB519A2BD63D680091BBDB /* MarketSearchViewModel.swift in Sources */, 11B35DB339D0E7CF183760F4 /* EnabledWalletCacheStorage.swift in Sources */, ABC9A80BCDA72347C6619E6C /* SendTimeLockErrorService.swift in Sources */, 11B350ADB6D0D0C3E97F73D6 /* NftCollectionViewController.swift in Sources */, @@ -10195,20 +10067,14 @@ 11B358033DAB0FF23CF0E309 /* NftActivityService.swift in Sources */, 11B3584017622E1F2B3BA464 /* NftAssetButtonCell.swift in Sources */, 11B353B085BD167026DE4B5B /* CustomToken.swift in Sources */, - 1A564BC7CE38935CD443C235 /* MarketOverviewTopCoinsDataSource.swift in Sources */, - 1A56405536E22BFF69EE9593 /* MarketOverviewTopCoinsViewModel.swift in Sources */, 6B29072A2AF0CB8A006157D6 /* EventHandler.swift in Sources */, - 1A564E1912184BFC886548D9 /* MarketOverviewCategoryDataSource.swift in Sources */, - 1A564C6CCA15813506F20561 /* MarketOverviewCategoryViewModel.swift in Sources */, - 1A5640D097E24A155C1F2E56 /* MarketOverviewCategoryCell.swift in Sources */, - 1A564335057D41EECDC8021B /* MarketOverviewTopCoinsService.swift in Sources */, - 1A5648AB801E8DAA9B3D288E /* MarketOverviewGlobalService.swift in Sources */, - 1A5645CA87E32639CEE6681F /* MarketOverviewGlobalViewModel.swift in Sources */, - 1A5642348A701CF7CF5CD805 /* MarketOverviewGlobalDataSource.swift in Sources */, + D3A580982BE8AA90003953F4 /* BitcoinSendSettingsViewModel.swift in Sources */, + D3F9B02C2BE3A9A1009FFA95 /* MultiSwapSendView.swift in Sources */, + 6B5F5E162C0DDD7500E03EB2 /* RankView.swift in Sources */, ABC9AB3DAD30AA400DEB719C /* SendBitcoinService.swift in Sources */, ABC9AD49CCD14F97CD912454 /* SendBitcoinAdapterService.swift in Sources */, ABC9AF9F8113DB5D54140E7A /* SendBitcoinViewController.swift in Sources */, - ABC9A25C4149CC1DC03B853E /* SendViewModel.swift in Sources */, + ABC9A25C4149CC1DC03B853E /* SendViewModelOld.swift in Sources */, ABC9A6E939BDC0269313A66D /* SendModule.swift in Sources */, ABC9A3D48A3E0E1733F70686 /* SendBinanceService.swift in Sources */, ABC9A8D215CC5D6A70736E84 /* SendBaseService.swift in Sources */, @@ -10226,13 +10092,6 @@ ABC9A55FD66CB6374F2D520D /* SendZcashViewController.swift in Sources */, ABC9A852D667D38030B7EF39 /* SendConfirmationModule.swift in Sources */, ABC9A4BD4CA7A7872CE6167E /* BaseSendViewController.swift in Sources */, - 1A5645A90CB5AAEF02745AC7 /* MarketNftTopCollectionsModule.swift in Sources */, - 1A56499414D2E3BBFF260D14 /* MarketNftTopCollectionsService.swift in Sources */, - 1A5644FA9A9599F94EE16916 /* MarketNftTopCollectionsViewModel.swift in Sources */, - 1A564BC0945C7CA8330A604E /* MarketNftTopCollectionsViewController.swift in Sources */, - 1A564C13834A2EE853542795 /* MarketListNftCollectionDecorator.swift in Sources */, - 1A564DFD9A3B16E7DA518F67 /* NftCollectionsMultiSortHeaderViewModel.swift in Sources */, - 1A564586DCCE6E87784B9E6E /* MarketListWatchViewModel.swift in Sources */, ABC9A78CFF8B232D330EC7B5 /* DiffLabel.swift in Sources */, ABC9ADE1C8B18509F080FD11 /* ProFeaturesStorage.swift in Sources */, ABC9AD565E3BAB7074D02D40 /* ProFeaturesAuthorizationAdapter.swift in Sources */, @@ -10245,23 +10104,12 @@ 11B352C0FB8BFE700B220E5B /* EvmLabelStorage.swift in Sources */, 11B35C5388370450DAF65C5B /* EvmUpdateStatus.swift in Sources */, 11B35A5CD6B04D269E281A6A /* SyncerState.swift in Sources */, + D3A580892BE4DAA2003953F4 /* EvmSendData.swift in Sources */, 11B35EE45B00510714693AA9 /* SyncerStateStorage.swift in Sources */, - 1A564C2E1B66C2C508F8327D /* MarketOverviewTopPlatformsViewModel.swift in Sources */, - 1A564D4581F280245579C9DF /* BaseMarketOverviewTopListDataSource.swift in Sources */, ABC9A2E921AE00E0AF5067DE /* CoinProChartModule.swift in Sources */, ABC9A32D8EFFA6779886A27A /* ProChartFetcher.swift in Sources */, - 1A564176DEB9ED375113DA3B /* MarketTopPlatformsModule.swift in Sources */, - 1A564EF252E8C535BEB0548B /* MarketTopPlatformsService.swift in Sources */, - 1A5640051485E3419FE674F1 /* MarketTopPlatformsViewModel.swift in Sources */, - 1A564047000F9270ABC4AEC1 /* MarketTopPlatformsViewController.swift in Sources */, - 1A564EBD7F2BB47C7C209EC5 /* TopPlatformsMultiSortHeaderViewModel.swift in Sources */, - 1A56412970FD129426474522 /* MarketOverviewTopPlatformsDataSource.swift in Sources */, - 1A56417AE8B0F2513FC009A9 /* MarketListTopPlatformDecorator.swift in Sources */, + D3DB51B02BD7AF860091BBDB /* DiffText.swift in Sources */, ABC9AA8B7C29398895204651 /* ChartCell.swift in Sources */, - ABC9A7AF4EE29CDE045ADEF7 /* MarketCategoryMarketCapFetcher.swift in Sources */, - 11B355C8567EB1690E3BDA77 /* MarketOverviewService.swift in Sources */, - 11B359515EE181B7C3D773D3 /* MarketOverviewTopPlatformsService.swift in Sources */, - 11B357BF7588CB317EA62167 /* MarketOverviewCategoryService.swift in Sources */, ABC9A3187D032F44CD4E8986 /* MarketCardTitleView.swift in Sources */, ABC9AE553D422A163A09E5F8 /* MarketCardValueView.swift in Sources */, ABC9A19695087B66FD79ED99 /* Array.swift in Sources */, @@ -10291,6 +10139,7 @@ 11B35858954659DEE0C44618 /* TransactionsViewItemFactory.swift in Sources */, 11B35C4D4120D85CD32CAD0F /* TransactionsViewController.swift in Sources */, D09200BD293F21520091981A /* RestoreMnemonicNonStandardViewModel.swift in Sources */, + D0E5E8562BE38AA2005080A4 /* TronSendHandler.swift in Sources */, 11B35A82220538FEE57546FB /* TransactionTypeFilter.swift in Sources */, 11B35D7520519FBE9A899C2D /* TransactionSource.swift in Sources */, 11B359A4F6C0F8A705FDE18E /* Pool.swift in Sources */, @@ -10298,7 +10147,9 @@ 11B35A81973F6FB70B24AF7A /* PoolProvider.swift in Sources */, 11B3525DD29BC2286526669F /* PoolGroup.swift in Sources */, 11B35AB989198F423D880695 /* PoolGroupFactory.swift in Sources */, + D3F9B0352BE3B3A7009FFA95 /* EvmDecorator.swift in Sources */, D09D76952A2E07BD004311E6 /* SendTronConfirmationService.swift in Sources */, + D09C5C642C076C0E00E6909E /* CoinIconView.swift in Sources */, 11B358A1409FB729B1C3013E /* PoolSource.swift in Sources */, ABC9A52E08E5C57665C07DBC /* PseudoAccessoryView.swift in Sources */, ABC9AC10D815702B812CFFB7 /* NftAssetOverviewService.swift in Sources */, @@ -10314,11 +10165,11 @@ 11B3544E1538F4326C10A735 /* HeaderAmountView.swift in Sources */, ABC9A295A99F39EFAAF8FCDA /* Integer.swift in Sources */, ABC9A794E47FC07ABFC32BBD /* FeePriceScale.swift in Sources */, - 1A56443EA3671FA2A40F1F7E /* TopPlatformModule.swift in Sources */, 1A56463FADAB4646BA106A5D /* TopPlatformMarketCapFetcher.swift in Sources */, 11B35BADA9ACC9921545BAD9 /* SecondaryButtonCell.swift in Sources */, 11B357DDBC0536428B997FE5 /* InputSecondaryCircleButtonWrapperView.swift in Sources */, 11B35567A098667C9955F1F9 /* RecoveryPhraseModule.swift in Sources */, + D3F9B0262BE38AF1009FFA95 /* RegularSendView.swift in Sources */, 11B35289269C634BC9219362 /* RecoveryPhraseViewController.swift in Sources */, 11B351D9E1CAF8AA5BCE39F5 /* RecoveryPhraseViewModel.swift in Sources */, 11B35F78F82224BE17D612AB /* RecoveryPhraseService.swift in Sources */, @@ -10336,12 +10187,15 @@ 11B35052C5059CDD8E4BA940 /* BackupMnemonicWordCell.swift in Sources */, 11B35DF1D8B5125CF13A1812 /* RestoreMnemonicHintView.swift in Sources */, 11B35C8A1082D0A8F0B354B1 /* RestoreMnemonicHintCell.swift in Sources */, + D3402AF22BF5D59D003BF6F8 /* WatchlistModifier.swift in Sources */, 11B35AC9650545DEBC6C2C90 /* EvmAccountRestoreState.swift in Sources */, 11B35EBC6D5608F23DF8581E /* EvmAccountRestoreStateManager.swift in Sources */, + D3402AF82BF71C11003BF6F8 /* WatchlistManager.swift in Sources */, 11B35C43886D9A0F0C69EF33 /* EvmAccountRestoreStateStorage.swift in Sources */, 11B35B100187D9909A8490A7 /* NftAdapterManager.swift in Sources */, 11B35EC3B9E9C778183E1136 /* EvmNftAdapter.swift in Sources */, 11B350F58D6907C9A9A79F6B /* NftViewController.swift in Sources */, + D3402AEF2BF5D58B003BF6F8 /* WatchlistViewModel.swift in Sources */, 11B3593134900A8FC7C075B6 /* NftService.swift in Sources */, 11B35B6C74E672B2699E8207 /* NftModule.swift in Sources */, 11B35F8649859802080BA580 /* NftViewModel.swift in Sources */, @@ -10372,6 +10226,7 @@ ABC9A04655D81FE5198B786F /* SendEip1155ViewModel.swift in Sources */, ABC9A0B58626A1E0C4248162 /* SendEip1155Service.swift in Sources */, ABC9A140E5EF9D0AD4234689 /* SendEip1155ViewController.swift in Sources */, + D0A690352C05D01C00E59296 /* UIImageView.swift in Sources */, ABC9AF1729BA19223BB39E06 /* SendEip721ViewModel.swift in Sources */, ABC9AFA3570AE29F26623FBF /* SendEip721ViewController.swift in Sources */, ABC9A95E667DD7BD26602D8E /* SendEip721Service.swift in Sources */, @@ -10406,6 +10261,7 @@ 11B35446CFD6FBB7C3B64B55 /* SingleSelectorViewController.swift in Sources */, 11B3577BDCF978797E6C283E /* RestoreService.swift in Sources */, 11B3502C799E4ED762F95252 /* AddEvmSyncSourceModule.swift in Sources */, + D3DB51A92BD787490091BBDB /* MarketCoinsView.swift in Sources */, 11B354D6DE193776FFACE1B5 /* AddEvmSyncSourceViewController.swift in Sources */, 11B354BC4D954CCDA2E75C68 /* AddEvmSyncSourceViewModel.swift in Sources */, 11B353F6BB87F0F1933D63C2 /* AddEvmSyncSourceService.swift in Sources */, @@ -10420,6 +10276,7 @@ 11B353D3A4F2305366835086 /* NftActivityHeaderView.swift in Sources */, 11B350D00FA0A18EF540C945 /* BottomSingleSelectorViewController.swift in Sources */, 11B359F1AB3B0B00DD42E61C /* TokenProtocol.swift in Sources */, + D3384D112BFDCBDE00515664 /* MarketEtfView.swift in Sources */, 11B3588C582E45D149BB42BB /* Token.swift in Sources */, 11B35FC689D745FFBB3684C4 /* TokenType.swift in Sources */, 11B354F237E59C24ED8F3759 /* TokenQuery.swift in Sources */, @@ -10436,10 +10293,12 @@ 2FA5D68102F8CD6FB793A158 /* EvmSendSettingsModule.swift in Sources */, 2FA5D4C40B723434DB9D2DBE /* EvmSendSettingsService.swift in Sources */, 2FA5D987AC6D91F7929DE933 /* EvmSendSettingsViewController.swift in Sources */, + D03F74832BF76D0A004FBCFA /* GasPriceData.swift in Sources */, D05E96AA2A28657F002CCD71 /* TronAccountManager.swift in Sources */, 2FA5D61F4FA6E818D25C4A96 /* EvmSendSettingsViewModel.swift in Sources */, 2FA5D201AED8FB83968A5220 /* StepperAmountInputView.swift in Sources */, 2FA5D069D16C6119C970EDF1 /* StepperAmountInputCell.swift in Sources */, + D3833ADB2BEE1A8300ACECFB /* MarketWatchlistViewModel.swift in Sources */, 2FA5D4341C017BB619D745A2 /* EvmCommonGasDataService.swift in Sources */, 2FA5DDDE9097C0CD9C0F5BD5 /* EvmFeeModule.swift in Sources */, 2FA5DF34B11401B29082BBD2 /* EvmFeeService.swift in Sources */, @@ -10447,12 +10306,14 @@ 2FA5D43131923C094E518B94 /* EvmRollupGasDataService.swift in Sources */, 2FA5DB16A7E67A97A08D43DC /* NonceDataSource.swift in Sources */, 2FA5D8E83DFBD235B805C79C /* NonceService.swift in Sources */, + 6B5F5E122C0C660900E03EB2 /* MarketPlatformViewModel.swift in Sources */, 2FA5D01D1C1E23AC162B267B /* NonceViewModel.swift in Sources */, 2FA5D6E8F9B4E66D9FC52C37 /* LegacyGasPriceService.swift in Sources */, 2FA5D5268D73DCC5651C09AF /* LegacyEvmFeeViewModel.swift in Sources */, 2FA5D50BFA714AAF76AC536B /* Eip1559GasPriceService.swift in Sources */, 11B35963D09A27ACE74B3A52 /* PublicKeysModule.swift in Sources */, 11B35B1F63DBDEEF57043C97 /* PublicKeysService.swift in Sources */, + D0E5E8532BE260C8005080A4 /* BitcoinPreSendHandler.swift in Sources */, 11B3536008462BFF8CA245BE /* PublicKeysViewModel.swift in Sources */, 11B3509603841DEF2FC02F24 /* PublicKeysViewController.swift in Sources */, 11B358AE1CCD292DF2D2AC42 /* PrivateKeysViewModel.swift in Sources */, @@ -10486,11 +10347,13 @@ 11B352B8015606DD9D48A092 /* CoinAnalyticsHoldersCell.swift in Sources */, ABC9AFA89983A9BCB78E4575 /* Contact.swift in Sources */, ABC9A57DE6436FB8795F50E4 /* ContactBookViewController.swift in Sources */, + D3384D142BFDCBE700515664 /* MarketEtfViewModel.swift in Sources */, ABC9ACE1EDEA27A054EDC2C4 /* ContactBookService.swift in Sources */, ABC9A3510E5BE401AD04DA98 /* ContactBookViewModel.swift in Sources */, ABC9AE042D6A3D70CA64F959 /* ContactBookModule.swift in Sources */, ABC9AFFD730AC3274811DA61 /* ContactBookContactViewController.swift in Sources */, ABC9ACDDAD1927EB799150D2 /* ContactBookContactViewModel.swift in Sources */, + 6BB14F6C2BF49E7100E879B2 /* WalletButtonHiddenManager.swift in Sources */, ABC9ACC4CA8C9CBCA7C1A182 /* ContactBookContactService.swift in Sources */, ABC9A6F88E51293F2605CACD /* ContactBookContactModule.swift in Sources */, ABC9A69A1A01DBD07CAAC9CD /* ContactBookAddressViewController.swift in Sources */, @@ -10503,20 +10366,10 @@ ABC9A6C1B2F55F1FFA8910CA /* ContactBookSettingsViewModel.swift in Sources */, ABC9A7655AE66379E42FE2A4 /* ContactBookSettingsViewController.swift in Sources */, ABC9A2A9540003916929DC77 /* ContactBookSettingsModule.swift in Sources */, + D3B73E2E2BDF6B6D0067429D /* MultiSwapSendHandler.swift in Sources */, ABC9A227648FF076E9518703 /* ContactBookHelper.swift in Sources */, - 11B353973C1ADD51D174AC74 /* CoinRankService.swift in Sources */, - 11B35B3F5DE4F73225EBFF36 /* CoinRankViewModel.swift in Sources */, - 11B35FA3A00690573A482BAC /* CoinRankViewController.swift in Sources */, - 11B35C81D9DFBF07955D2461 /* CoinRankModule.swift in Sources */, - 11B35A89B3F0D81C42DC5C40 /* CoinRankHeaderView.swift in Sources */, ABC9AEA4EF88D31D00781014 /* ContactLabelService.swift in Sources */, ABC9AC87FA11BA3478A8E801 /* TransactionsContactLabelService.swift in Sources */, - 11B351EEC95E342C2B4F5BC4 /* MarketHeaderCell.swift in Sources */, - 11B35CC691C53A0B870BB910 /* MarketFilteredListService.swift in Sources */, - 11B35C4A68DD7F7AF6DC4F27 /* TopPlatformViewModel.swift in Sources */, - 11B35E584C30C56AE18DE076 /* TopPlatformHeaderCell.swift in Sources */, - 11B35929AD3C9F27463392C6 /* TopPlatformViewController.swift in Sources */, - 11B35C83BC2D0EA7FF5B4832 /* MarketCategoryViewController.swift in Sources */, 11B356F39BE75B7663BBA399 /* Misc.swift in Sources */, ABC9A2746046C136F98F970A /* BackupContact.swift in Sources */, ABC9AE0D23A7B54521E77052 /* ECashAdapter.swift in Sources */, @@ -10527,6 +10380,8 @@ 11B35EA628D9401F5C3A9CB8 /* NftKit.swift in Sources */, 11B35BC602EA104EE1C0540C /* BinanceChainKit.swift in Sources */, 11B35DE5BD5716307300AD2F /* OneInchKit.swift in Sources */, + D3833AD82BEE1A7900ACECFB /* MarketWatchlistView.swift in Sources */, + D3384D1B2BFF0CAF00515664 /* MarketMarketCapView.swift in Sources */, ABC9ADDC1F55F835C68DB4C7 /* UniswapV3Provider.swift in Sources */, D0F132A22B6B98E100C7310E /* RbfService.swift in Sources */, ABC9A3CC73251E7F83A94181 /* UniswapV3TradeService.swift in Sources */, @@ -10550,6 +10405,7 @@ ABC9ACDD29B7F82884A5AE39 /* CipherParams.swift in Sources */, ABC9AA80C5197F9CC6221FC8 /* WalletBackup.swift in Sources */, ABC9A8AE39B8925B28B97F77 /* AppBackupProvider.swift in Sources */, + D02447DA2C09FA5200A04BBC /* CoinTreasuriesView.swift in Sources */, ABC9A99724D817AF0E6C5EC3 /* FileStorage.swift in Sources */, ABC9A3BC9A18F74818EF5C17 /* MetadataMonitor.swift in Sources */, ABC9A8CBDB7CF4E781896C49 /* RestoreTypeModule.swift in Sources */, @@ -10570,9 +10426,11 @@ 11B35DAC8D4C5DA2EC6D6E74 /* PasteInputView.swift in Sources */, 11B350A24C41C436EA7DC598 /* PasteInputCell.swift in Sources */, 11B354E72D9BDF04E75C8748 /* WalletHeaderCell.swift in Sources */, + D311DA262BD23C890013DB8F /* ScrollableTabHeaderView.swift in Sources */, 11B3562D78E70F5F14B81B3A /* CexWithdrawNetwork.swift in Sources */, 11B354DEFBE83147106A5FFE /* CexAssetRecord.swift in Sources */, 11B35C4A0250F05179488A91 /* CexWithdrawNetworkRaw.swift in Sources */, + D3384D1E2BFF0CB800515664 /* MarketMarketCapViewModel.swift in Sources */, 11B357573D364030813F231C /* CexAssetManager.swift in Sources */, 11B350BFC559991F9BA7A63F /* CexAssetRecordStorage.swift in Sources */, 11B357EE5114E13940C0D631 /* CexAssetResponse.swift in Sources */, @@ -10599,6 +10457,7 @@ ABC9AD27E074CF3FA292C647 /* IndicatorAdviceView.swift in Sources */, ABC9A305CBB28F2B19EB00D2 /* CoinDetailAdviceViewController.swift in Sources */, ABC9AADE6C4251FCC9222125 /* ManageBarButtonView.swift in Sources */, + D3833AF62BF20B8D00ACECFB /* MarketPairsViewModel.swift in Sources */, ABC9AB11FDD018A96BB86557 /* BottomGradientHolder.swift in Sources */, 11B35311CEEC40EA3089293D /* SubscriptionInfoViewController.swift in Sources */, 11B35A5DA8197D193B7CF8D9 /* AmountData.swift in Sources */, @@ -10611,6 +10470,7 @@ 11B35D57964143D9FAAC6A4F /* CexCoinService.swift in Sources */, 11B3520F75BD8D46B1F44B3F /* CexAmountInputViewModel.swift in Sources */, 11B359029FFF4106B703694C /* CexDepositNetworkSelectModule.swift in Sources */, + D3DB51AC2BD787A00091BBDB /* MarketCoinsViewModel.swift in Sources */, 11B35895C483A56545A6700E /* CexDepositNetworkSelectViewController.swift in Sources */, 11B359D10C2A7258AF0F2B60 /* CexDepositNetworkSelectViewModel.swift in Sources */, 11B3562EE896D758066FEECB /* CexDepositNetworkSelectService.swift in Sources */, @@ -10625,6 +10485,7 @@ 11B35974BDD90836CF5C0DB1 /* CexDepositService.swift in Sources */, 11B35963BA1215A80E8B26D0 /* CexDepositModule.swift in Sources */, 11B35FB4B6E5E6B442ADE3B2 /* BinanceCexProvider.swift in Sources */, + D311DA202BD115240013DB8F /* MarketGlobalViewModel.swift in Sources */, 11B355F11DDA5EC8082C43DF /* BinanceWithdrawHandler.swift in Sources */, 11B354FA6B9C8A5DBD4D4BA9 /* RestoreCexViewController.swift in Sources */, 11B35A1BD3EA5E3547D0E3FF /* RestoreBinanceViewModel.swift in Sources */, @@ -10668,6 +10529,8 @@ ABC9AD46AE6B5F432E0D2085 /* WalletTokenBalanceViewModel.swift in Sources */, ABC9A69264C2086E4B3B09D2 /* WalletTokenBalanceService.swift in Sources */, ABC9A2A6C3A1EFDD33D53287 /* WalletTokenBalanceModule.swift in Sources */, + D3384D172BFDEF6800515664 /* Etf.swift in Sources */, + D313698A2BEA188D00BA6B5B /* ZcashPreSendHandler.swift in Sources */, ABC9A55470228BD7B1535B9B /* WalletTokenBalanceViewItemFactory.swift in Sources */, ABC9ABD7DA0C144C545EE228 /* WalletTokenBalanceCell.swift in Sources */, ABC9AC7B5E1E2EE48117EFC4 /* BalanceButtonsCell.swift in Sources */, @@ -10679,16 +10542,19 @@ 11B35EEE72F3A78415A2A552 /* HorizontalDivider.swift in Sources */, 11B35AA627F0357B3FB5F9E8 /* ListRow.swift in Sources */, 11B35608F7D19B3E6318CB22 /* Text.swift in Sources */, + D3F9B0322BE3B39D009FFA95 /* EvmDecoration.swift in Sources */, 11B3501EA3658BFDC67DEB1F /* ThemeView.swift in Sources */, ABC9A097A0BDD99777D5374D /* DonateDescriptionCell.swift in Sources */, ABC9A02C18AACAA8CC930665 /* WalletTokenListDataSource.swift in Sources */, ABC9A096B05E5491A40A327C /* DonateDescriptionDataSource.swift in Sources */, ABC9AE9A8BECB2CE0EEF8271 /* DonateAddressViewController.swift in Sources */, + D3D13A602C0D9DCB002484BC /* MarketAdvancedSearchViewModel.swift in Sources */, ABC9AAE6886B6AB060FD9E99 /* DonateAddressViewModel.swift in Sources */, ABC9A728EEF4A054C7B8722B /* DonateAddressModule.swift in Sources */, 11B35ACD13702502B1ED3362 /* HighlightedTextView.swift in Sources */, 11B35F134E5EF8572BF330CB /* NavigationRow.swift in Sources */, 11B35FA70EB07440E1576A56 /* RowButtonStyle.swift in Sources */, + D31369872BEA187E00BA6B5B /* ZcashSendHandler.swift in Sources */, 11B35CA92AA402BE72B4F5D6 /* Image.swift in Sources */, ABC9AD2688A8DF327A3F92FC /* NoAccountWalletTokenListService.swift in Sources */, ABC9A3FCFC46EC73A7E57EA3 /* WalletConnectPairingModule.swift in Sources */, @@ -10723,6 +10589,7 @@ 11B35FFC8C3E4CF638397650 /* UnlockView.swift in Sources */, 11B3564236FEF4E5ACC8C838 /* UnlockModule.swift in Sources */, 11B3527C3BD088DCCA6959C3 /* ModuleUnlockView.swift in Sources */, + D3833B062BF4AFB800ACECFB /* MarqueeView.swift in Sources */, 11B3531640EE1F9D29B63325 /* AppUnlockViewModel.swift in Sources */, 11B3561679C05C31F16EDC77 /* BaseUnlockViewModel.swift in Sources */, 11B35F98393E6F3B76381ECF /* ModuleUnlockViewModel.swift in Sources */, @@ -10752,6 +10619,7 @@ ABC9ABE3F52BF2307533D8FB /* InputTextRow.swift in Sources */, ABC9A36D3A4EEABF6EA6DBA0 /* Shake.swift in Sources */, ABC9A4A21CFBA188A7EEC930 /* ActivityView.swift in Sources */, + 6BB14F812C06F19300E879B2 /* DefiCoin.swift in Sources */, 11B357740CC018527301C4AE /* AppStatusView.swift in Sources */, 11B359BD68E234293DCF33CC /* AppStatusViewModel.swift in Sources */, 11B356562D2B4F5BCAB4FC80 /* AboutView.swift in Sources */, @@ -10765,6 +10633,7 @@ ABC9A8AC5E635D9CB1704568 /* BackupDisclaimerView.swift in Sources */, ABC9A437473D0E77F9DBEB42 /* RestoreAppViewModel.swift in Sources */, ABC9AE7DA8EFD812710C7BE4 /* RestorePassphraseViewModel.swift in Sources */, + D3384D222BFF0CCA00515664 /* MarketVolumeView.swift in Sources */, ABC9A93E05AAF5D98C1DF4D6 /* RestorePassphraseService.swift in Sources */, ABC9AA016413C37F4CC95080 /* RestorePassphraseViewController.swift in Sources */, ABC9A453F337BA22A5698DCC /* RestorePassphraseModule.swift in Sources */, @@ -10780,6 +10649,7 @@ ABC9AE262936C29D89DC61C8 /* BackupManagerModule.swift in Sources */, ABC9ACFCC63CDB6C7712E512 /* BackupManagerViewController.swift in Sources */, 11B3560F69D84432665A2BAA /* CoinPageViewModelNew.swift in Sources */, + D3833AE22BEE3FE800ACECFB /* MarketPlatformsViewModel.swift in Sources */, 11B3542694E183882F9BEBEC /* CoinPageView.swift in Sources */, 11B35B5451BA0A3C825809A2 /* TabHeaderView.swift in Sources */, 11B35DDE363387B6E7A1D3B9 /* TabButtonStyle.swift in Sources */, @@ -10790,7 +10660,9 @@ ABC9AA802C533F489EB72FDE /* BackupManagerViewModel.swift in Sources */, 11B359F812683F62595AFEE2 /* DateFormatterCache.swift in Sources */, 11B35483AFC1088E56BC7F37 /* LanguageHourFormatter.swift in Sources */, + D36E50852BF75B6900C361BD /* WatchlistTimePeriod.swift in Sources */, 11B35B298DCBAA8AE9DADA34 /* LanguageSettingsModule.swift in Sources */, + D3F9B03B2BE3BB36009FFA95 /* WalletConnectSendViewModel.swift in Sources */, 11B35C22C95B08142C13B14D /* LanguageSettingsViewModel.swift in Sources */, 11B355BDD19498AA9CD250BB /* LanguageSettingsView.swift in Sources */, D0532CC22B149E110015DF40 /* WatchViewController.swift in Sources */, @@ -10800,6 +10672,7 @@ 11B35EBBFE0CA22D35BA4A94 /* KeychainManager.swift in Sources */, 11B3552937152C5A4169C018 /* PasscodeLockManager.swift in Sources */, 11B3558A626C2E58C2656FF6 /* KeychainStorage.swift in Sources */, + D0E5E8502BE22172005080A4 /* BitcoinSendHandler.swift in Sources */, 11B354908D10B00AF7FF0AA3 /* UserDefaultsStorage.swift in Sources */, 11B35993ADB991F644E5EE98 /* PasscodeLockState.swift in Sources */, 11B35F906F9708CFC86E53FB /* NoPasscodeViewController.swift in Sources */, @@ -10833,6 +10706,7 @@ 11B35076F57EA6103BBE3D63 /* WCSendEthereumTransactionRequestViewController.swift in Sources */, 11B35BD102629037A4348B3C /* WCSignEthereumTransactionRequestViewController.swift in Sources */, 11B353FF811AF49F4BC7BD43 /* CoinMarketsViewModel.swift in Sources */, + D311DA232BD23C230013DB8F /* MarketAdvancedSearchView.swift in Sources */, 11B3507AB99A089727463C16 /* CoinMarketsView.swift in Sources */, 11B350473A42FF70E806AF96 /* ThemeList.swift in Sources */, 11B35CC16ABA93A94F46FD5B /* SelectorButtonStyle.swift in Sources */, @@ -10841,12 +10715,14 @@ ABC9A5361D9712C95F456376 /* Extensions.swift in Sources */, D3DD672C2BC3BF5200EC7F78 /* OneInchMultiSwapConfirmationQuote.swift in Sources */, ABC9A50D63AD21802AF5DE22 /* BaseAnimation.swift in Sources */, + D3384D4F2C07020300515664 /* PriceChangeMode.swift in Sources */, ABC9A8F221BB603CA4EBFA1D /* AlphaDismissAnimation.swift in Sources */, ABC9A724798E2748EAC06A36 /* AlphaPresentAnimation.swift in Sources */, ABC9A1FD7594369D1F36C4EB /* MovingDismissAnimation.swift in Sources */, ABC9A23D99D437A8A42C47E8 /* MovingPresentAnimation.swift in Sources */, ABC9A32407A840A2100B3F76 /* TransitionDriver.swift in Sources */, ABC9A400CF531FBE9FD0D7B1 /* ActionSheetAnimator.swift in Sources */, + D3833AEB2BEE4CAA00ACECFB /* TopPlatform.swift in Sources */, ABC9A14731CC409C5EBC4978 /* DismissPanGestureRecognizer.swift in Sources */, ABC9AA66E5F1C6C5EC8B6A1F /* InteractiveTransitionDelegate.swift in Sources */, ABC9A231F63CDD5EC0BB71EF /* ActionSheetPresentationController.swift in Sources */, @@ -10860,6 +10736,7 @@ 11B3523804E0F4F1DA8A1D9E /* BaseCurrencySettingsViewModel.swift in Sources */, 11B35F18FEEEAA9EC6043CA6 /* BaseCurrencySettingsView.swift in Sources */, 11B35E749106C1ABD9335778 /* TransactionFilterModule.swift in Sources */, + D3384D0A2BFCB43800515664 /* MarketWatchlistSignalsView.swift in Sources */, 11B3591E867F4E701F5458F9 /* TransactionFilterView.swift in Sources */, 11B352E8348A715EB537F643 /* TransactionFilterViewModel.swift in Sources */, 11B35AAD64D68265B2128C25 /* TransactionBlockchainSelectView.swift in Sources */, @@ -10871,12 +10748,10 @@ 11B3535D6A72ED1D564A0F7C /* TonOutgoingTransactionRecord.swift in Sources */, 11B35C60FE9B94994FCCB0CB /* TonTransactionRecord.swift in Sources */, 11B3589CF4D819A0430DE3D9 /* TonIncomingTransactionRecord.swift in Sources */, + D389BC502C0DEF1800724504 /* MarketAdvancedSearchResultsView.swift in Sources */, 11B35927C89712EF8ED36981 /* InputRowModifier.swift in Sources */, - 11B35E1853C328487870BACA /* SendAmountViewModel.swift in Sources */, - 11B35179EE2C14CF52443183 /* SendAmountView.swift in Sources */, - 11B359C0E0A7F674061B1199 /* SendModuleNew.swift in Sources */, - 11B359926B194C0207B1C8E6 /* SendView.swift in Sources */, - 11B35FA298822DABDB1CD109 /* SendViewModelNew.swift in Sources */, + 11B359926B194C0207B1C8E6 /* PreSendView.swift in Sources */, + 11B35FA298822DABDB1CD109 /* PreSendViewModel.swift in Sources */, 11B35504EF11FA59D2A358BE /* SendTonViewController.swift in Sources */, 11B35C4797940E34E6361A83 /* SendTonService.swift in Sources */, 11B35988DD7E3E4E2EEE4444 /* SendTonFactory.swift in Sources */, @@ -10885,17 +10760,10 @@ ABC9A57D1DA5481E44DEE8C0 /* PrimaryCircleButtonStyle.swift in Sources */, ABC9A22690729B58621A1BBA /* InformedModifier.swift in Sources */, ABC9A8117EAF046CDD020077 /* BorderedEmptyCell.swift in Sources */, - 11B358946C5E7A72712EACB2 /* MarketCategoryView.swift in Sources */, 11B35AF710C287EB89018342 /* UIWindow.swift in Sources */, ABC9A341284773472DC4AF60 /* SecondaryActiveButtonStyle.swift in Sources */, + D389BC472C0DCF4100724504 /* Advice.swift in Sources */, ABC9A582FAB4466B5B25E013 /* UsedAddressesView.swift in Sources */, - 11B3527103D25C72BC849651 /* MarketOverviewTopPairsDataSource.swift in Sources */, - 11B35D06FF4A25AF74553C36 /* MarketOverviewTopPairsViewModel.swift in Sources */, - 11B353E17EA348D431520FAD /* MarketListMarketPairDecorator.swift in Sources */, - 11B3543546FE55AE0AB91FAF /* MarketOverviewTopPairsService.swift in Sources */, - 11B353F04B500616AC5CB9C5 /* MarketTopPairsViewController.swift in Sources */, - 11B35FF14C4B775E570375EC /* MarketTopPairsViewModel.swift in Sources */, - 11B358634BEC799056209B93 /* MarketTopPairsModule.swift in Sources */, ABC9A9E7E67D0C8A7B2114B8 /* UnspentOutputsViewModel.swift in Sources */, D06B302D2B6A120E0012A161 /* LegacyFeeSettingsViewModel.swift in Sources */, ABC9A929CE4DC2F1569658D6 /* UnspentOutputsCell.swift in Sources */, @@ -10904,7 +10772,6 @@ ABC9AC726857BA4662F1CD72 /* BaseFiatService.swift in Sources */, ABC9A2C63884586EC3A6B508 /* AmountOutputSelectorViewModel.swift in Sources */, ABC9A4FB2D21F111ED63208D /* AddressOutputSelectorViewModel.swift in Sources */, - ABC9A160495EB67472B97E61 /* MarketWatchlistDecorator.swift in Sources */, ABC9A00DA1F4148859D9D1AB /* Eip1559FeeSettingsView.swift in Sources */, ABC9A252B1848D5A2E46C071 /* Eip1559FeeSettingsViewModel.swift in Sources */, 11B35E433AC4217552A8D668 /* MultiSwapView.swift in Sources */, @@ -10913,8 +10780,8 @@ 11B358A9EB8B15436E923FFA /* MultiSwapCircularProgressView.swift in Sources */, 11B35D2C16299881FE8BD910 /* MultiSwapQuotesView.swift in Sources */, 11B35D098F3E0435A3C4512B /* MultiSwapButtonState.swift in Sources */, - 11B3542DB7195238D606D057 /* MultiSwapConfirmationView.swift in Sources */, 11B35ED4BE062F70D930F92C /* MultiSwapSettingStorage.swift in Sources */, + D0DEFF072BD1253C004C9DF0 /* BitcoinFeeSettingsView.swift in Sources */, D0740B1C2B87585100B085F9 /* ResendBitcoinService.swift in Sources */, 11B35CFBC57B9DF24CE0665C /* UniswapV2MultiSwapProvider.swift in Sources */, 11B35DC168B3102FB2293CE4 /* PancakeV3MultiSwapProvider.swift in Sources */, @@ -10931,6 +10798,7 @@ ABC9ABB622E7AF7573E71C7A /* DebounceTextField.swift in Sources */, ABC9AF8E2B0AC66EE65B81B9 /* BaseEvmMultiSwapProvider.swift in Sources */, ABC9ABE0119C5E5EB43A9964 /* RightCheckingView.swift in Sources */, + D0DEFF0D2BD1257F004C9DF0 /* BitcoinFeeData.swift in Sources */, 11B35BFFB56EA4955808B3E8 /* CoinAnalyticsIssueCell.swift in Sources */, 11B35D24064D9CE75ACCD59A /* CoinAnalyticsIssuesView.swift in Sources */, 11B35498BAB60D962C9C1172 /* QuickSwapMultiSwapProvider.swift in Sources */, @@ -10941,7 +10809,6 @@ ABC9AFED164965D301F27D1F /* MultiSwapSlippageView.swift in Sources */, 11B35830E8C818E413DEAAFC /* RecipientAndSlippageMultiSwapSettingsView.swift in Sources */, 11B35AF420F4D90CD6C9E6E3 /* OneInchMultiSwapProvider.swift in Sources */, - 11B350C02EDA928A80F3AD83 /* MultiSwapConfirmationViewModel.swift in Sources */, ABC9A0CE61C14C2E46BE50EA /* MultiSwapApproveView.swift in Sources */, ABC9A8A507E8AF95CB00379A /* MultiSwapApproveViewModel.swift in Sources */, ABC9A2F69C11C20F72C2FF63 /* Binding.swift in Sources */, @@ -10951,20 +10818,20 @@ ABC9AD02AD6340C230AC4F9D /* TransactionContactSelectViewModel.swift in Sources */, ABC9AC0F2A4A48BB223005EA /* TransactionBlockchainSelectViewModel.swift in Sources */, ABC9A24A0A2F6C7C476D7F69 /* TransactionTokenSelectViewModel.swift in Sources */, - 11B35EFEEB7F361BD5FCCC3D /* SendConfirmationNewView.swift in Sources */, - 11B3525F2CAA4B98F29705A7 /* SendConfirmationNewViewModel.swift in Sources */, - 11B3565DC90BD451F8DE7120 /* SendEvmHandler.swift in Sources */, + 11B35EFEEB7F361BD5FCCC3D /* SendView.swift in Sources */, + 11B3525F2CAA4B98F29705A7 /* SendViewModel.swift in Sources */, + 11B3565DC90BD451F8DE7120 /* EvmSendHandler.swift in Sources */, ABC9A5280FD23BAD3166BE3B /* SliderGradientView.swift in Sources */, ABC9A565DE76B57BE559DBCA /* TechnicalIndicatorCell.swift in Sources */, 11B358591C71F77058A3A08F /* ISendHandler.swift in Sources */, 11B35488B94F239EB58F8769 /* BaseSendEvmData.swift in Sources */, 11B35C943710774694282388 /* IMultiSwapQuote.swift in Sources */, 11B35772618CDA2F19CCDB2C /* IMultiSwapConfirmationQuote.swift in Sources */, - 11B354B484C4785A27216BEC /* ISendConfirmationData.swift in Sources */, - 11B35A4428B94FF4E4F01AA9 /* SendDataNew.swift in Sources */, + 11B354B484C4785A27216BEC /* ISendData.swift in Sources */, + 11B35A4428B94FF4E4F01AA9 /* SendData.swift in Sources */, 11B358C4D4C466ACCEF0E4C7 /* MultiSwapMainField.swift in Sources */, 11B353D4BC292465358979D3 /* ValueLevel.swift in Sources */, - 11B350CE805774C98948353C /* SendConfirmField.swift in Sources */, + 11B350CE805774C98948353C /* SendField.swift in Sources */, 11B359BACF4B66607F50944D /* MultiSwapPreSwapStep.swift in Sources */, 11B358B7A012C6F4922DC092 /* SendHandlerFactory.swift in Sources */, 11B357012D58588855E42A38 /* FeeData.swift in Sources */, @@ -10976,7 +10843,9 @@ 11B351354518B4193A672044 /* EvmFeeEstimator.swift in Sources */, ABC9A07B88375991F5749391 /* SlideButton.swift in Sources */, ABC9A4DE73BE3055DD97343B /* ShimmerEffect.swift in Sources */, + D3833B032BF38A8000ACECFB /* MarketTabViewModel.swift in Sources */, ABC9A6C591067D34C6DF2673 /* SlideButtonStyling.swift in Sources */, + D34A29B82BFB4E3200F63036 /* WatchlistSortBy.swift in Sources */, 11B35A32114022EF422E6602 /* MultiSwapRevokeView.swift in Sources */, 11B357DCC0BA9E888DE64CB1 /* MultiSwapRevokeViewModel.swift in Sources */, 11B35FEC7AA2A8887FCF0AE6 /* StatManager.swift in Sources */, @@ -11018,12 +10887,14 @@ D384067421831B3D007D50AD /* MainSettingsModule.swift in Sources */, D384067521831B3D007D50AD /* MainSettingsViewModel.swift in Sources */, D384067721831B3D007D50AD /* MainSettingsService.swift in Sources */, + D3B73E302BDFC5580067429D /* PriceRow.swift in Sources */, D384067821831B3D007D50AD /* MainSettingsViewController.swift in Sources */, D384067A21831B3D007D50AD /* LanguageManager.swift in Sources */, D384067B21831B3D007D50AD /* LocalStorage.swift in Sources */, D384067E21831B3D007D50AD /* SecuritySettingsModule.swift in Sources */, D023D26A2A24CD16004F65B0 /* BaseTronAdapter.swift in Sources */, D384068121831B3D007D50AD /* SecuritySettingsViewModel.swift in Sources */, + D0DEFF042BD1253C004C9DF0 /* BitcoinFeeSettingsViewModel.swift in Sources */, D384068221831B3D007D50AD /* BitcoinAdapter.swift in Sources */, D384068921831B3D007D50AD /* KeyboardObservingViewController.swift in Sources */, D384069021831B3D007D50AD /* Date.swift in Sources */, @@ -11032,6 +10903,7 @@ D384069121831B3D007D50AD /* UIAlertController.swift in Sources */, D384069421831B3D007D50AD /* DateHelper.swift in Sources */, D384069521831B3D007D50AD /* SystemInfoManager.swift in Sources */, + D3DB51A52BD685B40091BBDB /* MarketTabView.swift in Sources */, D384069621831B3D007D50AD /* PermissionsHelper.swift in Sources */, 11B3541F6B5316F3B373D1EA /* ValueFormatter.swift in Sources */, D09D768C2A2E066E004311E6 /* SendTronConfirmationModule.swift in Sources */, @@ -11045,13 +10917,14 @@ 11B35A3495FF00B1A3FF4197 /* AuthData.swift in Sources */, 5046E671259C491000A941E5 /* InfoSeparatorHeaderView.swift in Sources */, 3C7B9BFD8EA7360907306675 /* UrlManager.swift in Sources */, + D084F6BE2BEB94F700407FA4 /* OutputSelectView2.swift in Sources */, 58AAA745D76A13D06800BDD1 /* EvmKitManager.swift in Sources */, 58AAA82745E47084A2B18F95 /* Eip20Adapter.swift in Sources */, + 6B5F5E0E2C0C65F700E03EB2 /* MarketPlatformViewNew.swift in Sources */, 58AAAF011B2E9CDF8455CA7B /* BaseEvmAdapter.swift in Sources */, 1A56491DC545ED4F8A6E6D40 /* Decimal.swift in Sources */, 11B35DE3E7E6EB3CFAB81329 /* UnlinkViewController.swift in Sources */, 11B350E34285A392F34198D0 /* UnlinkModule.swift in Sources */, - 3A73FC9B258B1AF700FE4D34 /* MarketWatchlistModule.swift in Sources */, 11B3555CA9B2F01358E055BE /* UnlinkViewModel.swift in Sources */, 11B35FB3A17F76325C98C2AB /* UnlinkService.swift in Sources */, 1A5648D075682B17EFE9CBB6 /* AddressUri.swift in Sources */, @@ -11061,14 +10934,15 @@ 11B3507DDEBA587C023CE898 /* DashAdapter.swift in Sources */, 11B3577193A4BD719E12CE2E /* EnabledWallet.swift in Sources */, 11B3588E8DA44A45661351D7 /* Account.swift in Sources */, - 3A73FCAD258B1AFC00FE4D34 /* GradientPercentCircle.swift in Sources */, 11B35A91D104DA35C08032B2 /* AccountType.swift in Sources */, 11B35068E05BC58C6C9A93D7 /* AccountManager.swift in Sources */, 11B35C8621E221DA1F157A5B /* AccountFactory.swift in Sources */, 3C7B9F51D15FBB02710E5EEB /* WelcomeScreenViewController.swift in Sources */, + D3384D0C2BFCB8F000515664 /* MarketWatchlistSignalBadge.swift in Sources */, 11B35BC6DFCA197FA842873B /* BackupManager.swift in Sources */, 11B35590A4DA4BCFB3D38DDF /* AppManager.swift in Sources */, D31C4761238BF176008CB818 /* MnemonicDerivation.swift in Sources */, + D0A6902E2C04969300E59296 /* CautionDataSourceViewModel.swift in Sources */, 11B3597622438E522E0F7AE1 /* AccountRecord.swift in Sources */, 11B35262A6D3AB8C41E2E245 /* AccountStorage.swift in Sources */, 1A56416B9DC6DA281AD34575 /* AdapterState.swift in Sources */, @@ -11087,7 +10961,6 @@ D02A67C5272A7460009B2C1C /* CoinTweetsViewModel.swift in Sources */, D36DE0C3272FD864000BC916 /* UniswapModule.swift in Sources */, 2FA5DDDD98533BAFCF3819C8 /* TimeInterval.swift in Sources */, - 3AB682BE25BADD97002197A5 /* MarketOverviewModule.swift in Sources */, 11B351B7666846080F31D976 /* FeeCoinProvider.swift in Sources */, 11B352184FCE4B2B3E68E459 /* CurrentDateProvider.swift in Sources */, 11B35AF1B38CDF6728158514 /* AppConfig.swift in Sources */, @@ -11095,6 +10968,7 @@ D0532CBE2B149DEE0015DF40 /* WatchViewModel.swift in Sources */, 11B3540F182F3EDE74245EC7 /* MainSettingsFooterCell.swift in Sources */, 6BCD53162A161F4800993F20 /* BackupViewController.swift in Sources */, + D3833AF22BF20B8600ACECFB /* MarketPairsView.swift in Sources */, 58AAAD33B32694AFA2E954D6 /* GradientLayer.swift in Sources */, 58AAAE430A2184D5A12202EA /* DebugModule.swift in Sources */, 58AAA54CF2169C04A4A38817 /* DebugRouter.swift in Sources */, @@ -11104,7 +10978,6 @@ 58AAAF6248682EE23B3C3D5A /* DebugLogger.swift in Sources */, 1A5644CF2BEC2E7C6227BDC7 /* AppStatusModule.swift in Sources */, D05E96902A261D82002CCD71 /* TronTransactionAdapter.swift in Sources */, - 3A73FCAA258B1AFC00FE4D34 /* MarketMetricView.swift in Sources */, D05E96A32A2627DA002CCD71 /* TronIncomingTransactionRecord.swift in Sources */, 11B35D51B52EF0000711CE05 /* MultiTextMetricsView.swift in Sources */, 1A5647EF2A5B8141F0BE4320 /* UIDevice.swift in Sources */, @@ -11122,6 +10995,7 @@ 58AAA2100166FDFB110FA6D0 /* ChartConfiguration.swift in Sources */, 58AAA415B26725FEF4A1128D /* DoubleSpendInfoViewController.swift in Sources */, 58AAA39A983D2E97066C3959 /* LastBlockInfo.swift in Sources */, + D3384D242BFF0CD100515664 /* MarketVolumeViewModel.swift in Sources */, 58AAA550B894B6F8FC8DA1B1 /* UITextView.swift in Sources */, 58AAA8E5EA8901CF69DDE43D /* LockDelegate.swift in Sources */, 58AAA6A77A2B953931A1D7FC /* DataStatus.swift in Sources */, @@ -11134,6 +11008,7 @@ 11B35898D710EC75D922785D /* WelcomeTextView.swift in Sources */, 1A5643651979E1907BE1B12C /* BlockchainSettingRecord.swift in Sources */, 1A56463AB918839385E8CDBD /* BlockchainSettingsStorage.swift in Sources */, + D3B73E272BDBC6120067429D /* IPreSendHandler.swift in Sources */, D3DD67282BC3BF4700EC7F78 /* OneInchMultiSwapQuote.swift in Sources */, 1A56454D8C03B7B1141635DA /* EnabledWallet_v_0_13.swift in Sources */, 1A5645DE1329BC81C1A9B711 /* BtcBlockchainManager.swift in Sources */, @@ -11149,13 +11024,13 @@ D023D2672A24BBC6004F65B0 /* TronAddressParser.swift in Sources */, 11B35C16BE16B6AEEA0690EC /* AlertModule.swift in Sources */, 11B3557E460F3BA25ED9F6CC /* AlertPresenter.swift in Sources */, - 3A73FC78258B1AE500FE4D34 /* MarketViewModel.swift in Sources */, 11B35E81542268ACDC17502A /* AlertRouter.swift in Sources */, 11B35FC5D80CC1D4854DBC94 /* BottomSheetTitleView.swift in Sources */, 1A564F382602A283C370E7FB /* AppError.swift in Sources */, D36DE0F9272FD92F000BC916 /* SwapSelectProviderModule.swift in Sources */, D36DE0E7272FD887000BC916 /* OneInchModule.swift in Sources */, 1A5649169A405D3288324442 /* BalanceErrorViewController.swift in Sources */, + D033289F2BF6199600BBB364 /* InfoNewView.swift in Sources */, 1A564D3DB55C8CB8B5AED664 /* BalanceErrorViewModel.swift in Sources */, D36DE0DE272FD887000BC916 /* OneInchViewModel.swift in Sources */, 1A5649926B0064083045BEEB /* BalanceErrorService.swift in Sources */, @@ -11199,7 +11074,6 @@ 11B356448BC036CD117EB7DC /* BrandFooterCell.swift in Sources */, 58AAA670C62548F1A92D680B /* CoinSelectModule.swift in Sources */, 58AAA1721028D6504074A158 /* CoinSelectViewController.swift in Sources */, - 3A73FC74258B1ADA00FE4D34 /* MarketViewController.swift in Sources */, 58AAA187A094370FA7CD2BDD /* InfoViewController.swift in Sources */, D05E96972A262149002CCD71 /* TronTransactionRecord.swift in Sources */, 58AAAE9383600A15C521DBD8 /* InfoModule.swift in Sources */, @@ -11218,6 +11092,7 @@ 58AAA7B99324DDA9C53692AD /* SwapApproveViewController.swift in Sources */, 58AAA64C3C4E4FA0E4213000 /* SwapApproveModule.swift in Sources */, 58AAAAC61D3D8AD1AC4BEEAE /* AdditionalDataWithErrorView.swift in Sources */, + D3B73E2A2BDBC61D0067429D /* EvmPreSendHandler.swift in Sources */, 58AAACD7A57AA93736CDB54D /* SwapApproveAmountView.swift in Sources */, 58AAA29F80AC65D37BD1D289 /* CoinSelectViewModel.swift in Sources */, 50701ACE25B041E600EDE51B /* JailbreakTestManager.swift in Sources */, @@ -11226,6 +11101,7 @@ 11B350D6CBB602F510882F1E /* WalletConnectRequest.swift in Sources */, D02A67CB272A7460009B2C1C /* TwitterUsersResponse.swift in Sources */, D008CA5A267C8DDF00001E0A /* EvmIncomingTransactionRecord.swift in Sources */, + D0DEFF0A2BD1257F004C9DF0 /* BitcoinTransactionService.swift in Sources */, 11B358902CE8D7EF2AD38448 /* CoinService.swift in Sources */, 1A5643BCA38A75A63D57F1AB /* SendEthereumErrorCell.swift in Sources */, 58AAA0642CB9B7B19C6235B5 /* AmountDecimalParser.swift in Sources */, @@ -11251,7 +11127,6 @@ D0C226132A66A3DB007101F7 /* PersonalSupportViewController.swift in Sources */, 11B3550424326606B055D7E5 /* AboutModule.swift in Sources */, D0A980AF2B60E73F00127AF4 /* LegacyFeeSettingsView.swift in Sources */, - 3A73FC99258B1AF600FE4D34 /* MarketWatchlistViewController.swift in Sources */, 11B35959AAF414186CE39698 /* AddTokenViewModel.swift in Sources */, 11B35A90F19DE20ABE21F423 /* AddTokenService.swift in Sources */, 11B35FAB3263E489CB9017FC /* AddTokenViewController.swift in Sources */, @@ -11276,10 +11151,8 @@ 11B357EA92E2E9C39DEF08B0 /* AmountInputView.swift in Sources */, 11B35FF65FCE441A69822E1C /* InputPrefixWrapperView.swift in Sources */, D3DD67342BC3CC2100EC7F78 /* ThorChainMultiSwapBtcQuote.swift in Sources */, - 179E7EBD494842280D9F19A4 /* FavoriteCoinRecord.swift in Sources */, 11B35C3AFFA5B40481AF15B9 /* AccountRecord_v_0_19.swift in Sources */, - 58AAA8D67EB6C19719BD760B /* MarketWatchlistService.swift in Sources */, - 11B35C8E09922F59B200E347 /* MarketListViewController.swift in Sources */, + 6B8BD39E2C11B959003ADE10 /* TextFieldAlert.swift in Sources */, 11B3556FCACE2ECE022138DF /* AddEvmTokenBlockchainService.swift in Sources */, D0E659BB2B875003000D8981 /* ResendBitcoinViewModel.swift in Sources */, 11B355FAD0E7823AF5F8EC83 /* SendEvmTransactionService.swift in Sources */, @@ -11296,9 +11169,11 @@ 11B3547BF16FF0C22F1D629D /* SendEvmViewController.swift in Sources */, 11B355EC1ED6673F7F3E196C /* SendEvmService.swift in Sources */, 11B350F8D2D09F1BD5ECF003 /* SendEvmViewModel.swift in Sources */, + D3DB51A22BD6857E0091BBDB /* MarketGlobalView.swift in Sources */, 11B35063C586D8E1365E9DFF /* SendEvmConfirmationViewController.swift in Sources */, 11B355984178BF117AB606F5 /* SendAvailableBalanceCell.swift in Sources */, 11B355C78B016FC2EDDAECCC /* SendEvmModule.swift in Sources */, + D084F6C12BEB951C00407FA4 /* OutputSelectorViewModel2.swift in Sources */, 11B353E61A5496074178741C /* SendAvailableBalanceViewModel.swift in Sources */, 58AAA8757A05F21766ED0EA5 /* SimpleSheetTitleView.swift in Sources */, 11B3516FFFEA842D66C51988 /* AmountTypeSwitchService.swift in Sources */, @@ -11309,7 +11184,6 @@ 1A56405DB1540DEC70FD5CFA /* HighlightedDescriptionBaseView.swift in Sources */, 1A564058DB366C810AC3C47A /* TitledHighlightedDescriptionView.swift in Sources */, 1A564564359185321F81541D /* TitledHighlightedDescriptionCell.swift in Sources */, - 58AAA48687661E27807E9DF1 /* FavoritesManager.swift in Sources */, 58AAA8D2A6FD519EFC668EC5 /* CoinPageModule.swift in Sources */, 58AAABA864BFEC1F10F57E4B /* CoinChartService.swift in Sources */, D36DE0EF272FD89B000BC916 /* SwapSettingsViewController.swift in Sources */, @@ -11321,6 +11195,7 @@ 1A564D81725888B31D56F389 /* PerformanceContentCollectionViewCell.swift in Sources */, 1A5648B6D95710939690F22A /* PerformanceTableViewCell.swift in Sources */, 1A564E09FB049006167E033B /* PerformanceSideCollectionViewCell.swift in Sources */, + D3833AFF2BF335D100ACECFB /* MarketNewsViewModel.swift in Sources */, D36DE0D8272FD887000BC916 /* OneInchDataSource.swift in Sources */, 58AAA2EBAFC1C443C48BA857 /* CoinChartFactory.swift in Sources */, 11B355EE734C8CAC81BA1BF9 /* CoinInvestorsModule.swift in Sources */, @@ -11348,16 +11223,20 @@ 11B3553D410457FB50DEFAE4 /* ManageAccountsViewModel.swift in Sources */, 11B3508846C7EB6EDC26E52C /* ManageAccountsService.swift in Sources */, 11B35F49252D04F02BD9C6D0 /* ManageAccountModule.swift in Sources */, + D3DB519C2BD685180091BBDB /* RedactedModifier.swift in Sources */, 11B35CB5B1C1F2D9EAF6DCBC /* ManageAccountService.swift in Sources */, 11B359F4651EA254E5B0AD00 /* ManageAccountViewController.swift in Sources */, 11B354D8DCBDAA82A6C51205 /* ManageAccountViewModel.swift in Sources */, 11B356C6E9FC6594917B3FF6 /* ActiveAccount.swift in Sources */, D02A67C8272A7460009B2C1C /* CoinTweetsViewController.swift in Sources */, D02A67C2272A7460009B2C1C /* TwitterText.swift in Sources */, + D3833AF82BF2181800ACECFB /* MarketPair.swift in Sources */, 6BE8A0812ADE2FAB0012DE7F /* CurrencyManager.swift in Sources */, + D349031A2BE8DF5F005F147B /* BinancePreSendHandler.swift in Sources */, 11B359F73F1D626BF832977F /* BackupModule.swift in Sources */, D0A980A92B5E3C0900127AF4 /* StepChangeButtonsView.swift in Sources */, 6BCD530C2A161F4100993F20 /* ICloudBackupNameViewModel.swift in Sources */, + D3833ADE2BEE3FE000ACECFB /* MarketPlatformsView.swift in Sources */, D09D76942A2E07BD004311E6 /* SendTronConfirmationService.swift in Sources */, 11B35328067C30C80DF244DF /* BackupVerifyWordsViewModel.swift in Sources */, 11B354460024FA6EDB8B27DC /* BackupVerifyWordsService.swift in Sources */, @@ -11373,6 +11252,7 @@ 11B3524401E294D8A919186E /* EnabledWallet_v_0_20.swift in Sources */, 11B3512E20E90C3332EFCC1B /* RestoreSettingRecord.swift in Sources */, 11B3504F9DECABF08D8C82BC /* RestoreSettingsManager.swift in Sources */, + D3A5808B2BE4DB11003953F4 /* WalletConnectSendHandler.swift in Sources */, 11B3588A5791AB25FA1BBEE0 /* RestoreSettingsViewModel.swift in Sources */, 11B35BC2641349F06DF6B6FF /* RestoreSettingsView.swift in Sources */, D09200C2293F21720091981A /* RestoreNonStandardViewController.swift in Sources */, @@ -11403,6 +11283,7 @@ 1A5645D6AC7D344E6D3D0CAF /* AppVersion_v_0_20.swift in Sources */, 1A564699CFB7CCE8BD3E5245 /* AppVersionStorage.swift in Sources */, 11B35C59583AFCDFD828B9D1 /* TextFieldStackView.swift in Sources */, + D389BC492C0DDA8F00724504 /* HsTimePeriod.swift in Sources */, 11B356B6BEB9562F670DFAC5 /* Caution.swift in Sources */, 11B350F9484020EFF74EFF1F /* MnemonicInputCell.swift in Sources */, 11B3547FBABFB1F67D778E10 /* TextFieldCell.swift in Sources */, @@ -11414,14 +11295,15 @@ 58AAAFD1F07293D4691F2294 /* MetricChartFactory.swift in Sources */, 58AAAAE64799E5DD40D4C54A /* MetricChartModule.swift in Sources */, 58AAA0CA81351C80C4F2E168 /* MetricChartViewController.swift in Sources */, + 6BB14F7B2C05FBAC00E879B2 /* MarketTvlViewModel.swift in Sources */, 58AAA4109DE36F8934808DE0 /* MarketGlobalModule.swift in Sources */, 11B352840E06275F96EFFCDB /* BaseCurrencySettingsModule.swift in Sources */, - 58AAA25AF71DF84F42A27157 /* MarketPostViewModel.swift in Sources */, + D3DB51B22BD912A00091BBDB /* MarketInfo.swift in Sources */, D023D26D2A24CD4F004F65B0 /* TronKitManager.swift in Sources */, - 58AAA2494D41B33DC091A3E6 /* MarketPostService.swift in Sources */, D00267BC2A57E72700D6B2D5 /* ResendPasteInputView.swift in Sources */, 1A56464440899E3299F79D32 /* JailbreakService.swift in Sources */, 58AAAE5341084CFA30D4832C /* CoinPageMarkdownParser.swift in Sources */, + D3833AFC2BF335C700ACECFB /* MarketNewsView.swift in Sources */, D05E96A62A2627E5002CCD71 /* TronOutgoingTransactionRecord.swift in Sources */, 1A5640D6FDF86EDB54213F9B /* DeepLinkManager.swift in Sources */, D36DE0EC272FD89B000BC916 /* SwapSettingsModule.swift in Sources */, @@ -11437,6 +11319,7 @@ D36DE0C0272FD864000BC916 /* UniswapService.swift in Sources */, 11B35ED22837284580055F0A /* BalanceData.swift in Sources */, 11B3534B567884E30A871F32 /* AddTokenModule.swift in Sources */, + 6BB14F722BFE550600E879B2 /* MarketEtfFetcher.swift in Sources */, 11B35758262A961566ABB87F /* AddBep2TokenBlockchainService.swift in Sources */, 58AAADEE16D9605E4FA0390A /* UniswapSettingsViewModel.swift in Sources */, D09200C5293F21720091981A /* RestoreNonStandardViewModel.swift in Sources */, @@ -11445,6 +11328,7 @@ 58AAA63F99B1FC1B88B5C8A0 /* UniswapSettings.swift in Sources */, 58AAAD97F2A1F00258CF9E00 /* OneInchSettings.swift in Sources */, D0C226192A66A703007101F7 /* PersonalSupportViewModel.swift in Sources */, + D34903172BE8DF48005F147B /* BinanceSendHandler.swift in Sources */, 58AAA126020F429D94D77A76 /* OneInchSettingsService.swift in Sources */, 58AAAB67059E3F289F557860 /* OneInchSettingsViewModel.swift in Sources */, 58AAA71839C54D05E04BC2EC /* UniswapSettingsDataSource.swift in Sources */, @@ -11456,7 +11340,9 @@ 58AAA289FEAE983739189B8D /* RecipientAddressViewModel.swift in Sources */, 58AAA49049C7EA04AABC41E9 /* SwapSlippageViewModel.swift in Sources */, 58AAA9ABBBA6A19CBC351D52 /* AddressResolutionProvider.swift in Sources */, + D086A9162BF4D08400462024 /* SendParameters.swift in Sources */, D0D5BCBC2976CB9F00587FDB /* PasswordInputView.swift in Sources */, + D3DB519F2BD6854A0091BBDB /* MarketSearchView.swift in Sources */, 58AAA10B748931BA5FA867DA /* SwapViewModel.swift in Sources */, 58AAA9475B1C057E82B25C76 /* OneInchFeeService.swift in Sources */, 58AAA6371183D7FB9606FEDA /* OneInchSendEvmTransactionService.swift in Sources */, @@ -11480,6 +11366,7 @@ 58AAA926E1D95F61CA06EFB8 /* SwapConfirmationModule.swift in Sources */, 58AAA1152EEEBC93FCC3CAAC /* SwapConfirmationViewController.swift in Sources */, 58AAAC9A4813120F3B786D18 /* SwapConfirmationAmountCell.swift in Sources */, + D0A6902B2C00ACF600E59296 /* CautionDataSource.swift in Sources */, 2FA5DEDEAF3C1D6251B47CB4 /* TransactionInfoModule.swift in Sources */, 2FA5DE68EF4783154A53CCEB /* TransactionInfoService.swift in Sources */, 2FA5D5FCFC8A89956755FAA3 /* TransactionInfoViewController.swift in Sources */, @@ -11523,6 +11410,8 @@ 11B35E4FE3117F6B681F6748 /* Wallet.swift in Sources */, 11B35CAD5A7E0C8709559FD2 /* WalletManager.swift in Sources */, 11B35D80D1A22BA2EB8F31B8 /* WalletStorage.swift in Sources */, + D3384D512C0703B400515664 /* PriceChangeModeManager.swift in Sources */, + D389BC522C0DEF2200724504 /* MarketAdvancedSearchResultsViewModel.swift in Sources */, 11B355A29CDAF16148F1C546 /* CoinManager.swift in Sources */, D36DE0B0272FD689000BC916 /* SwapModule.swift in Sources */, 11B3580B9C21B55ACC07B043 /* AdapterManager.swift in Sources */, @@ -11531,82 +11420,44 @@ 11B351909FE0FA637B5B1EC5 /* CoinValue.swift in Sources */, 2FA5DE1250DA9D85CD9BF1A3 /* TransactionValue.swift in Sources */, 58AAA72230207B253E2153EA /* SelectorButton.swift in Sources */, - 58AAAB5D89B03B266A4F9B57 /* MarketOverviewViewController.swift in Sources */, - 58AAAB41ED4FC861D1C99AAC /* MarketOverviewHeaderCell.swift in Sources */, D36DE0E4272FD887000BC916 /* OneInchService.swift in Sources */, - 58AAA07B54F11F0DF5E3E876 /* MarketPostViewController.swift in Sources */, - 58AAA31D8AD811C0C5434426 /* MarketPostModule.swift in Sources */, - 11B35EF70120304F6D4F5561 /* MarketMultiSortHeaderView.swift in Sources */, D05E969A2A26278D002CCD71 /* TronApproveTransactionRecord.swift in Sources */, 2FA5D40FA7CAC1BA01C0B373 /* CoinOverviewViewModel.swift in Sources */, 2FA5DBE42827FF3D114DBF4B /* CoinOverviewService.swift in Sources */, 2FA5DF4BB73C12C8DDF42599 /* CoinOverviewModule.swift in Sources */, D05E969D2A2627AF002CCD71 /* TronContractCallTransactionRecord.swift in Sources */, 2FA5DF596B5EE00090E050D9 /* CoinOverviewViewController.swift in Sources */, - 11B3502DA4A2B869638AF4D8 /* MarketListViewModel.swift in Sources */, - 11B35CCC81AE83E1CBB61504 /* MarketMultiSortHeaderViewModel.swift in Sources */, - 11B353260AE7B998C07955E6 /* MarketCategoryService.swift in Sources */, D36DE0C9272FD864000BC916 /* UniswapProvider.swift in Sources */, - 11B3553109794AE192BF7591 /* MarketCategoryModule.swift in Sources */, - 11B354ECE594CE629BA46BE1 /* MarketCategoryViewModel.swift in Sources */, - 11B3504029EB87A32DB63666 /* MarketTopService.swift in Sources */, - 11B35A426FD3D729DEB89DEA /* MarketTopViewController.swift in Sources */, - 11B357405174FF9F9BEB3704 /* MarketTopModule.swift in Sources */, - 11B352712EC6F2C7F6965443 /* MarketWatchlistViewModel.swift in Sources */, - 58AAA2960B54658E2614D72E /* MarketGlobalMetricService.swift in Sources */, - 58AAA996622FCD647B51A3C5 /* MarketGlobalMetricModule.swift in Sources */, - 58AAA5C000029E9EB74C46C4 /* MarketGlobalMetricViewController.swift in Sources */, D00DAE452B626C2900F48E1D /* GasPrice.swift in Sources */, - 58AAA004F244AFFC352ADCEF /* MarketSingleSortHeaderView.swift in Sources */, - 58AAA35AF4F4454E0E9C7C60 /* MarketSingleSortHeaderViewModel.swift in Sources */, - 11B353CB3021FA5266D07607 /* MarketWatchlistToggleService.swift in Sources */, - 11B35F1152FB1004E554B922 /* MarketOverviewMetricsCell.swift in Sources */, - 11B3521427196CEE9057D6A0 /* MarketAdvancedSearchResultModule.swift in Sources */, - 11B358CB129212E2A0E455E4 /* MarketAdvancedSearchResultViewController.swift in Sources */, - 11B35B6586B14C6A9F35E39D /* MarketAdvancedSearchResultService.swift in Sources */, D02A67D1272A7460009B2C1C /* TweetsPageResponse.swift in Sources */, - 11B35DDC98FFF447333278FF /* MarketAdvancedSearchViewController.swift in Sources */, - 11B3506ECD6D4D8D0C7717B6 /* MarketAdvancedSearchViewModel.swift in Sources */, - 11B359DDFEAF887EEE3063A7 /* MarketAdvancedSearchService.swift in Sources */, - 11B350FF14C22F4CE7FB153F /* MarketAdvancedSearchModule.swift in Sources */, - 11B35C5E7A90AA7B302EB0CD /* MarketListMarketFieldDecorator.swift in Sources */, - 58AAA08E3204C7E7326E1DF9 /* MarketTvlSortHeaderViewModel.swift in Sources */, - 58AAA626C0B12976749A948E /* MarketTvlSortHeaderView.swift in Sources */, 11B35411C817EDE73D4E6242 /* CoinPageService.swift in Sources */, D087627629815DAE00E6FFD4 /* ChooseWatchViewModel.swift in Sources */, 11B35ED9D5F95988E9335440 /* CoinAnalyticsModule.swift in Sources */, + D389BC4C2C0DDCF500724504 /* MarketAdvancedSearchBlockchainsView.swift in Sources */, D0118E4B2B7CC63300D55CE6 /* ResendBitcoinViewController.swift in Sources */, 11B357DD5767D3922E261237 /* CoinAnalyticsViewController.swift in Sources */, 11B352AEE675D3E19701FC76 /* CoinAnalyticsService.swift in Sources */, 11B3529CFD24A94DC35B476E /* CoinAnalyticsViewModel.swift in Sources */, 2FA5DE2F0DF6317DB807B7F3 /* TweetAttachmentView.swift in Sources */, 2FA5D6224C65D97B910F3A20 /* TweetPollView.swift in Sources */, + 6BB14F752C01D04200E879B2 /* CheckBoxUiView.swift in Sources */, 2FA5D3C9CDBD7CF26508E1E9 /* ReferencedTweetView.swift in Sources */, 11B3551E5E9A6D167F7BA078 /* LaunchScreenManager.swift in Sources */, 11B35FA6F9EE876BD65E9AD6 /* LaunchScreen.swift in Sources */, D07157DB2A2DD968006F141F /* SendTronModule.swift in Sources */, 11B351EE1B16B2A26B5D6A40 /* CoinInvestorsService.swift in Sources */, - 11B35EDC3703B04ED8B72BA8 /* CoinTreasuriesModule.swift in Sources */, 6B2907272AF0CB8A006157D6 /* WidgetCoinAppShowModule.swift in Sources */, D0532CC42B149E450015DF40 /* WatchService.swift in Sources */, - 11B35147B6C9B62FC86A5BB9 /* CoinTreasuriesViewController.swift in Sources */, - 11B3503C1335F0860B6DF7B8 /* CoinTreasuriesService.swift in Sources */, 6BCD53042A161F4100993F20 /* ICloudBackupTermsViewController.swift in Sources */, 11B35E99BBF6DCCA72BDA4D1 /* CoinTreasuriesViewModel.swift in Sources */, - 11B351F81CCF4675CBDAC9B0 /* DropdownSortHeaderView.swift in Sources */, + D054DAE32BE5123F0040B7C9 /* InitialTransactionSettings.swift in Sources */, 11B354542A3E929C1A7924FD /* CoinReportsViewController.swift in Sources */, 11B35FB74A0FAB9385945628 /* CoinReportsModule.swift in Sources */, 11B35CB98E9934D0C8EB4F08 /* CoinReportsViewModel.swift in Sources */, 11B35E48AA074CB56CAC4A9C /* CoinReportsService.swift in Sources */, 11B35EBC08855AC0CDC0AF09 /* PostCell.swift in Sources */, - 58AAAAC777502E0C331C109F /* MarketGlobalDefiMetricService.swift in Sources */, 6BA5117D2BCFA06F00CB5A54 /* FirstAppearModifier.swift in Sources */, - 58AAA153DF6764D7FEA99D63 /* MarketGlobalTvlMetricService.swift in Sources */, - 58AAAEB0F729B839B9B99A04 /* MarketListDefiDecorator.swift in Sources */, - 58AAA331B4A743D9183F8449 /* MarketListTvlDecorator.swift in Sources */, - 58AAACA5A8EC9B2A3182395F /* MarketGlobalTvlFetcher.swift in Sources */, - 11B35D16F2F7B8E11A771B18 /* FavoriteCoinRecord_v_0_22.swift in Sources */, - 11B35D46B65772A1CC17B099 /* MarketGlobalTvlMetricViewController.swift in Sources */, + 11B35D16F2F7B8E11A771B18 /* FavoriteCoinRecord_v_0_38.swift in Sources */, 58AAADEF6F21AED422D7B569 /* AddressParserChain.swift in Sources */, 58AAAC635552C279592F60F9 /* EvmAddressParserItem.swift in Sources */, D3DD672E2BC3C5C400EC7F78 /* ThorChainMultiSwapEvmQuote.swift in Sources */, @@ -11619,6 +11470,7 @@ 1A5646632409BBC2A8790807 /* ReleaseNotesMarkdownConfig.swift in Sources */, 11B35B10C473B24F3AD2F1D3 /* EvmAccountManager.swift in Sources */, 11B35CFD84BE04CC93DE7DF9 /* UnlinkWatchViewController.swift in Sources */, + D3A580942BE8AA80003953F4 /* BitcoinSendSettingsView.swift in Sources */, 58AAAE0E85B0DFB61C94E14D /* BitcoinAddressParserItem.swift in Sources */, 1A5645C7334EED7FF57FFD47 /* DashAddressParserItem.swift in Sources */, 1A564AB8471E098F68FDB9D7 /* BinanceAddressParserItem.swift in Sources */, @@ -11631,6 +11483,7 @@ 11B35CF897C3C30DACC27693 /* NftStorage.swift in Sources */, 2FA5D3AFFBE0E51DD79236EF /* LegacyEvmFeeDataSource.swift in Sources */, 2FA5DB404D05A47AD451C1B5 /* LegacyGasPriceService.swift in Sources */, + D092C58B2C12DE3D0060D915 /* PriceChangeManager+HsTimePeriod.swift in Sources */, 2FA5D7E8805931F9F566668E /* SendEvmCautionsFactory.swift in Sources */, 2FA5D5189F8BE0AD6E10BA6B /* FeeCell.swift in Sources */, 11B353F671EA70F8BE7F02F0 /* NftAssetTitleCell.swift in Sources */, @@ -11640,6 +11493,7 @@ 1A5649CEE6EFCC06A10F69CF /* TraitCell.swift in Sources */, ABC9A3C3FC55AB33A9896382 /* UIViewController.swift in Sources */, 6BE8A07B2ADE2F8D0012DE7F /* Currency.swift in Sources */, + D3F9B0372BE3B5AA009FFA95 /* WalletConnectSendView.swift in Sources */, ABC9A9AC7890BE4AAE7DDC84 /* WalletConnectSessionManager.swift in Sources */, 2FA5D18A57B386FD3A4384BA /* Eip1559EvmFeeViewModel.swift in Sources */, 2FA5DA30F665BF2C170210C8 /* Eip1559EvmFeeDataSource.swift in Sources */, @@ -11657,12 +11511,13 @@ ABC9ADF2CFB90B882B5DE3F9 /* WalletConnectService.swift in Sources */, ABC9A4F4B7F17169DC240A98 /* WalletConnectUriHandler.swift in Sources */, ABC9AA27A709AC5F85176A53 /* WalletConnectModule.swift in Sources */, + 6BB14F7D2C05FBAF00E879B2 /* MarketTvlView.swift in Sources */, ABC9ABC375B65451761D4766 /* SendFeeViewModel.swift in Sources */, ABC9A933C2603486BA181B19 /* SendFeeService.swift in Sources */, + 6B5F5E182C0DDD8700E03EB2 /* RankViewModel.swift in Sources */, ABC9A4B643D98FB95F431401 /* SendBitcoinAmountInputService.swift in Sources */, ABC9ABF99296DEA24FC5BFF0 /* SendAmountCautionService.swift in Sources */, ABC9AB1E703AE57DF856ECD9 /* SendAmountCautionViewModel.swift in Sources */, - 1A564E2897197EB14584A62E /* MarketOverviewViewModel.swift in Sources */, 11B3538CDAA5FE6682A661D0 /* EvmAccountManagerFactory.swift in Sources */, 11B35696E9CD808522BEFCD6 /* BlockchainSettingRecordStorage.swift in Sources */, 11B3581F4D975FC21B9A25F2 /* BtcBlockchainSettingsModule.swift in Sources */, @@ -11670,6 +11525,7 @@ 11B35D2C28E3116F58A543E2 /* BtcBlockchainSettingsService.swift in Sources */, 11B358AE5241256C9AAFB588 /* SyncMode_v_0_24.swift in Sources */, 11B359AA9A3F1FD68323C64E /* BlockchainSettingRecord_v_0_24.swift in Sources */, + D311DA1C2BD114B00013DB8F /* MarketView.swift in Sources */, ABC9A70AE588307EA1D3A414 /* SendConfirmationService.swift in Sources */, 6BCD53062A161F4100993F20 /* ICloudBackupTermsService.swift in Sources */, ABC9A556361B2644555659D8 /* SendConfirmationViewController.swift in Sources */, @@ -11677,8 +11533,8 @@ 11B3572E55D689C7414621EE /* SectionsTableView.swift in Sources */, 11B350CB4E7C006C26AE5FB3 /* EnabledWalletStorage.swift in Sources */, 11B356C081AC552CBDA9147B /* AccountRecordStorage.swift in Sources */, + D3DB51992BD63D680091BBDB /* MarketSearchViewModel.swift in Sources */, 11B35727A4950C1E066F2244 /* LogRecordStorage.swift in Sources */, - 11B3532D56D7D1F8286B39AC /* FavoriteCoinRecordStorage.swift in Sources */, 11B35262B98EA59CDA12DF97 /* WalletConnectSessionStorage.swift in Sources */, 11B350ACC851B0F8C911AC3E /* ActiveAccountStorage.swift in Sources */, 11B35A2DC1B9521150F81EA6 /* RestoreSettingsStorage.swift in Sources */, @@ -11706,20 +11562,14 @@ 11B35AC60BE4DC210C3C2312 /* NftActivityService.swift in Sources */, 11B35DC513240DBF0C78ED92 /* NftAssetButtonCell.swift in Sources */, 11B351DDFD1A7BC393EFA6E1 /* CustomToken.swift in Sources */, - 1A56405B48462C0750A47ECA /* MarketOverviewTopCoinsDataSource.swift in Sources */, - 1A564A45605FA993F9680646 /* MarketOverviewTopCoinsViewModel.swift in Sources */, - 1A5649ADA72E39CE24BB64FC /* MarketOverviewCategoryDataSource.swift in Sources */, - 1A56470F4D18CCC43B85DEB3 /* MarketOverviewCategoryViewModel.swift in Sources */, - 1A564FC84916FF6D6224FB33 /* MarketOverviewCategoryCell.swift in Sources */, - 1A56405220ED225C0B973A7F /* MarketOverviewTopCoinsService.swift in Sources */, - 1A564A4CF522A7A959482AA6 /* MarketOverviewGlobalService.swift in Sources */, - 1A564F3C50FC28F2AF4AF4ED /* MarketOverviewGlobalViewModel.swift in Sources */, - 1A5643CB57594E84707686A3 /* MarketOverviewGlobalDataSource.swift in Sources */, + D3A580972BE8AA90003953F4 /* BitcoinSendSettingsViewModel.swift in Sources */, + D3F9B02B2BE3A9A1009FFA95 /* MultiSwapSendView.swift in Sources */, + 6B5F5E152C0DDD7100E03EB2 /* RankView.swift in Sources */, ABC9A5CB5C5D56F50FE5F64C /* SendTimeLockErrorService.swift in Sources */, ABC9A9493F250B81E1152012 /* SendBitcoinService.swift in Sources */, ABC9A774500F8D8D3D9E04DD /* SendBitcoinAdapterService.swift in Sources */, ABC9A08340695A0AFCE9C2F2 /* SendBitcoinViewController.swift in Sources */, - ABC9A69BADD39C6E9239A2A1 /* SendViewModel.swift in Sources */, + ABC9A69BADD39C6E9239A2A1 /* SendViewModelOld.swift in Sources */, ABC9A7E28714A9A19A2160D4 /* SendModule.swift in Sources */, ABC9A5BBFC1960B1DD8F62B7 /* SendBinanceService.swift in Sources */, ABC9A621D59D9DAE28A03865 /* SendBaseService.swift in Sources */, @@ -11737,13 +11587,6 @@ ABC9AA85AC9DC60A08211D16 /* SendZcashViewController.swift in Sources */, ABC9A78D3A4267CAC0F5D0E8 /* SendConfirmationModule.swift in Sources */, ABC9A1D42EED3235129D810B /* BaseSendViewController.swift in Sources */, - 1A56444C8342498C892E931E /* MarketNftTopCollectionsModule.swift in Sources */, - 1A564240E0BC3E93EDB3BA22 /* MarketNftTopCollectionsService.swift in Sources */, - 1A56427F500823630D07E75D /* MarketNftTopCollectionsViewModel.swift in Sources */, - 1A564DCC65AA9EADE56F2B7F /* MarketNftTopCollectionsViewController.swift in Sources */, - 1A5643E2C1AB8F24605342A0 /* MarketListNftCollectionDecorator.swift in Sources */, - 1A5646322B606C56DFFA324A /* NftCollectionsMultiSortHeaderViewModel.swift in Sources */, - 1A56422231460374E3830E56 /* MarketListWatchViewModel.swift in Sources */, ABC9A69FA41A9BC474DD1915 /* DiffLabel.swift in Sources */, ABC9A96132AD85DD613EC773 /* ProFeaturesStorage.swift in Sources */, ABC9AEDCDECE827D610F025A /* ProFeaturesAuthorizationAdapter.swift in Sources */, @@ -11752,27 +11595,16 @@ 11B352FF1C3FA152E2EEFE67 /* EvmMethodLabel.swift in Sources */, 11B35FB7BEC8C0DA32C1F670 /* EvmAddressLabel.swift in Sources */, 11B3535C10D649F8CD1BCDAF /* HsLabelProvider.swift in Sources */, + D3A580882BE4DAA2003953F4 /* EvmSendData.swift in Sources */, 11B35CADA5EE093B974C5A4A /* EvmLabelManager.swift in Sources */, 11B35C8A95E4C8D43F9279B6 /* EvmLabelStorage.swift in Sources */, 11B351DED0D2632D24084263 /* EvmUpdateStatus.swift in Sources */, 11B35E83437BEB5CCE342ACB /* SyncerState.swift in Sources */, 11B35E61083F8A098D458EBC /* SyncerStateStorage.swift in Sources */, - 1A5648701B55E09FD8E4585D /* MarketOverviewTopPlatformsViewModel.swift in Sources */, - 1A5647BB75003E8292C8144B /* BaseMarketOverviewTopListDataSource.swift in Sources */, ABC9A6D1C4EF73D4A8D6F3BD /* CoinProChartModule.swift in Sources */, + D3DB51AF2BD7AF860091BBDB /* DiffText.swift in Sources */, ABC9AECE6AD4A9DEA41DDBD9 /* ProChartFetcher.swift in Sources */, - 1A5648ACF2A5B2F7420DA5F6 /* MarketTopPlatformsModule.swift in Sources */, - 1A564E08B4F6C5B1CDB121F6 /* MarketTopPlatformsService.swift in Sources */, - 1A56402D725D6AB0C4149066 /* MarketTopPlatformsViewModel.swift in Sources */, - 1A564A9E6FF0EB1C6F2AA102 /* MarketTopPlatformsViewController.swift in Sources */, - 1A5646E52D8DFADAA5ACFCAD /* TopPlatformsMultiSortHeaderViewModel.swift in Sources */, - 1A5643B0422BC87461CC25C5 /* MarketOverviewTopPlatformsDataSource.swift in Sources */, - 1A56418183EC6CB602873B51 /* MarketListTopPlatformDecorator.swift in Sources */, ABC9A59B465A9C59F93DFB96 /* ChartCell.swift in Sources */, - ABC9AD3276132B33F6045AFF /* MarketCategoryMarketCapFetcher.swift in Sources */, - 11B358122FE64E16EC25F095 /* MarketOverviewService.swift in Sources */, - 11B35D06C75228488867EB22 /* MarketOverviewTopPlatformsService.swift in Sources */, - 11B352A7A3457BBAF4BF704F /* MarketOverviewCategoryService.swift in Sources */, ABC9A9562DD283B6FCACBCF9 /* MarketCardTitleView.swift in Sources */, ABC9AC1BD5C95957726F8AE8 /* MarketCardValueView.swift in Sources */, ABC9ADE7C30F3F992FD9E1CC /* Array.swift in Sources */, @@ -11798,6 +11630,7 @@ 1A564E69BA99DF8CD4562902 /* PlaceholderViewModule.swift in Sources */, ABC9A91D03FB46F6AD21EEF4 /* WalletConnectSocketConnectionService.swift in Sources */, 11B3564FBC180A0E6D30BCFA /* TransactionsModule.swift in Sources */, + D0E5E8552BE38AA2005080A4 /* TronSendHandler.swift in Sources */, 11B3501BA3EA411F93E9C694 /* TransactionsService.swift in Sources */, 11B354DB9BD0F91CFF4EB9C6 /* TransactionsViewItemFactory.swift in Sources */, 11B352E420B3FBCD85612E63 /* TransactionsViewController.swift in Sources */, @@ -11805,7 +11638,9 @@ 11B35AB0C3F757E23D249330 /* TransactionTypeFilter.swift in Sources */, 11B35A3FD624D80C6D98A1CF /* TransactionSource.swift in Sources */, 11B3515A1106B2AE8509263E /* Pool.swift in Sources */, + D3F9B0342BE3B3A7009FFA95 /* EvmDecorator.swift in Sources */, 11B3541BC8A0EF1066EBA464 /* NonSpamPoolProvider.swift in Sources */, + D09C5C632C076C0E00E6909E /* CoinIconView.swift in Sources */, 11B3594DD9B54E11190B4CD5 /* PoolProvider.swift in Sources */, 11B351E4BD2180A5D6D59F23 /* PoolGroup.swift in Sources */, 11B35AC389ACC3E4096EC645 /* PoolGroupFactory.swift in Sources */, @@ -11823,9 +11658,8 @@ 11B356FF5E5CC9AA34B89C86 /* HeaderAmountView.swift in Sources */, ABC9A7CBFDC0DF741E29EA44 /* Integer.swift in Sources */, ABC9A3D46AA7356763213BA6 /* FeePriceScale.swift in Sources */, - 1A564C1395A1264F1A9B3AB5 /* TopPlatformModule.swift in Sources */, - 1A5648B10B34B402167EEA84 /* TopPlatformService.swift in Sources */, 1A5640DE72AC306799695F48 /* TopPlatformMarketCapFetcher.swift in Sources */, + D3F9B0252BE38AF1009FFA95 /* RegularSendView.swift in Sources */, 11B357D1A2BD673DAB7B4C61 /* SecondaryButtonCell.swift in Sources */, 11B35B3C7A60FEE011EFBF73 /* InputSecondaryCircleButtonWrapperView.swift in Sources */, 11B35C1ACD5B3874921CEFDE /* RecoveryPhraseModule.swift in Sources */, @@ -11843,12 +11677,15 @@ 6BCD53022A161F4100993F20 /* ICloudBackupTermsViewModel.swift in Sources */, 11B35066480B6EB9F124EBC0 /* ExtendedKeyService.swift in Sources */, 11B359601D8967EEE00B5991 /* BackupMnemonicWordsCell.swift in Sources */, + D3402AF12BF5D59D003BF6F8 /* WatchlistModifier.swift in Sources */, 11B3590E40C88ADD16DEEABB /* BackupMnemonicWordCell.swift in Sources */, 11B35FD18C255E2C6D75F38A /* RestoreMnemonicHintView.swift in Sources */, + D3402AF72BF71C11003BF6F8 /* WatchlistManager.swift in Sources */, 11B356BCDD5E64C6D6F49489 /* RestoreMnemonicHintCell.swift in Sources */, 11B35B152001ADE5E98D1414 /* EvmAccountRestoreState.swift in Sources */, 11B3573F7ED8577EF9F12EF9 /* EvmAccountRestoreStateManager.swift in Sources */, 11B3563B5D19C7A4EDFC8FC1 /* EvmAccountRestoreStateStorage.swift in Sources */, + D3402AEE2BF5D58B003BF6F8 /* WatchlistViewModel.swift in Sources */, 11B356C6C07BE3588A5D52DE /* NftAdapterManager.swift in Sources */, 11B3583C1A73A11974ADAEBB /* EvmNftAdapter.swift in Sources */, 11B3575108E28705A2F47BA9 /* NftViewController.swift in Sources */, @@ -11879,6 +11716,7 @@ 11B355028142F82D805752AF /* NftDoubleCell.swift in Sources */, 11B35FF1D956D812F815FA86 /* NftImageView.swift in Sources */, 11B35DF3813AEB74E254A05A /* NftAssetView.swift in Sources */, + D0A690342C05D01C00E59296 /* UIImageView.swift in Sources */, ABC9A13DB3ADB580D59F66E4 /* SendEip1155ViewModel.swift in Sources */, ABC9A4CD35CF43C88EC13909 /* SendEip1155Service.swift in Sources */, ABC9ACA04EBC7AF903A01FE3 /* SendEip1155ViewController.swift in Sources */, @@ -11913,6 +11751,7 @@ 11B352841C5901ABCC0096C2 /* BlockchainSettingsViewModel.swift in Sources */, 11B3540CF7A5AA26C196D796 /* CreateAccountSimpleViewController.swift in Sources */, 11B357C5FC1B7FDE86244DA5 /* SingleSelectorViewController.swift in Sources */, + D3DB51A82BD787490091BBDB /* MarketCoinsView.swift in Sources */, 11B35BE5E6974F580927CAFA /* RestoreService.swift in Sources */, 11B35D4B4DE7C4620C34AA11 /* AddEvmSyncSourceModule.swift in Sources */, 11B35F72D67DB96FA83C9004 /* AddEvmSyncSourceViewController.swift in Sources */, @@ -11927,6 +11766,7 @@ 11B35B7D8E3DA75CFD13E1FF /* ReservoirNftProvider.swift in Sources */, 11B3519760CB3D8D8C97F689 /* NftContractMetadata.swift in Sources */, 11B352C452D8E9C00FD26E8A /* NftActivityHeaderView.swift in Sources */, + D3384D102BFDCBDE00515664 /* MarketEtfView.swift in Sources */, 11B3589541E87A032D9D2D50 /* BottomSingleSelectorViewController.swift in Sources */, 11B359752E118A95F4705B95 /* TokenProtocol.swift in Sources */, 11B35DDD77B56489D1EB72C5 /* Token.swift in Sources */, @@ -11943,10 +11783,12 @@ 11B35CA6259EDA3708695416 /* FaqUrlHelper.swift in Sources */, 11B3534B12C5E7596E4953F0 /* RestoreSettingsModule.swift in Sources */, 2FA5DF494C667E041B22C804 /* EvmSendSettingsModule.swift in Sources */, + D03F74822BF76D0A004FBCFA /* GasPriceData.swift in Sources */, D05E96A92A28657F002CCD71 /* TronAccountManager.swift in Sources */, 2FA5DBD32AA258A75A2FDD95 /* EvmSendSettingsService.swift in Sources */, 2FA5D7C86A2A55B906953B76 /* EvmSendSettingsViewController.swift in Sources */, 2FA5DE6911CB4127CAB11F35 /* EvmSendSettingsViewModel.swift in Sources */, + D3833ADA2BEE1A8300ACECFB /* MarketWatchlistViewModel.swift in Sources */, 2FA5DF3D135C616A61B37292 /* StepperAmountInputView.swift in Sources */, 2FA5D9A3C943976E38293621 /* StepperAmountInputCell.swift in Sources */, 2FA5D6CF72BEC8CF1AEE3F9D /* EvmCommonGasDataService.swift in Sources */, @@ -11954,12 +11796,14 @@ 2FA5D98FF7412EBBD2FFFBC4 /* EvmFeeService.swift in Sources */, 2FA5D01A5570C6DE5D07E2C4 /* EvmFeeViewItemFactory.swift in Sources */, 2FA5DE1153742388007774D3 /* EvmRollupGasDataService.swift in Sources */, + 6B5F5E112C0C660900E03EB2 /* MarketPlatformViewModel.swift in Sources */, 2FA5DE2D347DBC660830CB41 /* NonceDataSource.swift in Sources */, 2FA5D1F6979A345597788DE9 /* NonceService.swift in Sources */, 2FA5D62FF4A9D8F54848D2C0 /* NonceViewModel.swift in Sources */, 2FA5DDC853CB680F672C1B50 /* LegacyEvmFeeViewModel.swift in Sources */, 11B3504619330D3DB0E0ECD1 /* PublicKeysModule.swift in Sources */, 11B35E70BF95197591A052EF /* PublicKeysService.swift in Sources */, + D0E5E8522BE260C8005080A4 /* BitcoinPreSendHandler.swift in Sources */, 11B35281808DE30B8D717B73 /* PublicKeysViewModel.swift in Sources */, 11B3550E1FD46B02F0154CBC /* PublicKeysViewController.swift in Sources */, 6BAAF3472B9B245C00EFE5B2 /* ShimmerEffect.swift in Sources */, @@ -11993,11 +11837,13 @@ 11B35F3F123BFF155DA7F417 /* CoinAnalyticsHoldersCell.swift in Sources */, ABC9A06BE632BD33E5CA4106 /* Contact.swift in Sources */, ABC9A2AA80535822D8731DA4 /* ContactBookViewController.swift in Sources */, + D3384D132BFDCBE700515664 /* MarketEtfViewModel.swift in Sources */, ABC9ADD2EA3745F828763EB4 /* ContactBookService.swift in Sources */, ABC9AD6C3EE6EDD0FB3D623A /* ContactBookViewModel.swift in Sources */, ABC9A05D9F96BE464CFC90CC /* ContactBookModule.swift in Sources */, ABC9A30629619D5BD6CEB952 /* ContactBookContactViewController.swift in Sources */, ABC9A20A25C4C683A73CB994 /* ContactBookContactViewModel.swift in Sources */, + 6BB14F6B2BF49E7100E879B2 /* WalletButtonHiddenManager.swift in Sources */, ABC9A2BE94B97921C3017C3F /* ContactBookContactService.swift in Sources */, ABC9A307AAD83C3ED0D591C7 /* ContactBookContactModule.swift in Sources */, ABC9AF81A6F30F0041FE1FAC /* ContactBookAddressViewController.swift in Sources */, @@ -12010,20 +11856,10 @@ ABC9AC79ACCB69BF97A01B53 /* ContactBookSettingsViewModel.swift in Sources */, ABC9A133A6BF0FC9A87FA14A /* ContactBookSettingsViewController.swift in Sources */, ABC9A359DB8C1A89269236CC /* ContactBookSettingsModule.swift in Sources */, + D3B73E2D2BDF6B6D0067429D /* MultiSwapSendHandler.swift in Sources */, ABC9ABE2B6B19113D7C5EDA3 /* ContactBookHelper.swift in Sources */, - 11B35D96A814579F37BAD3D0 /* CoinRankService.swift in Sources */, - 11B359C2651DA1F00A3C613C /* CoinRankViewModel.swift in Sources */, - 11B35F9CC94DB2BC7B43BB59 /* CoinRankViewController.swift in Sources */, - 11B352D8DDF054073BC79FC2 /* CoinRankModule.swift in Sources */, - 11B35BCF9FFC93255EFB2774 /* CoinRankHeaderView.swift in Sources */, ABC9A20F6F7D5EA2A1A55A9E /* ContactLabelService.swift in Sources */, ABC9AC84790F5BEAA514C731 /* TransactionsContactLabelService.swift in Sources */, - 11B35D2B9A488D3382D8693D /* MarketHeaderCell.swift in Sources */, - 11B354CBCCB0FFD2FDBEE757 /* MarketFilteredListService.swift in Sources */, - 11B35D90644F7735556DB3D5 /* TopPlatformViewModel.swift in Sources */, - 11B354CC5E68F04E22D633D9 /* TopPlatformHeaderCell.swift in Sources */, - 11B35D1F6602B517D19D5C76 /* TopPlatformViewController.swift in Sources */, - 11B353E15F4A208D393C7262 /* MarketCategoryViewController.swift in Sources */, 11B3511A2036EBD9611B3434 /* Misc.swift in Sources */, ABC9A074995C051E714FAFAB /* BackupContact.swift in Sources */, ABC9AEF0A2ECD0E627AF065B /* ECashAdapter.swift in Sources */, @@ -12034,6 +11870,8 @@ 11B355734D16C412220BBEBD /* NftKit.swift in Sources */, 11B35146CA9BE897C858AB73 /* BinanceChainKit.swift in Sources */, 11B352309B81355B88BF6B66 /* OneInchKit.swift in Sources */, + D3833AD72BEE1A7900ACECFB /* MarketWatchlistView.swift in Sources */, + D3384D1A2BFF0CAF00515664 /* MarketMarketCapView.swift in Sources */, ABC9A7E1F93B0A85976C826D /* UniswapV3Provider.swift in Sources */, ABC9AC900545DC0DD2201DEE /* UniswapV3TradeService.swift in Sources */, ABC9ACCD1ED14FA216AF1E65 /* UniswapV3Service.swift in Sources */, @@ -12057,6 +11895,7 @@ ABC9AB6EB596E2F8B15D00E4 /* CipherParams.swift in Sources */, ABC9AF9C828BEBB740468204 /* WalletBackup.swift in Sources */, ABC9A7EACB2FA65355C2BA4E /* AppBackupProvider.swift in Sources */, + D02447D92C09FA5200A04BBC /* CoinTreasuriesView.swift in Sources */, ABC9AF5B0B1D5FE002288AE1 /* FileStorage.swift in Sources */, ABC9AF371FBB4BEA654A78B6 /* MetadataMonitor.swift in Sources */, ABC9AA27A42D7E2A72B4A932 /* RestoreTypeModule.swift in Sources */, @@ -12077,9 +11916,11 @@ 11B352346D3565C7D6395D21 /* PasteInputView.swift in Sources */, 11B3555865BB7F4661A80DE1 /* PasteInputCell.swift in Sources */, 11B358622B30F9ED5B734A94 /* WalletHeaderCell.swift in Sources */, + D311DA252BD23C890013DB8F /* ScrollableTabHeaderView.swift in Sources */, 11B3511098C99D7B7D5A492A /* CexWithdrawNetwork.swift in Sources */, 11B350C214D423CE2DCD6853 /* CexAssetRecord.swift in Sources */, 11B355B56270FCD8A17A49B5 /* CexWithdrawNetworkRaw.swift in Sources */, + D3384D1D2BFF0CB800515664 /* MarketMarketCapViewModel.swift in Sources */, 11B35841E0B353B727DCD9CF /* CexAssetManager.swift in Sources */, 6B2907212AF0CB8A006157D6 /* WalletConnectAppShowModule.swift in Sources */, 11B35C95EA77972246D5F3BD /* CexAssetRecordStorage.swift in Sources */, @@ -12106,6 +11947,7 @@ ABC9A74F192AB94CFD1D1649 /* IndicatorAdviceCell.swift in Sources */, ABC9AFE47A405844612EB01A /* IndicatorAdviceView.swift in Sources */, ABC9A0866C672D2D560DA23C /* CoinDetailAdviceViewController.swift in Sources */, + D3833AF52BF20B8D00ACECFB /* MarketPairsViewModel.swift in Sources */, ABC9AD3001AAA0570B503876 /* ManageBarButtonView.swift in Sources */, ABC9A54FFFFBFC3C7B23F0B8 /* BottomGradientHolder.swift in Sources */, 11B354AC828E53D98222EE71 /* SubscriptionInfoViewController.swift in Sources */, @@ -12118,6 +11960,7 @@ 11B356D67F706464900DBD25 /* CexCoinService.swift in Sources */, 11B350667CC409D86A3C4C6A /* CexAmountInputViewModel.swift in Sources */, 11B35A51BDF5BA34DA7B227E /* CexDepositNetworkSelectModule.swift in Sources */, + D3DB51AB2BD787A00091BBDB /* MarketCoinsViewModel.swift in Sources */, 11B35A07ED63F869C0203244 /* CexDepositNetworkSelectViewController.swift in Sources */, 11B358DC90F3372DB98BD4A5 /* CexDepositNetworkSelectViewModel.swift in Sources */, 11B3544A8CB646621B14DBAA /* CexDepositNetworkSelectService.swift in Sources */, @@ -12132,6 +11975,7 @@ 11B3517B16E90C016A588C7C /* CexWithdrawConfirmViewController.swift in Sources */, 11B3590189E28D408E207E19 /* CexDepositService.swift in Sources */, 11B35C2EB8D6EC593915640F /* CexDepositModule.swift in Sources */, + D311DA1F2BD115240013DB8F /* MarketGlobalViewModel.swift in Sources */, 11B3595BD960FE1B998ADF6F /* BinanceCexProvider.swift in Sources */, 11B353C149EC597A051E8310 /* BinanceWithdrawHandler.swift in Sources */, 11B35E0B20AA8E1BCE449058 /* RestoreCexViewController.swift in Sources */, @@ -12175,6 +12019,8 @@ ABC9AFAB3BB4A1D2BFD4283B /* DataSourceChain.swift in Sources */, ABC9A30A4C740609D0898809 /* WalletTokenBalanceDataSource.swift in Sources */, ABC9A427B3166B8A0630EC8A /* WalletTokenBalanceViewModel.swift in Sources */, + D3384D162BFDEF6800515664 /* Etf.swift in Sources */, + D31369892BEA188D00BA6B5B /* ZcashPreSendHandler.swift in Sources */, ABC9A2692A01293B1229EF50 /* WalletTokenBalanceService.swift in Sources */, ABC9AC8ACB374C9B96F05B3C /* WalletTokenBalanceModule.swift in Sources */, ABC9A462CFF86970878025CE /* WalletTokenBalanceViewItemFactory.swift in Sources */, @@ -12186,16 +12032,19 @@ ABC9A2CA505DB49DE0FB28DD /* WalletTokenBalanceCustomAmountCell.swift in Sources */, 11B35A80AB419A754EF9955A /* ListSection.swift in Sources */, 11B35F91E53BA1F835DD4B4F /* HorizontalDivider.swift in Sources */, + D3F9B0312BE3B39D009FFA95 /* EvmDecoration.swift in Sources */, 11B35EECB57465598F305C5B /* ListRow.swift in Sources */, 11B3538D0F8C037E40623A81 /* Text.swift in Sources */, 11B35503B3E84FEFCDF1AFED /* ThemeView.swift in Sources */, ABC9A005F31836B4EBAB1C97 /* DonateDescriptionCell.swift in Sources */, ABC9AD85EF6798DF4302FD0E /* WalletTokenListDataSource.swift in Sources */, + D3D13A5F2C0D9DCB002484BC /* MarketAdvancedSearchViewModel.swift in Sources */, ABC9AEB71EB75575A97408BC /* DonateDescriptionDataSource.swift in Sources */, ABC9AC692F695C5F81E0453D /* DonateAddressViewController.swift in Sources */, ABC9A994D6AC5771ED49EFD1 /* DonateAddressViewModel.swift in Sources */, ABC9A140CD70E91A1F4A3A5B /* DonateAddressModule.swift in Sources */, 11B353FD73E7731A9BC50C4E /* HighlightedTextView.swift in Sources */, + D31369862BEA187E00BA6B5B /* ZcashSendHandler.swift in Sources */, 11B3574287AAA5FC16E3E3DA /* NavigationRow.swift in Sources */, 11B35631BD5C6570C9359BEC /* RowButtonStyle.swift in Sources */, 11B3541ED37746BAFF1832BA /* Image.swift in Sources */, @@ -12230,6 +12079,7 @@ 11B35C9570D3C283E9C943D5 /* CreatePasscodeViewModel.swift in Sources */, 11B356A5B50D4E6EF2282398 /* EditDuressPasscodeViewModel.swift in Sources */, 11B35C5F856FB531028F8C0A /* CreateDuressPasscodeViewModel.swift in Sources */, + D3833B052BF4AFB800ACECFB /* MarqueeView.swift in Sources */, 11B35F655F8C5ECDB870712D /* UnlockView.swift in Sources */, 11B35251E1B11235D00E6565 /* UnlockModule.swift in Sources */, 11B35F1949F7203F34347550 /* ModuleUnlockView.swift in Sources */, @@ -12259,6 +12109,7 @@ ABC9A2035980B70E1C0790A8 /* CheckboxStyle.swift in Sources */, ABC9A99A45187C36D48840F8 /* BackupAppModule.swift in Sources */, ABC9AE51262C09EABF5CCEEE /* InputTextView.swift in Sources */, + 6BB14F802C06F19300E879B2 /* DefiCoin.swift in Sources */, ABC9A542CA987F09C93F04A9 /* InputTextRow.swift in Sources */, ABC9A7C2087C3A641C3F9AD4 /* Shake.swift in Sources */, ABC9A12A4D114A2E4F4C711A /* ActivityView.swift in Sources */, @@ -12272,6 +12123,7 @@ ABC9A04FAB83D7A8D251DA90 /* BackupPasswordView.swift in Sources */, D0F9F5172B99857700C3190A /* FeeSettings.swift in Sources */, ABC9A0CE0155F89F12350DFC /* BackupListView.swift in Sources */, + D3384D212BFF0CCA00515664 /* MarketVolumeView.swift in Sources */, ABC9A4465982823773CE1B50 /* BackupDisclaimerView.swift in Sources */, ABC9AA18996E714C955E7E13 /* RestoreAppViewModel.swift in Sources */, ABC9AF04946C86FA6DBD4225 /* RestorePassphraseViewModel.swift in Sources */, @@ -12287,6 +12139,7 @@ 11B35FB362526C723329C9ED /* ChartView.swift in Sources */, 11B353E4793549B6A4F23997 /* CoinOverviewView.swift in Sources */, 11B35FDF03CD52FEC5B1745A /* CoinOverviewViewModelNew.swift in Sources */, + D3833AE12BEE3FE800ACECFB /* MarketPlatformsViewModel.swift in Sources */, ABC9AC170807B409634706E6 /* BackupManagerModule.swift in Sources */, ABC9A37B5FAB65E7AB66547E /* BackupManagerViewController.swift in Sources */, 11B35916211F5D5EA0DBD207 /* CoinPageViewModelNew.swift in Sources */, @@ -12297,7 +12150,9 @@ 11B35D5CEB75CD7626D6A612 /* SharedLocalStorage.swift in Sources */, D350DDB22AE27E3B00CF1989 /* AppWidget.intentdefinition in Sources */, 11B35FD593B38EEEE5F18010 /* AppWidgetConstants.swift in Sources */, + D36E50842BF75B6900C361BD /* WatchlistTimePeriod.swift in Sources */, 11B35909DEBFA098976E1D87 /* DateFormatterCache.swift in Sources */, + D3F9B03A2BE3BB36009FFA95 /* WalletConnectSendViewModel.swift in Sources */, 11B350938ACDA1EEF888E846 /* LanguageHourFormatter.swift in Sources */, 6B2907252AF0CB8A006157D6 /* WalletConnectAppShowView.swift in Sources */, 11B357D1BFE20978857A2FDC /* LanguageSettingsModule.swift in Sources */, @@ -12307,6 +12162,7 @@ 11B3520B4589E8C57FD4774F /* ScanQrBlurView.swift in Sources */, 11B3542290A3C8B680AA1943 /* ScanQrAlertView.swift in Sources */, 11B35082449BDCDAF7CECC3E /* KeychainManager.swift in Sources */, + D0E5E84F2BE22172005080A4 /* BitcoinSendHandler.swift in Sources */, 11B35CAD54059FA55DF81972 /* PasscodeLockManager.swift in Sources */, 11B35051B85C51018A1C7A3A /* KeychainStorage.swift in Sources */, 11B35A81895CBF7E86C0C437 /* UserDefaultsStorage.swift in Sources */, @@ -12340,6 +12196,7 @@ 11B35B2B8B3093530144F877 /* WCSignEthereumTransactionRequestModule.swift in Sources */, 11B35E60B65A327311D8CAB1 /* WCSignEthereumTransactionRequestViewModel.swift in Sources */, 11B354B551E301C652D67319 /* WCSendEthereumTransactionRequestViewController.swift in Sources */, + D311DA222BD23C230013DB8F /* MarketAdvancedSearchView.swift in Sources */, 11B35B99BA314135CB139D02 /* WCSignEthereumTransactionRequestViewController.swift in Sources */, 11B3530E911B6F8B121BF174 /* CoinMarketsViewModel.swift in Sources */, 11B359330CB6A60E5960CEC2 /* CoinMarketsView.swift in Sources */, @@ -12348,12 +12205,14 @@ 11B35DCCBE18D2F1F16C01C5 /* PlaceholderViewNew.swift in Sources */, D3DD672B2BC3BF5200EC7F78 /* OneInchMultiSwapConfirmationQuote.swift in Sources */, 11B35ED9F3C0EA3CCC4C0FF4 /* SyncErrorView.swift in Sources */, + D3384D4E2C07020300515664 /* PriceChangeMode.swift in Sources */, ABC9AE362775FBF83C15C231 /* Extensions.swift in Sources */, ABC9A1109D59CD5F71FBC153 /* BaseAnimation.swift in Sources */, ABC9A9A71EA9758D096AAA68 /* AlphaDismissAnimation.swift in Sources */, ABC9AE59D5128137249B4C18 /* AlphaPresentAnimation.swift in Sources */, ABC9AB83192CC09204F5D128 /* MovingDismissAnimation.swift in Sources */, ABC9A2B772D1C72F1AFA18CD /* MovingPresentAnimation.swift in Sources */, + D3833AEA2BEE4CAA00ACECFB /* TopPlatform.swift in Sources */, ABC9AE56D91FF5CF88605DC2 /* TransitionDriver.swift in Sources */, ABC9A8C5CBD21B029D07652D /* ActionSheetAnimator.swift in Sources */, ABC9AF2C595E6CC96FB8AF4A /* DismissPanGestureRecognizer.swift in Sources */, @@ -12367,6 +12226,7 @@ ABC9AE558CE5912B5C15B6EB /* ActionSheetControllerNew.swift in Sources */, 11B3537EE13B3EFB2E979821 /* BadgeViewNew.swift in Sources */, 11B35955EE2F47EFAFCBCE9F /* BaseCurrencySettingsViewModel.swift in Sources */, + D3384D092BFCB43800515664 /* MarketWatchlistSignalsView.swift in Sources */, 11B3565D4E4EAD663143ED9B /* BaseCurrencySettingsView.swift in Sources */, 11B353BAEF83867422611E7B /* TransactionFilterModule.swift in Sources */, 11B35B09AADB1FBF7DDE765C /* TransactionFilterView.swift in Sources */, @@ -12378,14 +12238,12 @@ 11B35D88633A14FD13E91702 /* TonAdapter.swift in Sources */, 11B354C1218C0776499FAA5E /* Kmm.swift in Sources */, 11B3589C124F6BBDDBB144F4 /* TonOutgoingTransactionRecord.swift in Sources */, + D389BC4F2C0DEF1800724504 /* MarketAdvancedSearchResultsView.swift in Sources */, 11B35EB4CAA93773DF09B479 /* TonTransactionRecord.swift in Sources */, 11B35264EC1BABABCDDD1F67 /* TonIncomingTransactionRecord.swift in Sources */, 11B359D912BC5502A9FA0E57 /* InputRowModifier.swift in Sources */, - 11B35ED2B4180D1FB4073100 /* SendAmountViewModel.swift in Sources */, - 11B35B03C6F88AFE3E45C13D /* SendAmountView.swift in Sources */, - 11B3597F2512CDE8172D7C81 /* SendModuleNew.swift in Sources */, - 11B35283F8170DB664A77C2B /* SendView.swift in Sources */, - 11B3598EE09251933E7FFD5D /* SendViewModelNew.swift in Sources */, + 11B35283F8170DB664A77C2B /* PreSendView.swift in Sources */, + 11B3598EE09251933E7FFD5D /* PreSendViewModel.swift in Sources */, 11B3582C5AB8B2BF15962AE7 /* SendTonViewController.swift in Sources */, 11B358AFE8EC87CA1DEB4C22 /* SendTonService.swift in Sources */, 11B3564E05702B4253453F23 /* SendTonFactory.swift in Sources */, @@ -12394,18 +12252,11 @@ ABC9AD530352E51084B1B4B7 /* PrimaryCircleButtonStyle.swift in Sources */, ABC9A639040A77968B5D86B8 /* InformedModifier.swift in Sources */, ABC9AB982879DE0BAE6701EC /* BorderedEmptyCell.swift in Sources */, - 11B358F04D8F15D43CB2BAA6 /* MarketCategoryView.swift in Sources */, + D389BC462C0DCF4100724504 /* Advice.swift in Sources */, 11B3580BC3B5D2CBC68854D2 /* UIWindow.swift in Sources */, 11B3505B911DD28AC464A694 /* BackupManagerViewModel.swift in Sources */, ABC9AFAB60E13F58DC2F3D5D /* SecondaryActiveButtonStyle.swift in Sources */, ABC9A70FE468AB48CFF63201 /* UsedAddressesView.swift in Sources */, - 11B35611F21D266215BD82A5 /* MarketOverviewTopPairsDataSource.swift in Sources */, - 11B35FEB268E7C6B085B56C9 /* MarketOverviewTopPairsViewModel.swift in Sources */, - 11B358F511E01944DA31FF7D /* MarketListMarketPairDecorator.swift in Sources */, - 11B35B02E53E6D7DDC12FC5D /* MarketOverviewTopPairsService.swift in Sources */, - 11B35577071CBB59D7692DE4 /* MarketTopPairsViewController.swift in Sources */, - 11B35C72EF5FB79182EAB119 /* MarketTopPairsViewModel.swift in Sources */, - 11B35CA725B9BAD70E40197F /* MarketTopPairsModule.swift in Sources */, ABC9A22DF26B2A8C66B723BC /* UnspentOutputsViewModel.swift in Sources */, D06B302C2B6A120E0012A161 /* LegacyFeeSettingsViewModel.swift in Sources */, ABC9A4AE1AE901132578CC28 /* UnspentOutputsCell.swift in Sources */, @@ -12414,7 +12265,6 @@ ABC9AA70F3B1EFA37CBB7011 /* BaseFiatService.swift in Sources */, ABC9A291506C14262275CE0E /* AmountOutputSelectorViewModel.swift in Sources */, ABC9AC10C743C53832289B8A /* AddressOutputSelectorViewModel.swift in Sources */, - ABC9A2DA629CF38FD7B893EC /* MarketWatchlistDecorator.swift in Sources */, ABC9A8310AFE013E16F3DBAC /* Eip1559FeeSettingsView.swift in Sources */, ABC9A1798D6E2E4C868DA366 /* Eip1559FeeSettingsViewModel.swift in Sources */, 11B3594BEB2E05413B0DB30F /* MultiSwapView.swift in Sources */, @@ -12422,8 +12272,8 @@ 11B358EC68642C5A57ACB803 /* IMultiSwapProvider.swift in Sources */, 11B35503841E40909EF690B3 /* MultiSwapCircularProgressView.swift in Sources */, 11B356A34BC00A9BEB4C13C2 /* MultiSwapQuotesView.swift in Sources */, + D0DEFF062BD1253C004C9DF0 /* BitcoinFeeSettingsView.swift in Sources */, 11B355D89EADD907D4FC3273 /* MultiSwapButtonState.swift in Sources */, - 11B35CA3524A5FEA07135535 /* MultiSwapConfirmationView.swift in Sources */, 11B356075F51B38338958A4A /* MultiSwapSettingStorage.swift in Sources */, D0740B1B2B87585000B085F9 /* ResendBitcoinService.swift in Sources */, 11B35F0FF89A5B636C2EFE48 /* UniswapV2MultiSwapProvider.swift in Sources */, @@ -12438,6 +12288,7 @@ ABC9A41BF96061D136A1D397 /* ShortcutButtonsView.swift in Sources */, ABC9AF86EF899AF417E1F4AE /* AddressViewNew.swift in Sources */, ABC9AF3418D0B0DE5FE4EC14 /* AddressViewModelNew.swift in Sources */, + D0DEFF0C2BD1257F004C9DF0 /* BitcoinFeeData.swift in Sources */, ABC9AB2CCF3AD358EA9F285C /* DebounceTextField.swift in Sources */, ABC9AC3E675F159901A28E6F /* BaseEvmMultiSwapProvider.swift in Sources */, ABC9A38568F5D28F0F48E44D /* RightCheckingView.swift in Sources */, @@ -12451,7 +12302,6 @@ ABC9AFFCAC47238A626A97C3 /* MultiSwapSlippageView.swift in Sources */, 11B35508E26446AC692EBAEF /* RecipientAndSlippageMultiSwapSettingsView.swift in Sources */, 11B35019C9DB5A6FDCE6BAE6 /* OneInchMultiSwapProvider.swift in Sources */, - 11B351D378E1EF2A72467B88 /* MultiSwapConfirmationViewModel.swift in Sources */, ABC9A66F2C6BAAA6CA00FAE6 /* MultiSwapApproveView.swift in Sources */, ABC9AB3C239A182E9B93AFDA /* MultiSwapApproveViewModel.swift in Sources */, ABC9A62E858360A9964084D2 /* Binding.swift in Sources */, @@ -12461,20 +12311,20 @@ ABC9A6C99739F0AC308F0574 /* TransactionContactSelectViewModel.swift in Sources */, ABC9A9873160D2EAFD4D6D2A /* TransactionBlockchainSelectViewModel.swift in Sources */, ABC9AD8175A3223E207E390E /* TransactionTokenSelectViewModel.swift in Sources */, - 11B3541A3A7BDE85240F71A0 /* SendConfirmationNewView.swift in Sources */, - 11B35563DA099AE8ABE34F8D /* SendConfirmationNewViewModel.swift in Sources */, - 11B3550846F2DD60D3778FE5 /* SendEvmHandler.swift in Sources */, + 11B3541A3A7BDE85240F71A0 /* SendView.swift in Sources */, + 11B35563DA099AE8ABE34F8D /* SendViewModel.swift in Sources */, + 11B3550846F2DD60D3778FE5 /* EvmSendHandler.swift in Sources */, ABC9A89C6D89CC91E2C96893 /* SliderGradientView.swift in Sources */, ABC9A0A78E18F832D2549CB2 /* TechnicalIndicatorCell.swift in Sources */, 11B350B33F891A2720420697 /* ISendHandler.swift in Sources */, 11B35A3B506EDD1EFB7D913E /* BaseSendEvmData.swift in Sources */, 11B3546D0D2F891FC9AE372C /* IMultiSwapQuote.swift in Sources */, 11B3584A053BC5DAE6E434F8 /* IMultiSwapConfirmationQuote.swift in Sources */, - 11B3557AF8D64E9897965526 /* ISendConfirmationData.swift in Sources */, - 11B3594FF624369050E43F8F /* SendDataNew.swift in Sources */, + 11B3557AF8D64E9897965526 /* ISendData.swift in Sources */, + 11B3594FF624369050E43F8F /* SendData.swift in Sources */, 11B35504619FDE878D865781 /* MultiSwapMainField.swift in Sources */, 11B359528BEC580E0BE7E7D3 /* ValueLevel.swift in Sources */, - 11B35DB4C3EAB5B4B9ABF904 /* SendConfirmField.swift in Sources */, + 11B35DB4C3EAB5B4B9ABF904 /* SendField.swift in Sources */, 11B35A3C2C4CC7A834D56517 /* MultiSwapPreSwapStep.swift in Sources */, 11B358D01760F90518DA612F /* SendHandlerFactory.swift in Sources */, 11B35B177527D30099B67C91 /* FeeData.swift in Sources */, @@ -12483,7 +12333,9 @@ 11B35BEDD1017EA9EDA7F2FB /* TransactionServiceFactory.swift in Sources */, 11B3566FB55E128CD6F22DAE /* TransactionSettings.swift in Sources */, 11B352B9206C86492CDDC9A7 /* EvmFeeData.swift in Sources */, + D3833B022BF38A8000ACECFB /* MarketTabViewModel.swift in Sources */, 11B35A920F2DB5784F178BDA /* EvmFeeEstimator.swift in Sources */, + D34A29B62BFB4E3200F63036 /* WatchlistSortBy.swift in Sources */, 11B3545369350E4253688D91 /* MultiSwapRevokeView.swift in Sources */, 11B35CBF59F85C3DAB5FE751 /* MultiSwapRevokeViewModel.swift in Sources */, 11B355FCAF8DC88550CE2DB3 /* StatManager.swift in Sources */, @@ -12509,6 +12361,7 @@ files = ( D3948EF72ADA846400FAE566 /* AppWidgetBundle.swift in Sources */, 11B350F24818BABB6DB6512A /* SingleCoinPriceView.swift in Sources */, + D092C5892C12DC8E0060D915 /* PriceChangeModeManager.swift in Sources */, 11B3543EF331DA9E5E33822A /* SingleCoinPriceEntry.swift in Sources */, 11B3525BA3799B70B25CF2FC /* SingleCoinPriceProvider.swift in Sources */, 11B356509E6083B971F37E0F /* Currency.swift in Sources */, @@ -12517,19 +12370,24 @@ 11B3513A8C5CFB4A4495D935 /* HorizontalDivider.swift in Sources */, 11B350C1B04946C9AA8B3430 /* ListSection.swift in Sources */, 11B35B5EED35DD5F8F8B19A8 /* ThemeListStyle.swift in Sources */, + D36E50942BF7852D00C361BD /* CoinListView.swift in Sources */, + D36E50862BF75B6C00C361BD /* WatchlistTimePeriod.swift in Sources */, 11B357229EFFA602F38D6C2C /* CoinPriceListEntry.swift in Sources */, + D36E508B2BF76FA700C361BD /* WatchlistEntry.swift in Sources */, 11B35E88C40DC151A3BEC0B1 /* CoinPriceListProvider.swift in Sources */, - 11B35A6F4E6931973277940A /* CoinPriceListView.swift in Sources */, 11B35CB0098E9628FE81AC39 /* TopCoinsWidget.swift in Sources */, 11B3538FA5A4953A7C9AC9E6 /* SingleCoinPriceWidget.swift in Sources */, 11B358D519ACFE88A7823C7E /* ApiProvider.swift in Sources */, 11B358587D9C3A1F10EC15A6 /* PriceChangeType.swift in Sources */, + D36E50822BF7534900C361BD /* WatchlistManager.swift in Sources */, 11B35530A9FC0972D8716C31 /* ValueFormatter.swift in Sources */, D350DDB62AE27E3B00CF1989 /* AppWidget.intentdefinition in Sources */, 11B3550548CB49D32EAC1DF5 /* WatchlistWidget.swift in Sources */, + D34A29B92BFB4E3200F63036 /* WatchlistSortBy.swift in Sources */, + D36E508E2BF76FB400C361BD /* WatchlistProvider.swift in Sources */, 11B35287E46AFFBC47162F67 /* Extensions.swift in Sources */, - 11B35DA145308F888592A7CF /* CoinPriceListMode.swift in Sources */, 11B3541E8CB5F0F743E9CDF3 /* AppWidgetConstants.swift in Sources */, + D092C58E2C12DE810060D915 /* PriceChangeMode.swift in Sources */, 11B354FA80BD8C5512B5ACFE /* WidgetConfig.swift in Sources */, 11B35B31D808ED62EFB3D38B /* BadgeViewNew.swift in Sources */, ); @@ -12552,6 +12410,7 @@ files = ( D3BA257F2ADFAD7C002B13EA /* AppWidgetBundle.swift in Sources */, D3BA25842ADFAD7C002B13EA /* SingleCoinPriceView.swift in Sources */, + D092C5882C12DC8D0060D915 /* PriceChangeModeManager.swift in Sources */, D3BA25852ADFAD7C002B13EA /* SingleCoinPriceEntry.swift in Sources */, D3BA25862ADFAD7C002B13EA /* SingleCoinPriceProvider.swift in Sources */, 11B35202BE66E6E764026D63 /* Currency.swift in Sources */, @@ -12560,19 +12419,24 @@ 11B357AD2632BDF26DCB4BFC /* HorizontalDivider.swift in Sources */, 11B35F728E5BE60FD7C87FA1 /* ListSection.swift in Sources */, 11B35A15391F470849534264 /* ThemeListStyle.swift in Sources */, + D36E50932BF7852D00C361BD /* CoinListView.swift in Sources */, + D36E50872BF75B6D00C361BD /* WatchlistTimePeriod.swift in Sources */, 11B35F6BC7EC8A90FDACD191 /* CoinPriceListEntry.swift in Sources */, + D36E508A2BF76FA700C361BD /* WatchlistEntry.swift in Sources */, 11B35529AB46C98BC35C72E4 /* CoinPriceListProvider.swift in Sources */, - 11B3530307FFDC0AF9D3A8F2 /* CoinPriceListView.swift in Sources */, 11B352F16ADEABF640D2B9FD /* TopCoinsWidget.swift in Sources */, 11B354D628AADF3AFD9123E1 /* SingleCoinPriceWidget.swift in Sources */, 11B353FCD118CAB48511CF12 /* ApiProvider.swift in Sources */, 11B35F0D313E455BCF24C42B /* PriceChangeType.swift in Sources */, + D36E50812BF7534700C361BD /* WatchlistManager.swift in Sources */, 11B3518F3962FEA97AE6C7CD /* ValueFormatter.swift in Sources */, D350DDB32AE27E3B00CF1989 /* AppWidget.intentdefinition in Sources */, 11B359BF322A7B912C778348 /* WatchlistWidget.swift in Sources */, + D34A29B72BFB4E3200F63036 /* WatchlistSortBy.swift in Sources */, + D36E508D2BF76FB400C361BD /* WatchlistProvider.swift in Sources */, 11B35700253CCD66C4CCE354 /* Extensions.swift in Sources */, - 11B359F926F72DF79E9245E3 /* CoinPriceListMode.swift in Sources */, 11B35B2E335C24608DE32B0A /* AppWidgetConstants.swift in Sources */, + D092C58D2C12DE800060D915 /* PriceChangeMode.swift in Sources */, 11B35BE346FCE9B2BE4096A8 /* WidgetConfig.swift in Sources */, 11B35C8BF55C38F198C3DAE6 /* BadgeViewNew.swift in Sources */, ); @@ -12643,7 +12507,7 @@ D350DDC72AE27E4900CF1989 /* es */, D350DDC92AE27E4A00CF1989 /* tr */, D350DDCA2AE2818A00CF1989 /* Base */, - D350DDCC2AE2819B00CF1989 /* en */, + D34A29B42BFB4AE200F63036 /* en */, ); name = AppWidget.intentdefinition; sourceTree = ""; @@ -12732,7 +12596,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.38.2; + MARKETING_VERSION = 0.39; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; OfficeMode = true; @@ -12804,7 +12668,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.38.2; + MARKETING_VERSION = 0.39; MTL_ENABLE_DEBUG_INFO = NO; OfficeMode = false; SDKROOT = iphoneos; @@ -13247,7 +13111,7 @@ repositoryURL = "https://github.com/horizontalsystems/BitcoinCore.Swift"; requirement = { kind = exactVersion; - version = 2.6.4; + version = 3.0.1; }; }; 6B55461F2A6E73190054B524 /* XCRemoteSwiftPackageReference "UIExtensions" */ = { @@ -13271,7 +13135,7 @@ repositoryURL = "https://github.com/horizontalsystems/ECashKit.Swift.git"; requirement = { kind = exactVersion; - version = 2.2.2; + version = 3.0.1; }; }; 6BDA29AE29D6F934003847ED /* XCRemoteSwiftPackageReference "HsToolKit.Swift" */ = { @@ -13303,7 +13167,7 @@ repositoryURL = "https://github.com/horizontalsystems/Hodler.Swift"; requirement = { kind = exactVersion; - version = 2.0.1; + version = 2.0.2; }; }; D0DA740B272A6EFC0072BE86 /* XCRemoteSwiftPackageReference "UnicodeURL" */ = { @@ -13367,7 +13231,7 @@ repositoryURL = "https://github.com/horizontalsystems/OneInchKit.Swift"; requirement = { kind = exactVersion; - version = 3.0.1; + version = 3.0.3; }; }; D3604E6428F02D9A0066C366 /* XCRemoteSwiftPackageReference "BitcoinKit" */ = { @@ -13375,7 +13239,7 @@ repositoryURL = "https://github.com/horizontalsystems/BitcoinKit.Swift"; requirement = { kind = exactVersion; - version = 2.2.2; + version = 3.0.0; }; }; D3604E6728F02DF30066C366 /* XCRemoteSwiftPackageReference "BitcoinCashKit" */ = { @@ -13383,7 +13247,7 @@ repositoryURL = "https://github.com/horizontalsystems/BitcoinCashKit.Swift"; requirement = { kind = exactVersion; - version = 2.2.2; + version = 3.0.0; }; }; D3604E6A28F02E3F0066C366 /* XCRemoteSwiftPackageReference "LitecoinKit" */ = { @@ -13391,7 +13255,7 @@ repositoryURL = "https://github.com/horizontalsystems/LitecoinKit.Swift"; requirement = { kind = exactVersion; - version = 2.3.3; + version = 3.0.0; }; }; D3604E6E28F03AC70066C366 /* XCRemoteSwiftPackageReference "MarketKit" */ = { @@ -13399,7 +13263,7 @@ repositoryURL = "https://github.com/horizontalsystems/MarketKit.Swift"; requirement = { kind = exactVersion; - version = 3.0.1; + version = 3.0.11; }; }; D3604E7D28F03C1D0066C366 /* XCRemoteSwiftPackageReference "Chart" */ = { @@ -13407,7 +13271,7 @@ repositoryURL = "https://github.com/horizontalsystems/Chart.Swift"; requirement = { kind = exactVersion; - version = 3.0.0; + version = 3.0.1; }; }; D3604E8028F03C6B0066C366 /* XCRemoteSwiftPackageReference "FeeRateKit" */ = { @@ -13423,7 +13287,7 @@ repositoryURL = "https://github.com/horizontalsystems/BinanceChainKit.Swift"; requirement = { kind = exactVersion; - version = 2.0.3; + version = 2.0.4; }; }; D3604E8628F03D9E0066C366 /* XCRemoteSwiftPackageReference "DashKit" */ = { @@ -13431,7 +13295,7 @@ repositoryURL = "https://github.com/horizontalsystems/DashKit.Swift"; requirement = { kind = exactVersion; - version = 2.2.5; + version = 3.0.0; }; }; D36E0C2828D084AB00B622B9 /* XCRemoteSwiftPackageReference "CollectionViewCenteredFlowLayout" */ = { @@ -13447,7 +13311,7 @@ repositoryURL = "https://github.com/zcash/ZcashLightClientKit"; requirement = { kind = exactVersion; - version = 2.1.3; + version = 2.1.8; }; }; D3993DAA28F42549008720FB /* XCRemoteSwiftPackageReference "WalletConnectSwiftV2" */ = { @@ -13455,7 +13319,7 @@ repositoryURL = "https://github.com/WalletConnect/WalletConnectSwiftV2"; requirement = { kind = exactVersion; - version = 1.6.10; + version = 1.18.5; }; }; D3993DC028F42992008720FB /* XCRemoteSwiftPackageReference "resolution-swift" */ = { @@ -13527,7 +13391,7 @@ repositoryURL = "https://github.com/horizontalsystems/Checkpoints"; requirement = { kind = exactVersion; - version = 1.0.17; + version = 1.0.18; }; }; D3C187CD290FCF2D00FE1900 /* XCRemoteSwiftPackageReference "ThemeKit" */ = { @@ -13543,7 +13407,7 @@ repositoryURL = "https://github.com/horizontalsystems/ComponentKit.Swift"; requirement = { kind = exactVersion; - version = 2.0.11; + version = 2.0.13; }; }; D3C187D3290FCF7D00FE1900 /* XCRemoteSwiftPackageReference "HUD" */ = { @@ -13593,6 +13457,16 @@ package = 6B55E3392AF26D6400616B60 /* XCRemoteSwiftPackageReference "Starscream" */; productName = Starscream; }; + 6BBCE4A22BDA419200ABBD55 /* Web3Wallet */ = { + isa = XCSwiftPackageProductDependency; + package = D3993DAA28F42549008720FB /* XCRemoteSwiftPackageReference "WalletConnectSwiftV2" */; + productName = Web3Wallet; + }; + 6BBCE4A42BDA419B00ABBD55 /* Web3Wallet */ = { + isa = XCSwiftPackageProductDependency; + package = D3993DAA28F42549008720FB /* XCRemoteSwiftPackageReference "WalletConnectSwiftV2" */; + productName = Web3Wallet; + }; 6BDA29AA29D6F37C003847ED /* ECashKit */ = { isa = XCSwiftPackageProductDependency; package = 6BDA29A929D6EA9B003847ED /* XCRemoteSwiftPackageReference "ECashKit.Swift" */; diff --git a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/check_2_24.imageset/check-2@2x.png b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/check_2_24.imageset/check-2@2x.png index 991b559865..0844165b52 100644 Binary files a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/check_2_24.imageset/check-2@2x.png and b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/check_2_24.imageset/check-2@2x.png differ diff --git a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/check_2_24.imageset/check-2@3x.png b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/check_2_24.imageset/check-2@3x.png index ca3f994bd7..c24f8f6f45 100644 Binary files a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/check_2_24.imageset/check-2@3x.png and b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/check_2_24.imageset/check-2@3x.png differ diff --git a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/circle_radiooff_24.imageset/radioff@2x.png b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/circle_radiooff_24.imageset/radioff@2x.png index 241715782d..8bbd646c2d 100644 Binary files a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/circle_radiooff_24.imageset/radioff@2x.png and b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/circle_radiooff_24.imageset/radioff@2x.png differ diff --git a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/circle_radiooff_24.imageset/radioff@3x.png b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/circle_radiooff_24.imageset/radioff@3x.png index 1c94d77259..3855633832 100644 Binary files a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/circle_radiooff_24.imageset/radioff@3x.png and b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/circle_radiooff_24.imageset/radioff@3x.png differ diff --git a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/circle_radioon_24.imageset/radion@2x.png b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/circle_radioon_24.imageset/radion@2x.png index 8ea0d62668..108883680e 100644 Binary files a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/circle_radioon_24.imageset/radion@2x.png and b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/circle_radioon_24.imageset/radion@2x.png differ diff --git a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/circle_radioon_24.imageset/radion@3x.png b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/circle_radioon_24.imageset/radion@3x.png index 21522d16a9..634bff80a4 100644 Binary files a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/circle_radioon_24.imageset/radion@3x.png and b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/circle_radioon_24.imageset/radion@3x.png differ diff --git a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/filled_reddit_24.imageset/Contents.json b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/filled_reddit_24.imageset/Contents.json deleted file mode 100644 index 1f694c9040..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/filled_reddit_24.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "reddit@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "reddit@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/filled_reddit_24.imageset/reddit@2x.png b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/filled_reddit_24.imageset/reddit@2x.png deleted file mode 100644 index f36d1debcb..0000000000 Binary files a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/filled_reddit_24.imageset/reddit@2x.png and /dev/null differ diff --git a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/filled_reddit_24.imageset/reddit@3x.png b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/filled_reddit_24.imageset/reddit@3x.png deleted file mode 100644 index 30a4114501..0000000000 Binary files a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/filled_reddit_24.imageset/reddit@3x.png and /dev/null differ diff --git a/UnstoppableWallet/UnstoppableWallet/Configuration/Development.template.xcconfig b/UnstoppableWallet/UnstoppableWallet/Configuration/Development.template.xcconfig index 2bf47481fc..fd72f9eecd 100644 --- a/UnstoppableWallet/UnstoppableWallet/Configuration/Development.template.xcconfig +++ b/UnstoppableWallet/UnstoppableWallet/Configuration/Development.template.xcconfig @@ -17,6 +17,8 @@ private_cloud_container_id = iCloud.io.horizontalsystems.bank-wallet.dev open_sea_api_key = unstoppable_domains_api_key = one_inch_api_key = +one_inch_commission = +one_inch_commission_address = swap_enabled = true donate_enabled = true diff --git a/UnstoppableWallet/UnstoppableWallet/Configuration/Production.template.xcconfig b/UnstoppableWallet/UnstoppableWallet/Configuration/Production.template.xcconfig index c66a4675f7..e144c49d22 100644 --- a/UnstoppableWallet/UnstoppableWallet/Configuration/Production.template.xcconfig +++ b/UnstoppableWallet/UnstoppableWallet/Configuration/Production.template.xcconfig @@ -17,5 +17,7 @@ private_cloud_container_id = iCloud.io.horizontalsystems.bank-wallet open_sea_api_key = unstoppable_domains_api_key = one_inch_api_key = +one_inch_commission = +one_inch_commission_address = swap_enabled = true donate_enabled = true diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/BinanceAdapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/BinanceAdapter.swift index 7271d3aa69..647339c6cc 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/BinanceAdapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/BinanceAdapter.swift @@ -5,7 +5,7 @@ import RxSwift class BinanceAdapter { static let confirmationsThreshold = 1 - static let transferFee: Decimal = 0.000075 + static let transferFee: Decimal = .init(string: "0.000075") ?? 0.000075 private let binanceKit: BinanceChainKit private let feeToken: Token @@ -144,6 +144,10 @@ extension BinanceAdapter: ISendBinanceAdapter { binanceKit.sendSingle(symbol: asset.symbol, to: address, amount: amount, memo: memo ?? "") .map { _ in () } } + + func send(amount: Decimal, address: String, memo: String?) async throws -> String { + try await binanceKit.send(symbol: asset.symbol, to: address, amount: amount, memo: memo ?? "") + } } extension BinanceAdapter: ITransactionsAdapter { diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/BitcoinBaseAdapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/BitcoinBaseAdapter.swift index 1d701fc59a..e761311841 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/BitcoinBaseAdapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/BitcoinBaseAdapter.swift @@ -142,19 +142,6 @@ class BitcoinBaseAdapter { } } - private func convertToSatoshi(value: Decimal) -> Int { - let coinValue: Decimal = value * coinRate - let handler = NSDecimalNumberHandler(roundingMode: .plain, scale: Int16(truncatingIfNeeded: 0), raiseOnExactness: false, raiseOnOverflow: false, raiseOnUnderflow: false, raiseOnDivideByZero: false) - return NSDecimalNumber(decimal: coinValue).rounding(accordingToBehavior: handler).intValue - } - - private func convertToKitSortMode(sort: TransactionDataSortMode) -> TransactionDataSortType { - switch sort { - case .shuffle: return .shuffle - case .bip69: return .bip69 - } - } - private func balanceData(balanceInfo: BalanceInfo) -> BalanceData { LockedBalanceData( available: Decimal(balanceInfo.spendable) / coinRate, @@ -290,8 +277,8 @@ extension BitcoinBaseAdapter: IBalanceAdapter { } extension BitcoinBaseAdapter { - func availableBalance(feeRate: Int, address: String?, memo: String?, unspentOutputs: [UnspentOutputInfo]?, pluginData: [UInt8: IBitcoinPluginData] = [:]) -> Decimal { - let amount = (try? abstractKit.maxSpendableValue(toAddress: address, memo: memo, feeRate: feeRate, unspentOutputs: unspentOutputs, pluginData: pluginData)) ?? 0 + func availableBalance(params: SendParameters) -> Decimal { + let amount = (try? abstractKit.maxSpendableValue(params: params)) ?? 0 return Decimal(amount) / coinRate } @@ -299,15 +286,15 @@ extension BitcoinBaseAdapter { try? abstractKit.maxSpendLimit(pluginData: pluginData).flatMap { Decimal($0) / coinRate } } - func minimumSendAmount(address: String?) -> Decimal { + func minimumSendAmount(params: SendParameters) -> Decimal { do { - return try Decimal(abstractKit.minSpendableValue(toAddress: address)) / coinRate + return try Decimal(abstractKit.minSpendableValue(params: params)) / coinRate } catch { return 0 } } - func validate(address: String, pluginData: [UInt8: IBitcoinPluginData]) throws { + func validate(address: String, pluginData: [UInt8: IPluginData]) throws { try abstractKit.validate(address: address, pluginData: pluginData) } @@ -315,10 +302,8 @@ extension BitcoinBaseAdapter { try validate(address: address, pluginData: [:]) } - func sendInfo(amount: Decimal, feeRate: Int, address: String?, memo: String?, unspentOutputs: [UnspentOutputInfo]?, pluginData: [UInt8: IBitcoinPluginData] = [:]) throws -> SendInfo { - let amount = convertToSatoshi(value: amount) - - let info = try abstractKit.sendInfo(for: amount, toAddress: address, memo: memo, feeRate: feeRate, unspentOutputs: unspentOutputs, pluginData: pluginData) + func sendInfo(params: SendParameters) throws -> SendInfo { + let info = try abstractKit.sendInfo(params: params) return SendInfo( unspentOutputs: info.unspentOutputs.map(\.info), fee: Decimal(info.fee) / coinRate, @@ -327,19 +312,20 @@ extension BitcoinBaseAdapter { ) } - var unspentOutputs: [UnspentOutputInfo] { - abstractKit.unspentOutputs + func unspentOutputs(filters: UtxoFilters) -> [UnspentOutputInfo] { + abstractKit.unspentOutputs(filters: filters) } - func sendSingle(amount: Decimal, address: String, memo: String?, feeRate: Int, unspentOutputs: [UnspentOutputInfo]?, pluginData: [UInt8: IBitcoinPluginData] = [:], sortMode: TransactionDataSortMode, rbfEnabled: Bool, logger: Logger) -> Single { - let satoshiAmount = convertToSatoshi(value: amount) - let sortType = convertToKitSortMode(sort: sortMode) + func send(params: SendParameters) throws { + _ = try abstractKit.send(params: params) + } - return Single.create { [weak self] observer in + func sendSingle(params: SendParameters, logger: Logger) -> Single { + Single.create { [weak self] observer in do { if let adapter = self { logger.debug("Sending to \(String(reflecting: adapter.abstractKit))", save: true) - _ = try adapter.abstractKit.send(to: address, memo: memo, value: satoshiAmount, feeRate: feeRate, sortType: sortType, rbfEnabled: rbfEnabled, unspentOutputs: unspentOutputs, pluginData: pluginData) + try adapter.send(params: params) } observer(.success(())) } catch { @@ -349,6 +335,19 @@ extension BitcoinBaseAdapter { return Disposables.create() } } + + func convertToSatoshi(value: Decimal) -> Int { + let coinValue: Decimal = value * coinRate + let handler = NSDecimalNumberHandler(roundingMode: .plain, scale: Int16(truncatingIfNeeded: 0), raiseOnExactness: false, raiseOnOverflow: false, raiseOnUnderflow: false, raiseOnDivideByZero: false) + return NSDecimalNumber(decimal: coinValue).rounding(accordingToBehavior: handler).intValue + } + + func convertToKitSortMode(sort: TransactionDataSortMode) -> TransactionDataSortType { + switch sort { + case .shuffle: return .shuffle + case .bip69: return .bip69 + } + } } extension BitcoinBaseAdapter: ITransactionsAdapter { diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/DashAdapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/DashAdapter.swift index 75164cab0a..62fb0c0d13 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/DashAdapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/DashAdapter.swift @@ -57,15 +57,15 @@ class DashAdapter: BitcoinBaseAdapter { } override var explorerTitle: String { - "dash.org" + "blockchair.com" } override func explorerUrl(transactionHash: String) -> String? { - "https://insight.dash.org/insight/tx/" + transactionHash + "https://blockchair.com/dash/transaction/" + transactionHash } override func explorerUrl(address: String) -> String? { - "https://insight.dash.org/insight/address/" + address + "https://blockchair.com/dash/address/" + address } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift index d032dce78f..c72e59ed6b 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/ZcashAdapter.swift @@ -66,8 +66,8 @@ class ZcashAdapter { private(set) var syncing: Bool = true init(wallet: Wallet, restoreSettings: RestoreSettings) throws { - logger = App.shared.logger.scoped(with: "ZCashKit") - // HsToolKit.Logger(minLogLevel: .debug) + logger = App.shared.logger.scoped(with: "ZCashKit") +// logger = HsToolKit.Logger(minLogLevel: .debug) guard let seed = wallet.account.type.mnemonicSeed else { throw AdapterError.unsupportedAccount @@ -779,6 +779,23 @@ extension ZcashAdapter: ISendZcashAdapter { } } + func send(amount: Decimal, address: Recipient, memo: Memo?) async throws { + guard let spendingKey else { + throw AppError.ZcashError.noReceiveAddress + } + + let pendingEntity = try await synchronizer.sendToAddress( + spendingKey: spendingKey, + zatoshi: Zatoshi.from(decimal: amount), + toAddress: address, + memo: memo + ) + + logger?.log(level: .debug, message: "Successful send TX: : \(pendingEntity.value.decimalValue.description):") + + reSyncPending() + } + func recipient(from stringEncodedAddress: String) -> ZcashLightClientKit.Recipient? { try? Recipient(stringEncodedAddress, network: network.networkType) } @@ -841,7 +858,7 @@ enum ZCashAdapterState: Equatable { case let .downloadingSapling(progress): return .customSyncing(main: "balance.downloading_sapling".localized(progress), secondary: nil, progress: progress) case let .downloadingBlocks(progress, _): - let percentValue = ValueFormatter.instance.format(percentValue: Decimal(Double(progress * 100)), showSign: false) + let percentValue = ValueFormatter.instance.format(percentValue: Decimal(Double(progress * 100)), signType: .never) return .customSyncing(main: "balance.downloading_blocks".localized, secondary: percentValue, progress: Int(progress * 100)) case let .notSynced(error): return .notSynced(error: error) } @@ -855,7 +872,7 @@ enum ZCashAdapterState: Equatable { case let .syncing(progress, lastDate): return "Syncing: progress = \(progress?.description ?? "N/A"), lastBlockDate: \(lastDate?.description ?? "N/A")" case let .downloadingSapling(progress): return "downloadingSapling: progress = \(progress)" case let .downloadingBlocks(progress, _): - let percentValue = ValueFormatter.instance.format(percentValue: Decimal(Double(progress * 100)), showSign: false) + let percentValue = ValueFormatter.instance.format(percentValue: Decimal(Double(progress * 100)), signType: .never) return "Downloading Blocks: \(percentValue?.description ?? "N/A") : \(Int(progress * 100))" case let .notSynced(error): return "Not synced \(error.localizedDescription)" } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/App.swift b/UnstoppableWallet/UnstoppableWallet/Core/App.swift index 5bcc76829d..197c3d42b9 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/App.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/App.swift @@ -39,6 +39,8 @@ class App { let balancePrimaryValueManager: BalancePrimaryValueManager let balanceHiddenManager: BalanceHiddenManager let balanceConversionManager: BalanceConversionManager + let walletButtonHiddenManager: WalletButtonHiddenManager + let priceChangeModeManager: PriceChangeModeManager let appVersionStorage: AppVersionStorage let appVersionManager: AppVersionManager @@ -51,7 +53,7 @@ class App { let networkManager: NetworkManager let guidesManager: GuidesManager let termsManager: TermsManager - let favoritesManager: FavoritesManager + let watchlistManager: WatchlistManager let contactManager: ContactBookManager let subscriptionManager: SubscriptionManager @@ -120,6 +122,7 @@ class App { userDefaultsStorage = UserDefaultsStorage() localStorage = LocalStorage(userDefaultsStorage: userDefaultsStorage) keychainStorage = KeychainStorage(service: "io.horizontalsystems.bank.dev") + let sharedLocalStorage = SharedLocalStorage() pasteboardManager = PasteboardManager() reachabilityManager = ReachabilityManager() @@ -139,6 +142,8 @@ class App { balancePrimaryValueManager = BalancePrimaryValueManager(userDefaultsStorage: userDefaultsStorage) balanceHiddenManager = BalanceHiddenManager(userDefaultsStorage: userDefaultsStorage) balanceConversionManager = BalanceConversionManager(marketKit: marketKit, userDefaultsStorage: userDefaultsStorage) + walletButtonHiddenManager = WalletButtonHiddenManager(userDefaultsStorage: userDefaultsStorage) + priceChangeModeManager = PriceChangeModeManager(storage: sharedLocalStorage) let appVersionRecordStorage = AppVersionRecordStorage(dbPool: dbPool) appVersionStorage = AppVersionStorage(storage: appVersionRecordStorage) @@ -152,14 +157,12 @@ class App { debugLogger = DebugLogger(localStorage: localStorage, dateProvider: CurrentDateProvider()) } - let sharedLocalStorage = SharedLocalStorage() currencyManager = CurrencyManager(storage: sharedLocalStorage) networkManager = NetworkManager(logger: logger) guidesManager = GuidesManager(networkManager: networkManager) termsManager = TermsManager(userDefaultsStorage: userDefaultsStorage) - let favoriteCoinRecordStorage = FavoriteCoinRecordStorage(dbPool: dbPool) - favoritesManager = FavoritesManager(storage: favoriteCoinRecordStorage, sharedStorage: sharedLocalStorage) + watchlistManager = WatchlistManager(storage: sharedLocalStorage, priceChangeModeManager: priceChangeModeManager) contactManager = ContactBookManager(localStorage: localStorage, ubiquityContainerIdentifier: AppConfig.privateCloudContainer, helper: ContactBookHelper(), logger: logger) subscriptionManager = SubscriptionManager(userDefaultsStorage: userDefaultsStorage, marketKit: marketKit) @@ -279,7 +282,7 @@ class App { accountManager: accountManager, accountFactory: accountFactory, walletManager: walletManager, - favoritesManager: favoritesManager, + watchlistManager: watchlistManager, evmSyncSourceManager: evmSyncSourceManager, btcBlockchainManager: btcBlockchainManager, restoreSettingsManager: restoreSettingsManager, @@ -293,7 +296,9 @@ class App { balancePrimaryValueManager: balancePrimaryValueManager, balanceConversionManager: balanceConversionManager, balanceHiddenManager: balanceHiddenManager, - contactManager: contactManager + contactManager: contactManager, + priceChangeModeManager: priceChangeModeManager, + walletButtonHiddenManager: walletButtonHiddenManager ) cloudBackupManager = CloudBackupManager( ubiquityContainerIdentifier: AppConfig.sharedCloudContainer, diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/SettingsBackup.swift b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/SettingsBackup.swift index 9b8a19065d..3a1a2f249f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Crypto/SettingsBackup.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Crypto/SettingsBackup.swift @@ -2,7 +2,7 @@ import Chart import Foundation import ThemeKit -struct SettingsBackup: Codable { +class SettingsBackup: Codable { var evmSyncSources: EvmSyncSourceManager.SyncSourceBackup let btcModes: [BtcBlockchainManager.BtcRestoreModeBackup] @@ -15,8 +15,10 @@ struct SettingsBackup: Codable { let mode: ThemeMode let showMarketTab: Bool + let priceChangeMode: PriceChangeMode let launchScreen: LaunchScreen let conversionTokenQueryId: String? + let balanceHideButtons: Bool let balancePrimaryValue: BalancePrimaryValue let balanceAutoHide: Bool let appIcon: String @@ -32,12 +34,73 @@ struct SettingsBackup: Codable { case baseCurrency = "currency" case mode = "theme_mode" case showMarketTab = "show_market" + case priceChangeMode = "price_change_mode" case launchScreen = "launch_screen" case conversionTokenQueryId = "conversion_token_query_id" + case balanceHideButtons = "balance_hide_buttons" case balancePrimaryValue = "balance_primary_value" case balanceAutoHide = "balance_auto_hide" case appIcon = "app_icon" } + + init( + evmSyncSources: EvmSyncSourceManager.SyncSourceBackup, + btcModes: [BtcBlockchainManager.BtcRestoreModeBackup], + remoteContactsSync: Bool?, + swapProviders: [DefaultProvider], + chartIndicators: ChartIndicatorsRepository.BackupIndicators, + indicatorsShown: Bool, + currentLanguage: String, + baseCurrency: String, + mode: ThemeMode, + showMarketTab: Bool, + priceChangeMode: PriceChangeMode, + launchScreen: LaunchScreen, + conversionTokenQueryId: String?, + balanceHideButtons: Bool, + balancePrimaryValue: BalancePrimaryValue, + balanceAutoHide: Bool, + appIcon: String + ) { + self.evmSyncSources = evmSyncSources + self.btcModes = btcModes + self.remoteContactsSync = remoteContactsSync + self.swapProviders = swapProviders + self.chartIndicators = chartIndicators + self.indicatorsShown = indicatorsShown + self.currentLanguage = currentLanguage + self.baseCurrency = baseCurrency + self.mode = mode + self.showMarketTab = showMarketTab + self.priceChangeMode = priceChangeMode + self.launchScreen = launchScreen + self.conversionTokenQueryId = conversionTokenQueryId + self.balanceHideButtons = balanceHideButtons + self.balancePrimaryValue = balancePrimaryValue + self.balanceAutoHide = balanceAutoHide + self.appIcon = appIcon + } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + evmSyncSources = try container.decode(EvmSyncSourceManager.SyncSourceBackup.self, forKey: .evmSyncSources) + btcModes = try container.decode([BtcBlockchainManager.BtcRestoreModeBackup].self, forKey: .btcModes) + remoteContactsSync = try? container.decode(Bool.self, forKey: .remoteContactsSync) + swapProviders = try container.decode([DefaultProvider].self, forKey: .swapProviders) + chartIndicators = try container.decode(ChartIndicatorsRepository.BackupIndicators.self, forKey: .chartIndicators) + indicatorsShown = try container.decode(Bool.self, forKey: .indicatorsShown) + currentLanguage = try container.decode(String.self, forKey: .currentLanguage) + baseCurrency = try container.decode(String.self, forKey: .baseCurrency) + mode = try container.decode(ThemeMode.self, forKey: .mode) + showMarketTab = try container.decode(Bool.self, forKey: .showMarketTab) + priceChangeMode = (try? container.decode(PriceChangeMode.self, forKey: .priceChangeMode)) ?? .hour24 + launchScreen = try container.decode(LaunchScreen.self, forKey: .launchScreen) + conversionTokenQueryId = try container.decode(String?.self, forKey: .conversionTokenQueryId) + balanceHideButtons = (try? container.decode(Bool.self, forKey: .balanceHideButtons)) ?? false + balancePrimaryValue = try container.decode(BalancePrimaryValue.self, forKey: .balancePrimaryValue) + balanceAutoHide = try container.decode(Bool.self, forKey: .balanceAutoHide) + appIcon = try container.decode(String.self, forKey: .appIcon) + } } extension SettingsBackup { diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Fiat/BaseFiatService.swift b/UnstoppableWallet/UnstoppableWallet/Core/Fiat/BaseFiatService.swift index 7e5e11c13b..8edca7e4ff 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Fiat/BaseFiatService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Fiat/BaseFiatService.swift @@ -53,7 +53,7 @@ class BaseFiatService { sync(coinPrice: marketKit.coinPrice(coinUid: coin.uid, currencyCode: currency.code)) if subscribe { - marketKit.coinPricePublisher(tag: "fiat-service", coinUid: coin.uid, currencyCode: currency.code) + marketKit.coinPricePublisher(coinUid: coin.uid, currencyCode: currency.code) .sink { [weak self] coinPrice in self?.sync(coinPrice: coinPrice) } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Fiat/FiatService.swift b/UnstoppableWallet/UnstoppableWallet/Core/Fiat/FiatService.swift index 43a59db0f7..6eeee5fc98 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Fiat/FiatService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Fiat/FiatService.swift @@ -126,7 +126,7 @@ class FiatService { sync(coinPrice: marketKit.coinPrice(coinUid: coin.uid, currencyCode: currency.code)) if subscribe { - marketKit.coinPricePublisher(tag: "fiat-service", coinUid: coin.uid, currencyCode: currency.code) + marketKit.coinPricePublisher(coinUid: coin.uid, currencyCode: currency.code) .sink { [weak self] coinPrice in self?.sync(coinPrice: coinPrice) } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/AppManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/AppManager.swift index c8b28cebd8..cb9e956ce5 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/AppManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/AppManager.swift @@ -1,3 +1,4 @@ +import Combine import Foundation import RxSwift import UIKit @@ -24,7 +25,8 @@ class AppManager { private let nftMetadataSyncer: NftMetadataSyncer private let didBecomeActiveSubject = PublishSubject() - private let willEnterForegroundSubject = PublishSubject() + private let willEnterForegroundSubjectOld = PublishSubject() + private let willEnterForegroundSubject = PassthroughSubject() init(accountManager: AccountManager, walletManager: WalletManager, adapterManager: AdapterManager, lockManager: LockManager, keychainManager: KeychainManager, passcodeLockManager: PasscodeLockManager, blurManager: BlurManager, @@ -105,7 +107,9 @@ extension AppManager { blurManager.willEnterForeground() debugBackgroundLogger?.logEnterForeground() - willEnterForegroundSubject.onNext(()) + + willEnterForegroundSubjectOld.onNext(()) + willEnterForegroundSubject.send() passcodeLockManager.handleForeground() lockManager.willEnterForeground() @@ -128,12 +132,18 @@ extension AppManager { } } +extension AppManager { + var willEnterForegroundPublisher: AnyPublisher { + willEnterForegroundSubject.eraseToAnyPublisher() + } +} + extension AppManager: IAppManager { var didBecomeActiveObservable: Observable { didBecomeActiveSubject.asObservable() } var willEnterForegroundObservable: Observable { - willEnterForegroundSubject.asObservable() + willEnterForegroundSubjectOld.asObservable() } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/BalanceConversionManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/BalanceConversionManager.swift index de93dfb898..e92d80aab8 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/BalanceConversionManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/BalanceConversionManager.swift @@ -1,12 +1,10 @@ +import HsExtensions import MarketKit -import RxRelay -import RxSwift class BalanceConversionManager { private let tokenQueries = [ TokenQuery(blockchainType: .bitcoin, tokenType: .derived(derivation: .bip84)), TokenQuery(blockchainType: .ethereum, tokenType: .native), - TokenQuery(blockchainType: .binanceSmartChain, tokenType: .native), ] private let keyBlockchainUid = "conversion-blockchain-uid" @@ -15,10 +13,8 @@ class BalanceConversionManager { let conversionTokens: [Token] - private let conversionTokenRelay = PublishRelay() - private(set) var conversionToken: Token? { + @PostPublished private(set) var conversionToken: Token? { didSet { - conversionTokenRelay.accept(conversionToken) userDefaultsStorage.set(value: conversionToken?.blockchain.uid, for: keyBlockchainUid) } } @@ -48,10 +44,6 @@ class BalanceConversionManager { } extension BalanceConversionManager { - var conversionTokenObservable: Observable { - conversionTokenRelay.asObservable() - } - func toggleConversionToken() { guard conversionTokens.count > 1, let conversionToken else { return @@ -63,12 +55,17 @@ extension BalanceConversionManager { } func set(conversionToken: Token?) { + guard self.conversionToken != conversionToken else { + return + } + self.conversionToken = conversionToken } func set(tokenQueryId: String?) { conversionToken = tokenQueryId .flatMap { TokenQuery(id: $0) } + .flatMap { tokenQueries.contains($0) ? $0 : nil } .flatMap { try? marketKit.token(query: $0) } ?? conversionTokens.first } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/DeepLinkManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/DeepLinkManager.swift index d50a9b2015..fa4742eba9 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/DeepLinkManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/DeepLinkManager.swift @@ -4,6 +4,7 @@ import RxRelay import RxSwift class DeepLinkManager { + static let deepLinkScheme = "unstoppable.money" static let tonDeepLinkScheme = "ton" private let newSchemeRelay = BehaviorRelay(value: nil) @@ -24,7 +25,7 @@ extension DeepLinkManager { let path = urlComponents.path let queryItems = urlComponents.queryItems - if (scheme == "unstoppable.money" && host == "wc") || (scheme == "https" && host == "unstoppable.money" && path == "/wc"), + if (scheme == DeepLinkManager.deepLinkScheme && host == "wc") || (scheme == "https" && host == DeepLinkManager.deepLinkScheme && path == "/wc"), let uri = queryItems?.first(where: { $0.name == "uri" })?.value { newSchemeRelay.accept(.walletConnect(url: uri)) @@ -41,7 +42,7 @@ extension DeepLinkManager { } } - if scheme == "unstoppable.money", host == "coin" { + if scheme == DeepLinkManager.deepLinkScheme, host == "coin" { let uid = path.replacingOccurrences(of: "/", with: "") newSchemeRelay.accept(.coin(uid: uid)) diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/PriceChangeModeManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/PriceChangeModeManager.swift new file mode 100644 index 0000000000..71476438d7 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/PriceChangeModeManager.swift @@ -0,0 +1,39 @@ +import Combine +import HsExtensions + +class PriceChangeModeManager { + private let keyPriceChangeMode = "price-change-mode" + + private let storage: SharedLocalStorage + + @PostPublished var priceChangeMode: PriceChangeMode { + didSet { + storage.set(value: priceChangeMode.rawValue, for: keyPriceChangeMode) + } + } + + var day1WatchlistPeriod: WatchlistTimePeriod { + switch priceChangeMode { + case .hour24: .hour24 + case .day1: .day1 + } + } + + init(storage: SharedLocalStorage) { + self.storage = storage + + if let rawValue: String = storage.value(for: keyPriceChangeMode), let value = PriceChangeMode(rawValue: rawValue) { + priceChangeMode = value + } else { + priceChangeMode = .hour24 + } + } + + func convert(period: WatchlistTimePeriod) -> WatchlistTimePeriod { + guard [WatchlistTimePeriod.day1, .hour24].contains(period) else { + return period + } + + return day1WatchlistPeriod + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/StatManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/StatManager.swift index 2fb59c9b5a..31a812e118 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/StatManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/StatManager.swift @@ -1,4 +1,5 @@ import Alamofire +import Combine import Foundation import HsToolKit import MarketKit @@ -10,6 +11,8 @@ func stat(page: StatPage, section: StatSection? = nil, event: StatEvent) { class StatManager { private static let keyLastSent = "stat_last_sent" + private static let keySendingAllowed = "sending_allowed" + private static let sendThreshold: TimeInterval = 1 * 60 * 60 // 1 hour private let marketKit: MarketKit.Kit @@ -18,6 +21,18 @@ class StatManager { private let appVersion: String private let appId: String? + var allowed: Bool { + didSet { + userDefaultsStorage.set(value: allowed, for: Self.keySendingAllowed) + allowedSubject.send(allowed) + } + } + + private let allowedSubject = PassthroughSubject() + var allowedPublisher: AnyPublisher { + allowedSubject.eraseToAnyPublisher() + } + init(marketKit: MarketKit.Kit, storage: StatStorage, userDefaultsStorage: UserDefaultsStorage) { self.marketKit = marketKit self.storage = storage @@ -25,76 +40,79 @@ class StatManager { appVersion = AppConfig.appVersion appId = AppConfig.appId + allowed = userDefaultsStorage.value(for: StatManager.keySendingAllowed) ?? true } func logStat(eventPage: StatPage, eventSection: StatSection? = nil, event: StatEvent) { -// var parameters: [String: Any]? -// -// if let params = event.params { -// parameters = [String: Any]() -// -// for (key, value) in params { -// parameters?[key.rawValue] = value -// } -// } -// -// let record = StatRecord( -// timestamp: Int(Date().timeIntervalSince1970), -// eventPage: eventPage.rawValue, -// eventSection: eventSection?.rawValue, -// event: event.name, -// params: parameters -// ) -// -// do { -// try storage.save(record: record) -// } catch { -// print("Cannot save StatRecord: \(error)") -// } + guard allowed else { return } + var parameters: [String: Any]? + + if let params = event.params { + parameters = [String: Any]() + + for (key, value) in params { + parameters?[key.rawValue] = value + } + } + + let record = StatRecord( + timestamp: Int(Date().timeIntervalSince1970), + eventPage: eventPage.rawValue, + eventSection: eventSection?.rawValue, + event: event.name, + params: parameters + ) + + do { + try storage.save(record: record) + } catch { + print("Cannot save StatRecord: \(error)") + } } func sendStats() { -// let lastSent: Double? = userDefaultsStorage.value(for: Self.keyLastSent) -// -// if let lastSent, Date().timeIntervalSince1970 - lastSent < Self.sendThreshold { -// return -// } -// -// Task { [storage] in -// let records = try storage.all() -// -// guard !records.isEmpty else { -// return -// } -// -// let stats = records.map { record in -// var object: [String: Any] = [ -// "time": record.timestamp, -// "event_page": record.eventPage, -// "event": record.event, -// ] -// -// if let eventSection = record.eventSection { -// object["event_section"] = eventSection -// } -// -// if let params = record.params { -// for (key, value) in params { -// object[key] = value -// } -// } -// -// return object -// } -// -//// let data = try JSONSerialization.data(withJSONObject: stats) -//// let string = String(data: data, encoding: .utf8) -//// print(string ?? "N/A") -// -// try await marketKit.send(stats: stats, appVersion: appVersion, appId: appId) -// -// userDefaultsStorage.set(value: Date().timeIntervalSince1970, for: Self.keyLastSent) -// try storage.clear() -// } + guard allowed else { return } + let lastSent: Double? = userDefaultsStorage.value(for: Self.keyLastSent) + + if let lastSent, Date().timeIntervalSince1970 - lastSent < Self.sendThreshold { + return + } + + Task { [storage] in + let records = try storage.all() + + guard !records.isEmpty else { + return + } + + let stats = records.map { record in + var object: [String: Any] = [ + "time": record.timestamp, + "event_page": record.eventPage, + "event": record.event, + ] + + if let eventSection = record.eventSection { + object["event_section"] = eventSection + } + + if let params = record.params { + for (key, value) in params { + object[key] = value + } + } + + return object + } + +// let data = try JSONSerialization.data(withJSONObject: stats) +// let string = String(data: data, encoding: .utf8) +// print(string ?? "N/A") + + try await marketKit.send(stats: stats, appVersion: appVersion, appId: appId) + + userDefaultsStorage.set(value: Date().timeIntervalSince1970, for: Self.keyLastSent) + try storage.clear() + } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/WalletButtonHiddenManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/WalletButtonHiddenManager.swift new file mode 100644 index 0000000000..2d1d36588d --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/WalletButtonHiddenManager.swift @@ -0,0 +1,30 @@ +import RxRelay +import RxSwift + +class WalletButtonHiddenManager { + private let keyButtonHidden = "wallet-button-hidden" + + private let userDefaultsStorage: UserDefaultsStorage + + private let buttonHiddenRelay = PublishRelay() + var buttonHidden: Bool { + get { + userDefaultsStorage.value(for: keyButtonHidden) ?? false + } + set { + guard buttonHidden != newValue else { return } + userDefaultsStorage.set(value: newValue, for: keyButtonHidden) + buttonHiddenRelay.accept(newValue) + } + } + + init(userDefaultsStorage: UserDefaultsStorage) { + self.userDefaultsStorage = userDefaultsStorage + } +} + +extension WalletButtonHiddenManager { + var buttonHiddenObservable: Observable { + buttonHiddenRelay.asObservable() + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/WatchlistManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/WatchlistManager.swift new file mode 100644 index 0000000000..2893b82d4c --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/WatchlistManager.swift @@ -0,0 +1,125 @@ +import Combine +import HsExtensions +import WidgetKit + +class WatchlistManager { + private let keyCoinUids = "watchlist-coin-uids" + private let keySortBy = "watchlist-sort-by" + private let keyTimePeriod = "watchlist-time-period" + private let keyShowSignals = "watchlist-show-signals" + + private let storage: SharedLocalStorage + private let priceChangeModeManager: PriceChangeModeManager + private var cancellables = Set() + + private let coinUidsSubject = PassthroughSubject<[String], Never>() + + var coinUids: [String] { + didSet { + coinUidSet = Set(coinUids) + coinUidsSubject.send(coinUids) + + storage.set(value: coinUids, for: keyCoinUids) + + WidgetCenter.shared.reloadTimelines(ofKind: AppWidgetConstants.watchlistWidgetKind) + } + } + + private var coinUidSet: Set + + var sortBy: WatchlistSortBy { + didSet { + storage.set(value: sortBy.rawValue, for: keySortBy) + WidgetCenter.shared.reloadTimelines(ofKind: AppWidgetConstants.watchlistWidgetKind) + } + } + + @PostPublished var timePeriod: WatchlistTimePeriod { + didSet { + guard timePeriod != oldValue else { + return + } + + storage.set(value: timePeriod.rawValue, for: keyTimePeriod) + WidgetCenter.shared.reloadTimelines(ofKind: AppWidgetConstants.watchlistWidgetKind) + } + } + + var showSignals: Bool { + didSet { + storage.set(value: showSignals, for: keyShowSignals) + // WidgetCenter.shared.reloadTimelines(ofKind: AppWidgetConstants.watchlistWidgetKind) + } + } + + init(storage: SharedLocalStorage, priceChangeModeManager: PriceChangeModeManager) { + self.storage = storage + self.priceChangeModeManager = priceChangeModeManager + + coinUids = storage.value(for: keyCoinUids) ?? [] + coinUidSet = Set(coinUids) + + let sortByRaw: String? = storage.value(for: keySortBy) + sortBy = sortByRaw.flatMap { WatchlistSortBy(rawValue: $0) } ?? .manual + + let timePeriodRaw: String? = storage.value(for: keyTimePeriod) + timePeriod = timePeriodRaw.flatMap { WatchlistTimePeriod(rawValue: $0) } ?? priceChangeModeManager.day1WatchlistPeriod + + showSignals = storage.value(for: keyShowSignals) ?? true + + WidgetCenter.shared.reloadTimelines(ofKind: AppWidgetConstants.watchlistWidgetKind) + + priceChangeModeManager.$priceChangeMode + .sink { [weak self] _ in + self?.syncPeriod() + } + .store(in: &cancellables) + } + + private func syncPeriod() { + timePeriod = priceChangeModeManager.convert(period: timePeriod) + } +} + +extension WatchlistManager { + var coinUidsPublisher: AnyPublisher<[String], Never> { + coinUidsSubject.eraseToAnyPublisher() + } + + var timePeriods: [WatchlistTimePeriod] { + [priceChangeModeManager.day1WatchlistPeriod, .week1, .month1, .month3] + } + + func set(coinUids: [String]) { + self.coinUids = coinUids + } + + func add(coinUid: String) { + guard !coinUids.contains(coinUid) else { + return + } + + coinUids.append(coinUid) + } + + func add(coinUids: [String]) { + let coinUids = coinUids.filter { !self.coinUids.contains($0) } + self.coinUids.append(contentsOf: coinUids) + } + + func removeAll() { + coinUids = [] + } + + func remove(coinUid: String) { + guard let index = coinUids.firstIndex(of: coinUid) else { + return + } + + coinUids.remove(at: index) + } + + func isWatched(coinUid: String) -> Bool { + coinUidSet.contains(coinUid) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Protocols.swift b/UnstoppableWallet/UnstoppableWallet/Core/Protocols.swift index 26724ed3f9..f924fa382c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Protocols.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Protocols.swift @@ -68,13 +68,15 @@ protocol ITransactionsAdapter { protocol ISendBitcoinAdapter { var blockchainType: BlockchainType { get } var balanceData: BalanceData { get } - func availableBalance(feeRate: Int, address: String?, memo: String?, unspentOutputs: [UnspentOutputInfo]?, pluginData: [UInt8: IBitcoinPluginData]) -> Decimal + func availableBalance(params: SendParameters) -> Decimal func maximumSendAmount(pluginData: [UInt8: IBitcoinPluginData]) -> Decimal? - func minimumSendAmount(address: String?) -> Decimal - func validate(address: String, pluginData: [UInt8: IBitcoinPluginData]) throws - var unspentOutputs: [UnspentOutputInfo] { get } - func sendInfo(amount: Decimal, feeRate: Int, address: String?, memo: String?, unspentOutputs: [UnspentOutputInfo]?, pluginData: [UInt8: IBitcoinPluginData]) throws -> SendInfo - func sendSingle(amount: Decimal, address: String, memo: String?, feeRate: Int, unspentOutputs: [UnspentOutputInfo]?, pluginData: [UInt8: IBitcoinPluginData], sortMode: TransactionDataSortMode, rbfEnabled: Bool, logger: HsToolKit.Logger) -> Single + func minimumSendAmount(params: SendParameters) -> Decimal + func validate(address: String, pluginData: [UInt8: IPluginData]) throws + func unspentOutputs(filters: UtxoFilters) -> [UnspentOutputInfo] + func sendInfo(params: SendParameters) throws -> SendInfo + func sendSingle(params: SendParameters, logger: HsToolKit.Logger) -> Single + func convertToSatoshi(value: Decimal) -> Int + func convertToKitSortMode(sort: TransactionDataSortMode) -> TransactionDataSortType } protocol ISendDashAdapter { @@ -121,6 +123,7 @@ protocol ISendBinanceAdapter { func validate(address: String) throws var fee: Decimal { get } func sendSingle(amount: Decimal, address: String, memo: String?) -> Single + func send(amount: Decimal, address: String, memo: String?) async throws -> String } protocol ISendZcashAdapter { @@ -128,6 +131,7 @@ protocol ISendZcashAdapter { func validate(address: String, checkSendToSelf: Bool) throws -> ZcashAdapter.AddressType var fee: Decimal { get } func sendSingle(amount: Decimal, address: Recipient, memo: Memo?) -> Single + func send(amount: Decimal, address: Recipient, memo: Memo?) async throws func recipient(from stringEncodedAddress: String) -> Recipient? } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Providers/AppConfig.swift b/UnstoppableWallet/UnstoppableWallet/Core/Providers/AppConfig.swift index 94a70c23a3..c56967ecd6 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Providers/AppConfig.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Providers/AppConfig.swift @@ -15,7 +15,6 @@ enum AppConfig { static let appGitHubRepository = "unstoppable-wallet-ios" static let appTwitterAccount = "unstoppablebyhs" static let appTelegramAccount = "unstoppable_announcements" - static let appRedditAccount = "UNSTOPPABLEWallet" static let mempoolSpaceUrl = "https://mempool.space" static let guidesIndexUrl = URL(string: "https://raw.githubusercontent.com/horizontalsystems/blockchain-crypto-guides/v1.2/index.json")! static let faqIndexUrl = URL(string: "https://raw.githubusercontent.com/horizontalsystems/unstoppable-wallet-website/master/src/faq.json")! @@ -117,7 +116,17 @@ enum AppConfig { } static var oneInchApiKey: String? { - (Bundle.main.object(forInfoDictionaryKey: "oneInchApiKey") as? String).flatMap { $0.isEmpty ? nil : $0 } + (Bundle.main.object(forInfoDictionaryKey: "OneInchApiKey") as? String).flatMap { $0.isEmpty ? nil : $0 } + } + + static var oneInchCommission: Decimal? { + (Bundle.main.object(forInfoDictionaryKey: "OneInchCommission") as? String).flatMap { + $0.isEmpty ? nil : Decimal(string: $0, locale: Locale(identifier: "en_US_POSIX")) + } + } + + static var oneInchCommissionAddress: String? { + (Bundle.main.object(forInfoDictionaryKey: "OneInchCommissionAddress") as? String).flatMap { $0.isEmpty ? nil : $0 } } static var defaultWords: String { diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Storage/FavoriteCoinRecordStorage.swift b/UnstoppableWallet/UnstoppableWallet/Core/Storage/FavoriteCoinRecordStorage.swift deleted file mode 100644 index bcbd06869e..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Core/Storage/FavoriteCoinRecordStorage.swift +++ /dev/null @@ -1,54 +0,0 @@ -import GRDB - -class FavoriteCoinRecordStorage { - private let dbPool: DatabasePool - - init(dbPool: DatabasePool) { - self.dbPool = dbPool - } -} - -extension FavoriteCoinRecordStorage { - var favoriteCoinRecords: [FavoriteCoinRecord] { - try! dbPool.read { db in - try FavoriteCoinRecord.fetchAll(db) - } - } - - func save(favoriteCoinRecord: FavoriteCoinRecord) { - _ = try! dbPool.write { db in - try favoriteCoinRecord.insert(db) - } - } - - func save(favoriteCoinRecords: [FavoriteCoinRecord]) { - _ = try? dbPool.write { db in - for record in favoriteCoinRecords { - try record.insert(db) - } - } - } - - func deleteAll() { - _ = try! dbPool.write { db in - try FavoriteCoinRecord - .deleteAll(db) - } - } - - func deleteFavoriteCoinRecord(coinUid: String) { - _ = try! dbPool.write { db in - try FavoriteCoinRecord - .filter(FavoriteCoinRecord.Columns.coinUid == coinUid) - .deleteAll(db) - } - } - - func favoriteCoinRecordExists(coinUid: String) -> Bool { - try! dbPool.read { db in - try FavoriteCoinRecord - .filter(FavoriteCoinRecord.Columns.coinUid == coinUid) - .fetchCount(db) > 0 - } - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Storage/LocalStorage.swift b/UnstoppableWallet/UnstoppableWallet/Core/Storage/LocalStorage.swift index db05e37428..cd2e4cdd08 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Storage/LocalStorage.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Storage/LocalStorage.swift @@ -17,6 +17,7 @@ class LocalStorage { private let keyUserChartIndicatorsSync = "user-chart-indicators" private let keyIndicatorsShown = "indicators-shown" private let keyTelegramSupportRequested = "telegram-support-requested" + private let keyNewSendEnabled = "new-send-enabled" private let userDefaultsStorage: UserDefaultsStorage @@ -91,6 +92,11 @@ extension LocalStorage { get { userDefaultsStorage.value(for: keyTelegramSupportRequested) ?? false } set { userDefaultsStorage.set(value: newValue, for: keyTelegramSupportRequested) } } + + var newSendEnabled: Bool { + get { userDefaultsStorage.value(for: keyNewSendEnabled) ?? false } + set { userDefaultsStorage.set(value: newValue, for: keyNewSendEnabled) } + } } extension LocalStorage { diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Storage/StorageMigrator.swift b/UnstoppableWallet/UnstoppableWallet/Core/Storage/StorageMigrator.swift index bb72595ffb..5e7ff27f5b 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Storage/StorageMigrator.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Storage/StorageMigrator.swift @@ -331,16 +331,6 @@ enum StorageMigrator { try db.drop(table: CoinRecord_v19.databaseTableName) } - migrator.registerMigration("recreateFavoriteCoins") { db in - if try db.tableExists("favorite_coins") { - try db.drop(table: "favorite_coins") - } - - try db.create(table: "favorite_coins_v20") { t in - t.column("coinType", .text).notNull() - } - } - migrator.registerMigration("createActiveAccount") { db in try db.create(table: ActiveAccount_v_0_36.databaseTableName) { t in t.column(ActiveAccount_v_0_36.Columns.uniqueId.name, .text).notNull() @@ -512,12 +502,6 @@ enum StorageMigrator { } } - migrator.registerMigration("newStructureForFavoriteCoins") { db in - try db.create(table: FavoriteCoinRecord.databaseTableName) { t in - t.column(FavoriteCoinRecord.Columns.coinUid.name, .text).primaryKey() - } - } - migrator.registerMigration("createWalletConnectV2Sessions") { db in try db.create(table: WalletConnectSession.databaseTableName) { t in t.column(WalletConnectSession.Columns.accountId.name, .text).notNull() @@ -823,6 +807,20 @@ enum StorageMigrator { } } + migrator.registerMigration("Migrate watchlist coin uids to local storage") { db in + if try db.tableExists(FavoriteCoinRecord_v_0_38.databaseTableName) { + let records = try FavoriteCoinRecord_v_0_38.fetchAll(db) + let coinUids = Array(Set(records.map(\.coinUid))) + + if !coinUids.isEmpty { + let sharedLocalStorage = SharedLocalStorage() + sharedLocalStorage.set(value: coinUids.sorted(), for: "watchlist-coin-uids") + } + + try db.drop(table: FavoriteCoinRecord_v_0_38.databaseTableName) + } + } + try migrator.migrate(dbPool) } diff --git a/UnstoppableWallet/UnstoppableWallet/Extensions/Advice.swift b/UnstoppableWallet/UnstoppableWallet/Extensions/Advice.swift new file mode 100644 index 0000000000..825e111814 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Extensions/Advice.swift @@ -0,0 +1,18 @@ +import MarketKit + +extension TechnicalAdvice.Advice: Identifiable { + public var id: Self { + self + } + + var shortTitle: String { + switch self { + case .oversold, .overbought: return "market.signal.risky".localized + case .strongBuy: return "market.signal.strong_buy".localized + case .buy: return "market.signal.buy".localized + case .neutral: return "market.signal.neutral".localized + case .sell: return "market.signal.sell".localized + case .strongSell: return "market.signal.strong_sell".localized + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Extensions/BlockchainType.swift b/UnstoppableWallet/UnstoppableWallet/Extensions/BlockchainType.swift index 7e497c2f4b..645944fbf9 100644 --- a/UnstoppableWallet/UnstoppableWallet/Extensions/BlockchainType.swift +++ b/UnstoppableWallet/UnstoppableWallet/Extensions/BlockchainType.swift @@ -85,6 +85,7 @@ extension BlockchainType { // used for EVM blockchains only var feePriceScale: FeePriceScale { switch self { + case .bitcoin, .bitcoinCash, .dash, .litecoin, .ecash: return .satoshi case .avalanche: return .nAvax default: return .gwei } diff --git a/UnstoppableWallet/UnstoppableWallet/Extensions/Coin.swift b/UnstoppableWallet/UnstoppableWallet/Extensions/Coin.swift index e7881e63b4..6d2b7debdc 100644 --- a/UnstoppableWallet/UnstoppableWallet/Extensions/Coin.swift +++ b/UnstoppableWallet/UnstoppableWallet/Extensions/Coin.swift @@ -6,4 +6,13 @@ extension Coin { let scale = Int(UIScreen.main.scale) return "https://cdn.blocksdecoded.com/coin-icons/32px/\(uid)@\(scale)x.png" } + + static func imageUrl(uid: String) -> String { + let scale = Int(UIScreen.main.scale) + return "https://cdn.blocksdecoded.com/coin-icons/32px/\(uid)@\(scale)x.png" + } +} + +extension Coin: Identifiable { + public var id: String { uid } } diff --git a/UnstoppableWallet/UnstoppableWallet/Extensions/CoinTreasury.swift b/UnstoppableWallet/UnstoppableWallet/Extensions/CoinTreasury.swift index b8684c2b4e..a76b386943 100644 --- a/UnstoppableWallet/UnstoppableWallet/Extensions/CoinTreasury.swift +++ b/UnstoppableWallet/UnstoppableWallet/Extensions/CoinTreasury.swift @@ -7,3 +7,13 @@ extension CoinTreasury { return "https://cdn.blocksdecoded.com/treasury-icons/\(fundUid)@\(scale)x.png" } } + +extension CoinTreasury: Hashable { + public static func == (lhs: CoinTreasury, rhs: CoinTreasury) -> Bool { + lhs.fundUid == rhs.fundUid + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(fundUid) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Extensions/DefiCoin.swift b/UnstoppableWallet/UnstoppableWallet/Extensions/DefiCoin.swift new file mode 100644 index 0000000000..490f47b8d4 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Extensions/DefiCoin.swift @@ -0,0 +1,21 @@ +import Foundation +import MarketKit + +extension DefiCoin { + var name: String { + switch type { + case let .defiCoin(name, _): return name + case let .fullCoin(fullCoin): return fullCoin.coin.name + } + } +} + +extension DefiCoin: Hashable { + public static func == (lhs: DefiCoin, rhs: DefiCoin) -> Bool { + lhs.uid == rhs.uid + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(uid) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Extensions/Etf.swift b/UnstoppableWallet/UnstoppableWallet/Extensions/Etf.swift new file mode 100644 index 0000000000..2e6e653b3c --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Extensions/Etf.swift @@ -0,0 +1,60 @@ +import Foundation +import MarketKit +import UIKit + +extension Etf { + var imageUrl: String { + let scale = Int(UIScreen.main.scale) + return "https://cdn.blocksdecoded.com/etf-tresuries/\(ticker)@\(scale)x.png" + } + + func inflow(timePeriod: MarketEtfViewModel.TimePeriod) -> Decimal? { + switch timePeriod { + case let .period(timePeriod): return inflows[timePeriod] + case .all: return totalInflow + } + } +} + +struct RankedEtf: Hashable { + let etf: Etf + let rank: Int + + public static func == (lhs: RankedEtf, rhs: RankedEtf) -> Bool { + lhs.etf.ticker == rhs.etf.ticker + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(etf.ticker) + } +} + +extension [RankedEtf] { + func sorted(sortBy: MarketEtfViewModel.SortBy, timePeriod: MarketEtfViewModel.TimePeriod) -> [RankedEtf] { + sorted { lhsRankedEtf, rhsRankedEtf in + let lhsEtf = lhsRankedEtf.etf + let rhsEtf = rhsRankedEtf.etf + + switch sortBy { + case .highestAssets, .lowestAssets: + guard let lhsAssets = lhsEtf.totalAssets else { + return false + } + guard let rhsAssets = rhsEtf.totalAssets else { + return true + } + + return sortBy == .highestAssets ? lhsAssets > rhsAssets : lhsAssets < rhsAssets + case .inflow, .outflow: + guard let lhsInflow = lhsEtf.inflow(timePeriod: timePeriod) else { + return false + } + guard let rhsInflow = rhsEtf.inflow(timePeriod: timePeriod) else { + return true + } + + return sortBy == .inflow ? lhsInflow > rhsInflow : lhsInflow < rhsInflow + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Extensions/FullCoin.swift b/UnstoppableWallet/UnstoppableWallet/Extensions/FullCoin.swift index 3026cf2f88..dfb9b96f81 100644 --- a/UnstoppableWallet/UnstoppableWallet/Extensions/FullCoin.swift +++ b/UnstoppableWallet/UnstoppableWallet/Extensions/FullCoin.swift @@ -5,3 +5,15 @@ extension FullCoin: Equatable { lhs.coin == rhs.coin && lhs.tokens == rhs.tokens } } + +extension FullCoin: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(coin.uid) + } +} + +extension FullCoin: Identifiable { + public var id: String { + coin.uid + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Extensions/HsTimePeriod.swift b/UnstoppableWallet/UnstoppableWallet/Extensions/HsTimePeriod.swift new file mode 100644 index 0000000000..c507886f1b --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Extensions/HsTimePeriod.swift @@ -0,0 +1,7 @@ +import MarketKit + +extension HsTimePeriod: Identifiable { + public var id: String { + rawValue + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Extensions/MarketInfo.swift b/UnstoppableWallet/UnstoppableWallet/Extensions/MarketInfo.swift new file mode 100644 index 0000000000..487d632611 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Extensions/MarketInfo.swift @@ -0,0 +1,11 @@ +import MarketKit + +extension MarketInfo: Hashable { + public static func == (lhs: MarketInfo, rhs: MarketInfo) -> Bool { + lhs.fullCoin.coin.uid == rhs.fullCoin.coin.uid + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(fullCoin.coin.uid) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Extensions/MarketPair.swift b/UnstoppableWallet/UnstoppableWallet/Extensions/MarketPair.swift new file mode 100644 index 0000000000..57d95859da --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Extensions/MarketPair.swift @@ -0,0 +1,13 @@ +import MarketKit + +extension MarketPair: Hashable { + public static func == (lhs: MarketPair, rhs: MarketPair) -> Bool { + lhs.base == rhs.base && lhs.target == rhs.target && lhs.marketName == rhs.marketName + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(base) + hasher.combine(target) + hasher.combine(marketName) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Extensions/Misc.swift b/UnstoppableWallet/UnstoppableWallet/Extensions/Misc.swift index 2b389e49d4..9fbd355e4e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Extensions/Misc.swift +++ b/UnstoppableWallet/UnstoppableWallet/Extensions/Misc.swift @@ -5,4 +5,9 @@ extension String { let scale = Int(UIScreen.main.scale) return "https://cdn.blocksdecoded.com/header-images/\(self)@\(scale)x.png" } + + var fiatImageUrl: String { + let scale = Int(UIScreen.main.scale) + return "https://cdn.blocksdecoded.com/fiat-icons/\(self)@\(scale)x.png" + } } diff --git a/UnstoppableWallet/UnstoppableWallet/Extensions/PriceChangeManager+HsTimePeriod.swift b/UnstoppableWallet/UnstoppableWallet/Extensions/PriceChangeManager+HsTimePeriod.swift new file mode 100644 index 0000000000..7a37130e94 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Extensions/PriceChangeManager+HsTimePeriod.swift @@ -0,0 +1,18 @@ +import MarketKit + +extension PriceChangeModeManager { + var day1Period: HsTimePeriod { + switch priceChangeMode { + case .hour24: .hour24 + case .day1: .day1 + } + } + + func convert(period: HsTimePeriod) -> HsTimePeriod { + guard [HsTimePeriod.day1, .hour24].contains(period) else { + return period + } + + return day1Period + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Extensions/SendParameters.swift b/UnstoppableWallet/UnstoppableWallet/Extensions/SendParameters.swift new file mode 100644 index 0000000000..894d2362b9 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Extensions/SendParameters.swift @@ -0,0 +1,20 @@ +import BitcoinCore + +extension SendParameters { + func copy() -> SendParameters { + SendParameters( + address: address, + value: value, + feeRate: feeRate, + sortType: sortType, + senderPay: senderPay, + rbfEnabled: rbfEnabled, + memo: memo, + unspentOutputs: unspentOutputs, + pluginData: pluginData, + dustThreshold: dustThreshold, + utxoFilters: utxoFilters, + changeToFirstInput: changeToFirstInput + ) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Extensions/StatExtensions.swift b/UnstoppableWallet/UnstoppableWallet/Extensions/StatExtensions.swift index 6495661032..695d1b5a14 100644 --- a/UnstoppableWallet/UnstoppableWallet/Extensions/StatExtensions.swift +++ b/UnstoppableWallet/UnstoppableWallet/Extensions/StatExtensions.swift @@ -3,6 +3,7 @@ import MarketKit extension HsTimePeriod { var statPeriod: StatPeriod { switch self { + case .hour24: return .hour24 case .day1: return .day1 case .week1: return .week1 case .week2: return .week2 @@ -16,6 +17,15 @@ extension HsTimePeriod { } } +extension MarketEtfViewModel.TimePeriod { + var statPeriod: StatPeriod { + switch self { + case .all: return .all + case let .period(timePeriod): return timePeriod.statPeriod + } + } +} + extension HsPeriodType { var statPeriod: StatPeriod { switch self { @@ -40,8 +50,10 @@ extension MainModule.Tab { extension MarketModule.Tab { var statTab: StatTab { switch self { - case .overview: return .overview - case .posts: return .news + case .coins: return .coins + case .news: return .news + case .pairs: return .pairs + case .platforms: return .platforms case .watchlist: return .watchlist } } @@ -103,68 +115,113 @@ extension CoinProChartModule.ProChartType { } } -extension MarketModule.MarketTop { +extension RankViewModel.RankType { + var statRankType: StatPage { + switch self { + case .cexVolume: return .coinRankCexVolume + case .dexVolume: return .coinRankDexVolume + case .dexLiquidity: return .coinRankDexLiquidity + case .address: return .coinRankAddress + case .txCount: return .coinRankTxCount + case .holders: return .coinRankHolders + case .fee: return .coinRankFee + case .revenue: return .coinRankRevenue + } + } +} + +extension MarketModule.Top { var statMarketTop: StatMarketTop { switch self { case .top100: return .top100 case .top200: return .top200 + case .top250: return .top250 case .top300: return .top300 + case .top500: return .top500 + case .top1000: return .top1000 + case .top1500: return .top1500 } } } -extension MarketModule.PriceChangeType { - var statPeriod: StatPeriod { +extension MarketModule.SortBy { + var statSortType: StatSortType { switch self { - case .day: return .day1 - case .week: return .week1 - case .week2: return .week2 - case .month: return .month1 - case .month6: return .month6 - case .year: return .year1 + case .highestCap: return .highestCap + case .lowestCap: return .lowestCap + case .highestVolume: return .highestVolume + case .lowestVolume: return .lowestVolume + case .gainers: return .topGainers + case .losers: return .topLosers } } } -extension MarketModule.MarketField { - var statField: StatField { +extension WatchlistSortBy { + var statSortType: StatSortType { switch self { - case .marketCap: return .marketCap - case .volume: return .volume - case .price: return .price + case .manual: return .manual + case .highestCap: return .highestCap + case .lowestCap: return .lowestCap + case .gainers: return .topGainers + case .losers: return .topLosers } } } -extension MarketModule.MarketPlatformField { - var statTvlChain: String { +extension MarketModule.SortOrder { + var statVolumeSortType: StatSortType { + switch self { + case .asc: return .lowestVolume + case .desc: return .highestVolume + } + } +} + +extension MarketTvlViewModel.DiffType { + var statField: String { + switch self { + case .percent: return "percent" + case .currencyValue: return "currency" + } + } +} + +extension MarketTvlViewModel.Platforms { + var statPlatform: String { switch self { case .all: return "all" - default: return chain + case .ethereum: return "Ethereum" + case .solana: return "Solana" + case .binance: return "Binance" + case .avalanche: return "Avalanche" + case .terra: return "Terra" + case .fantom: return "Fantom" + case .arbitrum: return "Arbitrum" + case .polygon: return "Polygon" } } } -extension MarketModule.SortingField { - var statSortType: StatSortType { +extension MarketEtfViewModel.SortBy { + var statSortBy: StatSortType { switch self { - case .highestCap: return .highestCap - case .lowestCap: return .lowestCap - case .highestVolume: return .highestVolume - case .lowestVolume: return .lowestVolume - case .topGainers: return .topGainers - case .topLosers: return .topLosers + case .highestAssets: return .highestAssets + case .lowestAssets: return .lowestAssets + case .inflow: return .inflow + case .outflow: return .outflow } } } -extension MarketTopPlatformsModule.SortType { - var statSortType: StatSortType { +extension WatchlistTimePeriod { + var statPeriod: StatPeriod { switch self { - case .highestCap: return .highestCap - case .lowestCap: return .lowestCap - case .topGainers: return .topGainers - case .topLosers: return .topLosers + case .hour24: return .hour24 + case .day1: return .day1 + case .week1: return .week1 + case .month1: return .month1 + case .month3: return .month3 } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Extensions/Token.swift b/UnstoppableWallet/UnstoppableWallet/Extensions/Token.swift index 1afdfa0529..9b6f23d28d 100644 --- a/UnstoppableWallet/UnstoppableWallet/Extensions/Token.swift +++ b/UnstoppableWallet/UnstoppableWallet/Extensions/Token.swift @@ -74,6 +74,14 @@ extension Token: Comparable { } extension Token { + func decimalValue(value: BigUInt) -> Decimal { + Decimal(bigUInt: value, decimals: decimals) ?? 0 + } + + func decimalValue(value: Int) -> Decimal { + Decimal(sign: value >= 0 ? .plus : .minus, exponent: -decimals, significand: Decimal(value)) + } + func fractionalMonetaryValue(value: Decimal) -> BigUInt { BigUInt(value.hs.roundedString(decimal: decimals)) ?? 0 } diff --git a/UnstoppableWallet/UnstoppableWallet/Extensions/TopPlatform.swift b/UnstoppableWallet/UnstoppableWallet/Extensions/TopPlatform.swift new file mode 100644 index 0000000000..991be96bcf --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Extensions/TopPlatform.swift @@ -0,0 +1,17 @@ +import MarketKit + +extension TopPlatform: Hashable { + public static func == (lhs: TopPlatform, rhs: TopPlatform) -> Bool { + lhs.blockchain.uid == rhs.blockchain.uid + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(blockchain.uid) + } +} + +extension TopPlatform: Identifiable { + public var id: String { + blockchain.uid + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Info.plist b/UnstoppableWallet/UnstoppableWallet/Info.plist index 33317bb65b..5f30b213a3 100644 --- a/UnstoppableWallet/UnstoppableWallet/Info.plist +++ b/UnstoppableWallet/UnstoppableWallet/Info.plist @@ -109,6 +109,12 @@ OfficeMode ${OfficeMode} + OneInchApiKey + ${one_inch_api_key} + OneInchCommission + ${one_inch_commission} + OneInchCommissionAddress + ${one_inch_commission_address} OpenSeaApiKey ${open_sea_api_key} OptimismEtherscanApiKey @@ -158,7 +164,5 @@ ${unstoppable_domains_api_key} WallectConnectV2ProjectKey ${wallet_connect_v2_project_key} - oneInchApiKey - ${one_inch_api_key} diff --git a/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift b/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift index 047deffee2..82451b008f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift @@ -320,14 +320,14 @@ extension AccountType { return (try? EvmKit.Address(hex: string)).map { AccountType.evmAddress(address: $0) } case .tronAddress: let hexData = Data(hex: string) - + let address: TronKit.Address? - if !hexData.isEmpty { // android convention address + if !hexData.isEmpty { // android convention address address = try? TronKit.Address(raw: hexData) - } else { // old ios style + } else { // old ios style address = try? TronKit.Address(address: string) } - + return address.map { AccountType.tronAddress(address: $0) } case .tonAddress: return AccountType.tonAddress(address: string) diff --git a/UnstoppableWallet/UnstoppableWallet/Models/AdapterState.swift b/UnstoppableWallet/UnstoppableWallet/Models/AdapterState.swift index 57f2b7dfde..fb4ad79efe 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/AdapterState.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/AdapterState.swift @@ -20,14 +20,6 @@ enum AdapterState { default: return false } } - - func spendAllowed(beforeSync: Bool) -> Bool { - switch self { - case .synced: return true - case .syncing, .customSyncing: return beforeSync ? true : false - case .stopped, .notSynced: return false - } - } } extension AdapterState: Equatable { diff --git a/UnstoppableWallet/UnstoppableWallet/Models/AppError.swift b/UnstoppableWallet/UnstoppableWallet/Models/AppError.swift index e59cf27a14..26ba913717 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/AppError.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/AppError.swift @@ -86,3 +86,18 @@ extension AppError: LocalizedError { } } } + +extension AppError { + var title: String? { + switch self { + case .notSupportedByHodler: return "fee_settings.time_lock".localized + default: return nil + } + } +} + +extension Error { + var title: String? { + (convertedError as? AppError).flatMap(\.title) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Models/BalanceData.swift b/UnstoppableWallet/UnstoppableWallet/Models/BalanceData.swift index 9bfc442988..d2675fa9da 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/BalanceData.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/BalanceData.swift @@ -20,10 +20,6 @@ class BalanceData: Codable, Equatable { available } - var sendBeforeSync: Bool { - false - } - var customStates: [CustomState] { [] } @@ -124,7 +120,6 @@ class VerifiedBalanceData: BalanceData { let fullBalance: Decimal override var balanceTotal: Decimal { super.balanceTotal } - override var sendBeforeSync: Bool { true } init(fullBalance: Decimal, available: Decimal) { self.fullBalance = fullBalance diff --git a/UnstoppableWallet/UnstoppableWallet/Models/BalancePrimaryValue.swift b/UnstoppableWallet/UnstoppableWallet/Models/BalancePrimaryValue.swift index f29ad54ddf..34ec715af8 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/BalancePrimaryValue.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/BalancePrimaryValue.swift @@ -1,18 +1,4 @@ enum BalancePrimaryValue: String, CaseIterable, Codable { case coin case currency - - var title: String { - switch self { - case .coin: return "appearance.balance_value.coin_value".localized - case .currency: return "appearance.balance_value.fiat_value".localized - } - } - - var subtitle: String { - switch self { - case .coin: return "appearance.balance_value.fiat_value".localized - case .currency: return "appearance.balance_value.coin_value".localized - } - } } diff --git a/UnstoppableWallet/UnstoppableWallet/Models/Deprecated/FavoriteCoinRecord_v_0_22.swift b/UnstoppableWallet/UnstoppableWallet/Models/Deprecated/FavoriteCoinRecord_v_0_22.swift deleted file mode 100644 index f811fd90c3..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Models/Deprecated/FavoriteCoinRecord_v_0_22.swift +++ /dev/null @@ -1,32 +0,0 @@ -// import GRDB -// import MarketKit -// -// class FavoriteCoinRecord_v_0_22: Record { -// let coinType: CoinType -// -// init(coinType: CoinType) { -// self.coinType = coinType -// -// super.init() -// } -// -// -// override class var databaseTableName: String { -// "favorite_coins_v20" -// } -// -// enum Columns: String, ColumnExpression { -// case coinType -// } -// -// required init(row: Row) { -// coinType = CoinType(id: row[Columns.coinType]) -// -// super.init(row: row) -// } -// -// override func encode(to container: inout PersistenceContainer) { -// container[Columns.coinType] = coinType.id -// } -// -// } diff --git a/UnstoppableWallet/UnstoppableWallet/Models/FavoriteCoinRecord.swift b/UnstoppableWallet/UnstoppableWallet/Models/Deprecated/FavoriteCoinRecord_v_0_38.swift similarity index 92% rename from UnstoppableWallet/UnstoppableWallet/Models/FavoriteCoinRecord.swift rename to UnstoppableWallet/UnstoppableWallet/Models/Deprecated/FavoriteCoinRecord_v_0_38.swift index 79216480b9..65bb7ca4c6 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/FavoriteCoinRecord.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/Deprecated/FavoriteCoinRecord_v_0_38.swift @@ -1,6 +1,6 @@ import GRDB -class FavoriteCoinRecord: Record { +class FavoriteCoinRecord_v_0_38: Record { let coinUid: String init(coinUid: String) { diff --git a/UnstoppableWallet/UnstoppableWallet/Models/FeePriceScale.swift b/UnstoppableWallet/UnstoppableWallet/Models/FeePriceScale.swift index 15613092fd..dc5d97007c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/FeePriceScale.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/FeePriceScale.swift @@ -21,7 +21,7 @@ enum FeePriceScale { } func description(value: Float, showSymbol: Bool = true) -> String { - ValueFormatter.instance.formatFull(value: Decimal(Double(value)), decimalCount: 9, symbol: showSymbol ? unit : nil, showSign: false) ?? value.description + ValueFormatter.instance.formatFull(value: Decimal(Double(value)), decimalCount: 9, symbol: showSymbol ? unit : nil, signType: .never) ?? value.description } func wrap(value: Int, step: Int) -> Float { diff --git a/UnstoppableWallet/UnstoppableWallet/Models/LaunchScreen.swift b/UnstoppableWallet/UnstoppableWallet/Models/LaunchScreen.swift index 875c86d711..b9e272b02b 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/LaunchScreen.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/LaunchScreen.swift @@ -30,4 +30,13 @@ extension LaunchScreen: Codable { case marketOverview = "market_overview" case watchlist } + + var statType: String { + switch self { + case .auto: return "auto" + case .balance: return "balance" + case .marketOverview: return "market_overview" + case .watchlist: return "watchlist" + } + } } diff --git a/UnstoppableWallet/UnstoppableWallet/Models/PriceChangeMode.swift b/UnstoppableWallet/UnstoppableWallet/Models/PriceChangeMode.swift new file mode 100644 index 0000000000..04655e535b --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Models/PriceChangeMode.swift @@ -0,0 +1,13 @@ +enum PriceChangeMode: String, CaseIterable, Codable { + case hour24 = "hour_24" + case day1 = "day_1" +} + +extension PriceChangeMode { + var statName: String { + switch self { + case .hour24: return "hour_24" + case .day1: return "day_1" + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Models/Stats.swift b/UnstoppableWallet/UnstoppableWallet/Models/Stats.swift index d07ba103ae..c8e8c03595 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/Stats.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/Stats.swift @@ -5,10 +5,10 @@ enum StatPage: String { case academy case accountExtendedPrivateKey = "account_extended_private_key" case accountExtendedPublicKey = "account_extended_public_key" - case addEvmSyncSource = "add_evm_sync_source" case addToken = "add_token" case advancedSearch = "advanced_search" case advancedSearchResults = "advanced_search_results" + case appStatus = "app_status" case appearance case backupManager = "backup_manager" case backupPromptAfterCreate = "backup_prompt_after_create" @@ -65,10 +65,12 @@ enum StatPage: String { case externalReddit = "external_reddit" case externalTelegram = "external_telegram" case externalTwitter = "external_twitter" + case externalWebsite = "external_website" case faq case globalMetricsDefiCap = "global_metrics_defi_cap" case globalMetricsMarketCap = "global_metrics_market_cap" case globalMetricsTvlInDefi = "global_metrics_tvl_in_defi" + case globalMetricsEtf = "global_metrics_etf" case globalMetricsVolume = "global_metrics_volume" case guide case importFull = "import_full" @@ -94,6 +96,7 @@ enum StatPage: String { case news case newWallet = "new_wallet" case newWalletAdvanced = "new_wallet_advanced" + case privacy case privateKeys = "private_keys" case publicKeys = "public_keys" case rateUs = "rate_us" @@ -111,6 +114,7 @@ enum StatPage: String { case swap case switchWallet = "switch_wallet" case tellFriends = "tell_friends" + case terms case tokenPage = "token_page" case topCoins = "top_coins" case topMarketPairs = "top_market_pairs" @@ -122,8 +126,12 @@ enum StatPage: String { case transactions case unlinkWallet = "unlink_wallet" case walletConnect = "wallet_connect" + case walletConnectPairings = "wallet_connect_pairings" + case walletConnectRequest = "wallet_connect_request" + case walletConnectSession = "wallet_connect_session" case watchlist case watchWallet = "watch_wallet" + case whatsNews = "whats_news" case widget } @@ -132,7 +140,12 @@ enum StatSection: String { case addressRecipient = "address_recipient" case addressSpender = "address_spender" case addressTo = "address_to" + case coins + case deepLink = "deep_link" case input + case news + case pairs + case platforms case popular case recent case searchResults = "search_results" @@ -141,6 +154,8 @@ enum StatSection: String { case topGainers = "top_gainers" case topLosers = "top_losers" case topPlatforms = "top_platforms" + case qrScan = "qr_scan" + case watchlist } enum StatEvent { @@ -149,7 +164,10 @@ enum StatEvent { case addToken(token: Token) case addToWallet case addToWatchlist(coinUid: String) + case approveRequest(chainUid: String) + case cancel case clear(entity: StatEntity) + case connect, reconnect, disconnect, reject case copy(entity: StatEntity) case copyAddress(chainUid: String) case createWallet(walletType: String) @@ -159,6 +177,7 @@ enum StatEvent { case edit(entity: StatEntity) case enableToken(token: Token) case exportFull + case hideBalanceButtons(hide: Bool) case importWallet(walletType: String) case importFull case open(page: StatPage) @@ -171,24 +190,37 @@ enum StatEvent { case openReceive(token: Token) case openResend(chainUid: String, type: String) case openSend(token: Token) + case openSendTokenList(coinUid: String?, chainUid: String?) case openTokenInfo(token: Token) case openTokenPage(element: WalletModule.Element) case paste(entity: StatEntity) case refresh + case rejectRequest(chainUid: String) case removeAmount case removeFromWallet case removeFromWatchlist(coinUid: String) case scanQr(entity: StatEntity) case select(entity: StatEntity) + case selectAppIcon(iconUid: String) + case openArticle(relativeUrl: String) + case selectBalanceConversion(coinUid: String) + case selectBalanceValue(type: String) + case selectLaunchScreen(type: String) + case selectTheme(type: String) case setAmount case share(entity: StatEntity) + case showMarketsTab(shown: Bool) + case showSignals(shown: Bool) + case switchBaseCurrency(code: String) case switchBtcSource(chainUid: String, type: BtcRestoreMode) case switchChartPeriod(period: StatPeriod) case switchEvmSource(chainUid: String, name: String) case switchField(field: StatField) case switchFilterType(type: String) + case switchLanguage(language: String) case switchMarketTop(marketTop: StatMarketTop) case switchPeriod(period: StatPeriod) + case switchPriceChangeMode(mode: PriceChangeMode) case switchSortType(sortType: StatSortType) case switchTab(tab: StatTab) case switchTvlChain(chain: String) @@ -198,7 +230,8 @@ enum StatEvent { case toggleIndicators(shown: Bool) case togglePrice case toggleSortDirection - case toggleTvlField + case toggleTvlField(field: String) + case walletConnectPair case watchWallet(walletType: String) var name: String { @@ -207,39 +240,58 @@ enum StatEvent { case .addEvmSource: return "add_evm_source" case .addToWallet: return "add_to_wallet" case .addToWatchlist: return "add_to_watchlist" + case .approveRequest: return "approve_request" + case .cancel: return "cancel" case .clear: return "clear" + case .connect: return "connect" case .copy, .copyAddress: return "copy" case .createWallet: return "create_wallet" case .delete: return "delete" case .deleteCustomEvmSource: return "delete_custom_evm_source" case .disableToken: return "disable_token" + case .disconnect: return "disconnect" case .edit: return "edit" case .enableToken: return "enable_token" case .exportFull: return "export_full" + case .hideBalanceButtons: return "hide_balance_buttons" case .importFull: return "import_full" case .importWallet: return "import_wallet" - case .open, .openCategory, .openCoin, .openPlatform, .openReceive, .openResend, .openSend, .openTokenPage, + case .open, .openCategory, .openCoin, .openPlatform, .openReceive, .openResend, .openSend, .openSendTokenList, .openTokenPage, .openBlockchainSettingsBtc, .openBlockchainSettingsEvm, .openBlockchainSettingsEvmAdd: return "open_page" case .openTokenInfo: return "open_token_info" case .paste: return "paste" + case .reconnect: return "reconnect" case .refresh: return "refresh" + case .reject: return "disconnect" + case .rejectRequest: return "reject_request" case .removeAmount: return "remove_amount" case .removeFromWallet: return "remove_from_wallet" case .removeFromWatchlist: return "remove_from_watchlist" case .scanQr: return "scan_qr" case .select: return "select" + case .selectAppIcon: return "select_app_icon" + case .openArticle: return "open_article" + case .selectBalanceConversion: return "select_balance_conversion" + case .selectBalanceValue: return "select_balance_value" + case .selectLaunchScreen: return "select_launch_screen" + case .selectTheme: return "select_theme" case .setAmount: return "set_amount" case .share: return "share" + case .showMarketsTab: return "show_markets_tab" + case .showSignals: return "show_signals" + case .switchBaseCurrency: return "switch_base_currency" case .switchBtcSource: return "switch_btc_source" case .switchChartPeriod: return "switch_chart_period" case .switchEvmSource: return "switch_evm_source" case .switchField: return "switch_field" case .switchFilterType: return "switch_filter_type" + case .switchLanguage: return "switch_language" case .switchMarketTop: return "switch_market_top" case .switchPeriod: return "switch_period" + case .switchPriceChangeMode: return "switch_price_change_mode" case .switchSortType: return "switch_sort_type" case .switchTab: return "switch_tab" - case .switchTvlChain: return "switch_tvl_platform" + case .switchTvlChain: return "switch_tvl_chain" case .toggleBalanceHidden: return "toggle_balance_hidden" case .toggleConversionCoin: return "toggle_conversion_coin" case .toggleHidden: return "toggle_hidden" @@ -247,6 +299,7 @@ enum StatEvent { case .togglePrice: return "toggle_price" case .toggleSortDirection: return "toggle_sort_direction" case .toggleTvlField: return "toggle_tvl_field" + case .walletConnectPair: return "wallet_connect_pair" case .watchWallet: return "watch_wallet" } } @@ -257,6 +310,7 @@ enum StatEvent { case let .addEvmSource(chainUid): return [.chainUid: chainUid] case let .addToken(token): return params(token: token).merging([.entity: StatEntity.token.rawValue]) { $1 } case let .addToWatchlist(coinUid): return [.coinUid: coinUid] + case let .approveRequest(chainUid): return [.chainUid: chainUid] case let .clear(entity): return [.entity: entity.rawValue] case let .copy(entity): return [.entity: entity.rawValue] case let .copyAddress(chainUid): return [.entity: StatEntity.address.rawValue, .chainUid: chainUid] @@ -266,6 +320,7 @@ enum StatEvent { case let .disableToken(token): return params(token: token) case let .edit(entity): return [.entity: entity.rawValue] case let .enableToken(token): return params(token: token) + case let .hideBalanceButtons(hide): return [.shown: hide] case let .importWallet(walletType): return [.walletType: walletType] case let .open(page): return [.page: page.rawValue] case let .openBlockchainSettingsBtc(chainUid: chainUid): return [.page: StatPage.blockchainSettingsBtc.rawValue, .chainUid: chainUid] @@ -273,6 +328,18 @@ enum StatEvent { case let .openBlockchainSettingsEvmAdd(chainUid: chainUid): return [.page: StatPage.blockchainSettingsEvmAdd.rawValue, .chainUid: chainUid] case let .openCategory(categoryUid): return [.page: StatPage.coinCategory.rawValue, .categoryUid: categoryUid] case let .openCoin(coinUid): return [.page: StatPage.coinPage.rawValue, .coinUid: coinUid] + case let .openSendTokenList(coinUid, chainUid): + var params: [StatParam: Any] = [.page: StatPage.sendTokenList.rawValue] + params[.coinUid] = coinUid + params[.chainUid] = chainUid + return params + case let .selectAppIcon(iconUid): return [.iconUid: iconUid] + case let .openArticle(relativeUrl): return [.relativeUrl: relativeUrl] + case let .rejectRequest(chainUid): return [.chainUid: chainUid] + case let .selectBalanceConversion(coinUid): return [.coinUid: coinUid] + case let .selectBalanceValue(type): return [.type: type] + case let .selectLaunchScreen(type): return [.type: type] + case let .selectTheme(type): return [.type: type] case let .openPlatform(chainUid): return [.page: StatPage.topPlatform.rawValue, .chainUid: chainUid] case let .openReceive(token): return params(token: token).merging([.page: StatPage.receive.rawValue]) { $1 } case let .openResend(chainUid, type): return [.page: StatPage.resend.rawValue, .chainUid: chainUid, .type: type] @@ -290,17 +357,23 @@ enum StatEvent { case let .scanQr(entity): return [.entity: entity.rawValue] case let .select(entity): return [.entity: entity.rawValue] case let .share(entity): return [.entity: entity.rawValue] + case let .showMarketsTab(shown): return [.shown: shown] + case let .showSignals(shown): return [.shown: shown] + case let .switchBaseCurrency(code: code): return [.currencyCode: code] case let .switchBtcSource(chainUid, type): return [.chainUid: chainUid, .type: type.rawValue] case let .switchChartPeriod(period): return [.period: period.rawValue] case let .switchEvmSource(chainUid, name): return [.chainUid: chainUid, .type: name] case let .switchField(field): return [.field: field.rawValue] case let .switchFilterType(type): return [.type: type] + case let .switchLanguage(language): return [.language: language] case let .switchMarketTop(marketTop): return [.marketTop: marketTop.rawValue] case let .switchPeriod(period): return [.period: period.rawValue] + case let .switchPriceChangeMode(priceChangeMode): return [.changeMode: priceChangeMode.statName] case let .switchSortType(sortType): return [.type: sortType.rawValue] case let .switchTab(tab): return [.tab: tab.rawValue] case let .switchTvlChain(chain): return [.tvlChain: chain] case let .toggleIndicators(shown): return [.shown: shown] + case let .toggleTvlField(field): return [.field: field] case let .watchWallet(walletType): return [.walletType: walletType] default: return nil } @@ -319,13 +392,19 @@ enum StatParam: String { case bitcoinCashCoinType = "bitcoin_cash_coin_type" case categoryUid = "category_uid" case chainUid = "chain_uid" + case currencyCode = "currency_code" case coinUid = "coin_uid" case derivation case entity case field + case hide + case iconUid = "icon_uid" + case language case marketTop = "market_top" case page case period + case changeMode = "change_mode" + case relativeUrl = "relative_url" case shown case tab case tvlChain = "tvl_chain" @@ -335,7 +414,7 @@ enum StatParam: String { enum StatTab: String { case markets, balance, transactions, settings - case overview, news, watchlist + case coins, overview, news, pairs, platforms, watchlist case analytics case all, incoming, outgoing, swap, approve } @@ -345,15 +424,22 @@ enum StatSortType: String { case name case priceChange = "price_change" + case manual case highestCap = "highest_cap" case lowestCap = "lowest_cap" case highestVolume = "highest_volume" case lowestVolume = "lowest_volume" case topGainers = "top_gainers" case topLosers = "top_losers" + + case highestAssets = "highest_assets" + case lowestAssets = "lowest_assets" + case inflow + case outflow } enum StatPeriod: String { + case hour24 = "24h" case day1 = "1d" case week1 = "1w" case week2 = "2w" @@ -375,25 +461,32 @@ enum StatField: String { enum StatMarketTop: String { case top100 case top200 + case top250 case top300 + case top500 + case top1000 + case top1500 } enum StatEntity: String { case account case address + case all case blockchain case cloudBackup = "cloud_backup" case contractAddress = "contract_address" case derivation case evmAddress = "evm_address" case evmPrivateKey = "evm_private_key" - case evmSyncSource = "evm_sync_source" case key case passphrase case receiveAddress = "receive_address" case recoveryPhrase = "recovery_phrase" + case session + case status case token case transactionId = "transaction_id" case wallet + case walletConnectPair = "wallet_connect_pair" case walletName = "wallet_name" } diff --git a/UnstoppableWallet/UnstoppableWallet/Models/TransactionDataSortMode.swift b/UnstoppableWallet/UnstoppableWallet/Models/TransactionDataSortMode.swift index 302d580eb3..92f40b26b2 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/TransactionDataSortMode.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/TransactionDataSortMode.swift @@ -1,4 +1,4 @@ -enum TransactionDataSortMode: String, CaseIterable { +enum TransactionDataSortMode: String, CaseIterable, Identifiable { case shuffle case bip69 @@ -9,4 +9,8 @@ enum TransactionDataSortMode: String, CaseIterable { var description: String { "btc_transaction_sort_mode.\(self).description".localized } + + var id: Self { + self + } } diff --git a/UnstoppableWallet/UnstoppableWallet/Models/TransactionValue.swift b/UnstoppableWallet/UnstoppableWallet/Models/TransactionValue.swift index f02866b60d..c911494ff9 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/TransactionValue.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/TransactionValue.swift @@ -82,12 +82,12 @@ enum TransactionValue { } } - func formattedFull(showSign: Bool = false) -> String? { + func formattedFull(signType: ValueFormatter.SignType = .never) -> String? { switch self { case let .coinValue(token, value): - return ValueFormatter.instance.formatFull(value: value, decimalCount: token.decimals, symbol: token.coin.code, showSign: showSign) + return ValueFormatter.instance.formatFull(value: value, decimalCount: token.decimals, symbol: token.coin.code, signType: signType) case let .tokenValue(_, tokenCode, tokenDecimals, value): - return ValueFormatter.instance.formatFull(value: value, decimalCount: tokenDecimals, symbol: tokenCode, showSign: showSign) + return ValueFormatter.instance.formatFull(value: value, decimalCount: tokenDecimals, symbol: tokenCode, signType: signType) case let .nftValue(_, value, _, tokenSymbol): return "\(value.sign == .plus ? "+" : "")\(value) \(tokenSymbol ?? "NFT")" case .rawValue: @@ -95,12 +95,12 @@ enum TransactionValue { } } - func formattedShort(showSign: Bool = false) -> String? { + func formattedShort(signType: ValueFormatter.SignType = .never) -> String? { switch self { case let .coinValue(token, value): - return ValueFormatter.instance.formatShort(value: value, decimalCount: token.decimals, symbol: token.coin.code, showSign: showSign) + return ValueFormatter.instance.formatShort(value: value, decimalCount: token.decimals, symbol: token.coin.code, signType: signType) case let .tokenValue(_, tokenCode, tokenDecimals, value): - return ValueFormatter.instance.formatShort(value: value, decimalCount: tokenDecimals, symbol: tokenCode, showSign: showSign) + return ValueFormatter.instance.formatShort(value: value, decimalCount: tokenDecimals, symbol: tokenCode, signType: signType) case let .nftValue(_, value, _, tokenSymbol): return "\(value.sign == .plus ? "+" : "")\(value) \(tokenSymbol ?? "NFT")" case .rawValue: diff --git a/UnstoppableWallet/UnstoppableWallet/Models/WatchlistSortBy.swift b/UnstoppableWallet/UnstoppableWallet/Models/WatchlistSortBy.swift new file mode 100644 index 0000000000..dc3fcfa1db --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Models/WatchlistSortBy.swift @@ -0,0 +1,7 @@ +enum WatchlistSortBy: String, CaseIterable { + case manual + case highestCap + case lowestCap + case gainers + case losers +} diff --git a/UnstoppableWallet/UnstoppableWallet/Models/WatchlistTimePeriod.swift b/UnstoppableWallet/UnstoppableWallet/Models/WatchlistTimePeriod.swift new file mode 100644 index 0000000000..dde3165ef0 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Models/WatchlistTimePeriod.swift @@ -0,0 +1,7 @@ +enum WatchlistTimePeriod: String, CaseIterable { + case hour24 = "24h" + case day1 = "1d" + case week1 = "1w" + case month1 = "1m" + case month3 = "3m" +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusView.swift index 178f520e14..8b1c70910b 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/AppStatus/AppStatusView.swift @@ -10,6 +10,7 @@ struct AppStatusView: View { VStack(spacing: .margin24) { HStack(spacing: .margin8) { Button(action: { + stat(page: .appStatus, event: .copy(entity: .status)) CopyHelper.copyAndNotify(value: viewModel.rawStatus) }) { Text("button.copy".localized) @@ -17,6 +18,7 @@ struct AppStatusView: View { .buttonStyle(PrimaryButtonStyle(style: .yellow)) Button(action: { + stat(page: .appStatus, event: .share(entity: .status)) shareText = viewModel.rawStatus }) { Text("button.share".localized) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift index d9f33e1ad6..d70f6ae206 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Backup/ICloud/AppBackupProvider.swift @@ -8,7 +8,7 @@ class AppBackupProvider { private let accountManager: AccountManager private let accountFactory: AccountFactory private let walletManager: WalletManager - private let favoritesManager: FavoritesManager + private let watchlistManager: WatchlistManager private let evmSyncSourceManager: EvmSyncSourceManager private let btcBlockchainManager: BtcBlockchainManager private let restoreSettingsManager: RestoreSettingsManager @@ -23,11 +23,13 @@ class AppBackupProvider { private let balanceConversionManager: BalanceConversionManager private let balanceHiddenManager: BalanceHiddenManager private let contactManager: ContactBookManager + private let priceChangeModeManager: PriceChangeModeManager + private let walletButtonHiddenManager: WalletButtonHiddenManager init(accountManager: AccountManager, accountFactory: AccountFactory, walletManager: WalletManager, - favoritesManager: FavoritesManager, + watchlistManager: WatchlistManager, evmSyncSourceManager: EvmSyncSourceManager, btcBlockchainManager: BtcBlockchainManager, restoreSettingsManager: RestoreSettingsManager, @@ -41,12 +43,14 @@ class AppBackupProvider { balancePrimaryValueManager: BalancePrimaryValueManager, balanceConversionManager: BalanceConversionManager, balanceHiddenManager: BalanceHiddenManager, - contactManager: ContactBookManager) + contactManager: ContactBookManager, + priceChangeModeManager: PriceChangeModeManager, + walletButtonHiddenManager: WalletButtonHiddenManager) { self.accountManager = accountManager self.accountFactory = accountFactory self.walletManager = walletManager - self.favoritesManager = favoritesManager + self.watchlistManager = watchlistManager self.evmSyncSourceManager = evmSyncSourceManager self.btcBlockchainManager = btcBlockchainManager self.restoreSettingsManager = restoreSettingsManager @@ -61,6 +65,8 @@ class AppBackupProvider { self.balanceConversionManager = balanceConversionManager self.balanceHiddenManager = balanceHiddenManager self.contactManager = contactManager + self.priceChangeModeManager = priceChangeModeManager + self.walletButtonHiddenManager = walletButtonHiddenManager } // Parts of backups @@ -98,8 +104,10 @@ class AppBackupProvider { baseCurrency: currencyManager.baseCurrency.code, mode: themeManager.themeMode, showMarketTab: launchScreenManager.showMarket, + priceChangeMode: priceChangeModeManager.priceChangeMode, launchScreen: launchScreenManager.launchScreen, conversionTokenQueryId: balanceConversionManager.conversionToken?.tokenQuery.id, + balanceHideButtons: walletButtonHiddenManager.buttonHidden, balancePrimaryValue: balancePrimaryValueManager.balancePrimaryValue, balanceAutoHide: balanceHiddenManager.balanceAutoHide, appIcon: appIconManager.appIcon.title @@ -125,7 +133,7 @@ class AppBackupProvider { let syncSources = EvmSyncSourceManager.SyncSourceBackup(selected: selected, custom: []) return RawFullBackup( accounts: accounts, - watchlistIds: favoritesManager.allCoinUids, + watchlistIds: watchlistManager.coinUids, contacts: contactManager.backupContactBook?.contacts ?? [], settings: settings(evmSyncSources: syncSources), customSyncSources: custom @@ -186,7 +194,7 @@ extension AppBackupProvider { for wallet in raw.accounts { restore(raws: [wallet]) } - favoritesManager.add(coinUids: raw.watchlistIds) + watchlistManager.add(coinUids: raw.watchlistIds) if !raw.contacts.isEmpty { try? contactManager.restore(contacts: raw.contacts, mergePolitics: .replace) @@ -204,8 +212,10 @@ extension AppBackupProvider { themeManager.themeMode = raw.settings.mode launchScreenManager.showMarket = raw.settings.showMarketTab launchScreenManager.launchScreen = raw.settings.launchScreen + priceChangeModeManager.priceChangeMode = raw.settings.priceChangeMode balancePrimaryValueManager.balancePrimaryValue = raw.settings.balancePrimaryValue + walletButtonHiddenManager.buttonHidden = raw.settings.balanceHideButtons balanceConversionManager.set(tokenQueryId: raw.settings.conversionTokenQueryId) balanceHiddenManager.set(balanceAutoHide: raw.settings.balanceAutoHide) let appIcon = AppIconManager.allAppIcons.first { $0.title == raw.settings.appIcon } ?? .main diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/BtcBlockchainSettings/BtcBlockchainSettingsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/BtcBlockchainSettings/BtcBlockchainSettingsView.swift index b1497e44cf..d01edb5aa4 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/BtcBlockchainSettings/BtcBlockchainSettingsView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/BtcBlockchainSettings/BtcBlockchainSettingsView.swift @@ -9,42 +9,44 @@ struct BtcBlockchainSettingsView: View { var body: some View { ThemeView { BottomGradientWrapper { - VStack(spacing: .margin32) { + ScrollView { VStack(spacing: .margin32) { - Text("btc_blockchain_settings.restore_source.description".localized) - .themeSubhead2() - .padding(EdgeInsets(top: 0, leading: .margin16, bottom: 0, trailing: .margin16)) + VStack(spacing: .margin32) { + Text("btc_blockchain_settings.restore_source.description".localized) + .themeSubhead2() + .padding(EdgeInsets(top: 0, leading: .margin16, bottom: 0, trailing: .margin16)) - ListSection { - ForEach(viewModel.restoreModes, id: \.restoreMode.id) { restoreMode in - ClickableRow(action: { - viewModel.selectedRestoreMode = restoreMode.restoreMode - }) { - switch restoreMode.icon { - case let .local(name): - Image(name) - case let .remote(url): - KFImage.url(URL(string: url)) - .resizable() - .frame(width: .iconSize32, height: .iconSize32) - } + ListSection { + ForEach(viewModel.restoreModes, id: \.restoreMode.id) { restoreMode in + ClickableRow(action: { + viewModel.selectedRestoreMode = restoreMode.restoreMode + }) { + switch restoreMode.icon { + case let .local(name): + Image(name) + case let .remote(url): + KFImage.url(URL(string: url)) + .resizable() + .frame(width: .iconSize32, height: .iconSize32) + } - VStack(spacing: 1) { - Text(restoreMode.title).themeBody() - Text(restoreMode.description).themeSubhead2() - } + VStack(spacing: 1) { + Text(restoreMode.title).themeBody() + Text(restoreMode.description).themeSubhead2() + } - if restoreMode.restoreMode == viewModel.selectedRestoreMode { - Image.checkIcon + if restoreMode.restoreMode == viewModel.selectedRestoreMode { + Image.checkIcon + } } } } } - } - HighlightedTextView(text: "btc_blockchain_settings.restore_source.alert".localized(viewModel.title)) + HighlightedTextView(text: "btc_blockchain_settings.restore_source.alert".localized(viewModel.title)) + } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) } - .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) } bottomContent: { Button(action: { viewModel.onTapSave() diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Cex/CexWithdraw/CexWithdrawViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Cex/CexWithdraw/CexWithdrawViewController.swift index 272606ba68..1babc95e9e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Cex/CexWithdraw/CexWithdrawViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Cex/CexWithdraw/CexWithdrawViewController.swift @@ -56,7 +56,7 @@ class CexWithdrawViewController: ThemeViewController, ICexWithdrawNetworkSelectD iconImageView.snp.makeConstraints { make in make.size.equalTo(CGFloat.iconSize24) } - iconImageView.setImage(withUrlString: viewModel.coinImageUrl, placeholder: nil) + iconImageView.setImage(coin: viewModel.coin) view.addSubview(tableView) tableView.snp.makeConstraints { maker in diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Cex/CexWithdraw/CexWithdrawViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Cex/CexWithdraw/CexWithdrawViewModel.swift index 137f4ed3e3..b80a24bd8f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Cex/CexWithdraw/CexWithdrawViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Cex/CexWithdraw/CexWithdrawViewModel.swift @@ -2,6 +2,7 @@ import BigInt import Combine import Foundation import HsExtensions +import MarketKit import RxCocoa import RxSwift @@ -65,8 +66,8 @@ extension CexWithdrawViewModel { service.cexAsset.coinCode } - var coinImageUrl: String { - service.cexAsset.coin?.imageUrl ?? "" + var coin: Coin? { + service.cexAsset.coin } var selectedNetworkIndex: Int? { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartUiView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartUiView.swift index 522e566005..293ed69bbe 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartUiView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartUiView.swift @@ -223,11 +223,21 @@ class ChartUiView: UIView { CGSize(width: UIView.noIntrinsicMetric, height: totalHeight) } + func timePeriod(show: Bool) { + timePeriodView.isHidden = !show + timePeriodView.snp.updateConstraints { maker in + maker.height.equalTo(show ? CGFloat.heightCell48 : 0) + } + + updateConstraints() + } + var totalHeight: CGFloat { .heightDoubleLineCell + configuration.mainHeight + (configuration.showIndicatorArea ? configuration.indicatorHeight : 0) - + .margin8 + .heightCell48 + .margin8 + + (timePeriodView.isHidden ? 0 : .heightCell48) + + .margin8 } private func updateIntervals() { @@ -235,6 +245,8 @@ class ChartUiView: UIView { if viewModel.showAll { viewItems.append(.item(title: "chart.time_duration.all".localized)) } + + timePeriod(show: !viewItems.isEmpty) timePeriodView.reload(filters: viewItems) } @@ -284,6 +296,14 @@ class ChartUiView: UIView { } else { currentSecondaryDiffLabel.isHidden = true } + case let .custom(title, value): + currentSecondaryTitleLabel.isHidden = false + currentSecondaryValueLabel.isHidden = false + currentSecondaryDiffLabel.isHidden = true + + currentSecondaryTitleLabel.text = title + currentSecondaryValueLabel.text = value + currentSecondaryValueLabel.textColor = .themeLeah } if !chartView.isPressed { @@ -349,6 +369,14 @@ class ChartUiView: UIView { } else { chartSecondaryDiffLabel.isHidden = true } + case let .custom(title, value): + chartSecondaryTitleLabel.isHidden = false + chartSecondaryValueLabel.isHidden = false + chartSecondaryDiffLabel.isHidden = true + + chartSecondaryTitleLabel.text = title + chartSecondaryValueLabel.text = value + chartSecondaryValueLabel.textColor = .themeLeah case let .indicators(top, bottom): chartSecondaryTitleLabel.isHidden = false chartSecondaryValueLabel.isHidden = false diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Chart/MarketCards/MarketWideCardCell.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Chart/MarketCards/MarketWideCardCell.swift index 9348474f23..392cfb2df8 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Chart/MarketCards/MarketWideCardCell.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Chart/MarketCards/MarketWideCardCell.swift @@ -90,7 +90,7 @@ class MarketWideCardCell: BaseSelectableThemeCell { let chartConfiguration: ChartConfiguration switch chartCurveType { case .line: chartConfiguration = .previewChart - case .bars: chartConfiguration = .previewBarChart + case .bars, .histogram: chartConfiguration = .previewBarChart } showChartView(configuration: chartConfiguration) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsViewController.swift index 3ce9b747a9..89e2a950df 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsViewController.swift @@ -375,7 +375,7 @@ class CoinAnalyticsViewController: ThemeViewController { } private func openTvlRank() { - let viewController = MarketGlobalMetricModule.tvlInDefiViewController() + let viewController = MarketTvlView().toViewController() parentNavigationController?.pushViewController(viewController, animated: true) } @@ -385,7 +385,7 @@ class CoinAnalyticsViewController: ThemeViewController { } private func openTreasuries() { - let viewController = CoinTreasuriesModule.viewController(coin: viewModel.coin) + let viewController = CoinTreasuriesView(coin: viewModel.coin).toViewController(title: "coin_analytics.treasuries".localized) parentNavigationController?.pushViewController(viewController, animated: true) } @@ -404,8 +404,8 @@ class CoinAnalyticsViewController: ThemeViewController { parentNavigationController?.present(viewController, animated: true) } - private func openRanks(type: CoinRankModule.RankType) { - let viewController = CoinRankModule.viewController(type: type) + private func openRanks(type: RankViewModel.RankType) { + let viewController = RankView(type: type).toViewController() parentNavigationController?.present(viewController, animated: true) } @@ -475,7 +475,7 @@ extension CoinAnalyticsViewController: SectionsDataSource { switch chartCurveType { case .line: chartTrend = viewItem.chartTrend - case .bars: chartTrend = .neutral + case .bars, .histogram: chartTrend = .neutral } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/TechnicalIndicators/CoinIndicatorViewItemFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/TechnicalIndicators/CoinIndicatorViewItemFactory.swift index 0739f727d8..d40c5d48ad 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/TechnicalIndicators/CoinIndicatorViewItemFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/TechnicalIndicators/CoinIndicatorViewItemFactory.swift @@ -87,7 +87,7 @@ extension CoinIndicatorViewItemFactory { rsiLine = "70%" } - let rsiValue = technicalAdvice.rsi.flatMap { ValueFormatter.instance.format(percentValue: $0, showSign: false) } + let rsiValue = technicalAdvice.rsi.flatMap { ValueFormatter.instance.format(percentValue: $0, signType: .never) } let signalTimeString = technicalAdvice.signalTimestamp.flatMap { let date = DateHelper.instance.formatShortDateOnly(date: Date(timeIntervalSince1970: $0)) return "technical_advice.over.indicators.signal_date".localized(date) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChart/ChartModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChart/ChartModule.swift index 1bde6072de..56e4ee1d30 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChart/ChartModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChart/ChartModule.swift @@ -12,18 +12,18 @@ enum ChartModule { let chartData: ChartData let indicators: [ChartIndicator] let chartTrend: MovementTrend - let chartDiff: Decimal? + let chartDiff: ValueDiff? let limitFormatter: ((Decimal) -> String?)? } struct SelectedPointViewItem { let value: String? - let diff: Decimal? + let diff: ValueDiff? let date: String let rightSideMode: RightSideMode - init(value: String?, diff: Decimal? = nil, date: String, rightSideMode: RightSideMode) { + init(value: String?, diff: ValueDiff? = nil, date: String, rightSideMode: RightSideMode) { self.value = value self.diff = diff self.date = date @@ -35,6 +35,7 @@ enum ChartModule { case none case volume(value: String?) case dominance(value: Decimal?, diff: Decimal?) + case custom(title: String, value: String?) case indicators(top: NSAttributedString?, bottom: NSAttributedString?) } } @@ -55,6 +56,11 @@ enum MovementTrend { } } +struct ValueDiff { + let value: String + let trend: MovementTrend +} + protocol IChartViewModel { var showAll: Bool { get } var intervals: [String] { get } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChart/CoinChartService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChart/CoinChartService.swift index 4b8b4f807c..eb2955ed1c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChart/CoinChartService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChart/CoinChartService.swift @@ -75,7 +75,7 @@ class CoinChartService { self.indicatorRepository = indicatorRepository self.coinUid = coinUid - periodType = .byCustomPoints(.day1, indicatorRepository.extendedPointCount) + periodType = .byCustomPoints(App.shared.priceChangeModeManager.day1Period, indicatorRepository.extendedPointCount) indicatorRepository.updatedPublisher .sink { [weak self] in self?.fetchWithUpdatedIndicators() @@ -122,7 +122,8 @@ class CoinChartService { let item = Item( coinUid: coinUid, rate: coinPrice.value, - rateDiff24h: coinPrice.diff, + rateDiff24h: coinPrice.diff24h, + rateDiff1d: coinPrice.diff1d, timestamp: coinPrice.timestamp, chartPointsItem: chartPointsItem, indicators: indicatorRepository.indicators, @@ -182,7 +183,7 @@ extension CoinChartService { func start() { coinPrice = marketKit.coinPrice(coinUid: coinUid, currencyCode: currency.code) - marketKit.coinPricePublisher(tag: "coin-chart-service", coinUid: coinUid, currencyCode: currency.code) + marketKit.coinPricePublisher(coinUid: coinUid, currencyCode: currency.code) .sink { [weak self] coinPrice in self?.coinPrice = coinPrice self?.syncState() @@ -223,6 +224,7 @@ extension CoinChartService { let coinUid: String let rate: Decimal let rateDiff24h: Decimal? + let rateDiff1d: Decimal? let timestamp: TimeInterval let chartPointsItem: ChartPointsItem let indicators: [ChartIndicator] diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChart/CoinChartViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChart/CoinChartViewModel.swift index f574646087..dea71f46bc 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChart/CoinChartViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChart/CoinChartViewModel.swift @@ -26,7 +26,7 @@ class CoinChartViewModel: ObservableObject { @Published private(set) var indicatorsShown: Bool var intervals: [String] { - service.validIntervals.map(\.title) + service.validIntervals.map(\.shortTitle) } init(service: CoinChartService, factory: CoinChartFactory) { @@ -168,22 +168,6 @@ extension CoinChartViewModel: IChartViewTouchDelegate { } } -extension HsTimePeriod { - var title: String { - switch self { - case .day1: return "chart.time_duration.day".localized - case .week1: return "chart.time_duration.week".localized - case .week2: return "chart.time_duration.week2".localized - case .month1: return "chart.time_duration.month".localized - case .month3: return "chart.time_duration.month3".localized - case .month6: return "chart.time_duration.halfyear".localized - case .year1: return "chart.time_duration.year".localized - case .year2: return "chart.time_duration.year2".localized - case .year5: return "chart.time_duration.year5".localized - } - } -} - extension [HsTimePeriod] { var periodTypes: [HsPeriodType] { map { .byPeriod($0) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChartFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChartFactory.swift index 2504427c04..10b36c3487 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChartFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChartFactory.swift @@ -22,21 +22,30 @@ class CoinChartFactory { points.append(point) lastPoint = point - // for daily chart we need change oldest visible point to 24h back timestamp-same point - if periodType.in([.day1]), let rateDiff24 = item.rateDiff24h { + var pointToPrepend: ChartPoint? + + if periodType.in([.hour24]), let rateDiff24 = item.rateDiff24h { + // for 24h chart we need change oldest visible point to 24h back timestamp-same point let timestamp = item.timestamp - 24 * 60 * 60 let value = 100 * item.rate / (100 + rateDiff24) - let point = ChartPoint(timestamp: timestamp, value: value) + pointToPrepend = ChartPoint(timestamp: timestamp, value: value) + } else if periodType.in([.day1]), let rateDiff1d = item.rateDiff1d { + // for 1day chart we need change oldest visible point to 24h back timestamp-same point + let value = 100 * item.rate / (100 + rateDiff1d) + + pointToPrepend = ChartPoint(timestamp: TimeInterval.midnightUTC(), value: value) + } - if let index = points.firstIndex(where: { $0.timestamp > timestamp }) { - points.insert(point, at: index) + if let pointToPrepend { + if let index = points.firstIndex(where: { $0.timestamp > pointToPrepend.timestamp }) { + points.insert(pointToPrepend, at: index) if index > 0 { points.remove(at: index - 1) } } - firstPoint = point + firstPoint = pointToPrepend } } @@ -51,6 +60,9 @@ class CoinChartFactory { } let chartData = ChartData(items: items, startWindow: firstPoint.timestamp, endWindow: lastPoint.timestamp) + let diff = (lastPoint.value - firstPoint.value) / firstPoint.value * 100 + let diffString = ValueFormatter.instance.format(percentValue: diff, signType: .always) + let valueDiff = diffString.map { ValueDiff(value: $0, trend: diff.isSignMinus ? .down : .up) } return ChartModule.ViewItem( value: ValueFormatter.instance.formatFull(currencyValue: CurrencyValue(currency: currency, value: item.rate)), valueDescription: nil, @@ -58,7 +70,7 @@ class CoinChartFactory { chartData: chartData, indicators: item.indicators, chartTrend: lastPoint.value > firstPoint.value ? .up : .down, - chartDiff: (lastPoint.value - firstPoint.value) / firstPoint.value * 100, + chartDiff: valueDiff, limitFormatter: { value in ValueFormatter.instance.formatFull(currency: currency, value: value) } ) } @@ -113,7 +125,7 @@ class CoinChartFactory { paragraphStyle.alignment = NSTextAlignment.right for (index, pair) in maPairs.enumerated() { - let formatted = ValueFormatter.instance.formatFull(value: pair.0, decimalCount: 8, showSign: pair.0 < 0) + let formatted = ValueFormatter.instance.formatFull(value: pair.0, decimalCount: 8, signType: pair.0 < 0 ? .always : .never) topLineString.append(NSAttributedString(string: formatted ?? "", attributes: [.foregroundColor: pair.1.withAlphaComponent(1), .paragraphStyle: paragraphStyle])) if index < maPairs.count - 1 { topLineString.append(NSAttributedString(string: " ")) @@ -125,7 +137,7 @@ class CoinChartFactory { case let rsi as RsiIndicator: let value = chartItem.indicators[rsi.json] let formatted = value.flatMap { - ValueFormatter.instance.formatFull(value: $0, decimalCount: 2, showSign: $0 < 0) + ValueFormatter.instance.formatFull(value: $0, decimalCount: 2, signType: $0 < 0 ? .always : .never) } bottomLineString.append(NSAttributedString(string: formatted ?? "", attributes: [.foregroundColor: rsi.configuration.color.value.withAlphaComponent(1), .paragraphStyle: paragraphStyle])) case let macd as MacdIndicator: @@ -145,7 +157,7 @@ class CoinChartFactory { pairs.append((macdValue, macd.configuration.longColor.value)) } for (index, pair) in pairs.enumerated() { - let formatted = ValueFormatter.instance.formatFull(value: pair.0, decimalCount: 8, showSign: pair.0 < 0) + let formatted = ValueFormatter.instance.formatFull(value: pair.0, decimalCount: 8, signType: pair.0 < 0 ? .always : .never) bottomLineString.append(NSAttributedString(string: formatted ?? "", attributes: [.foregroundColor: pair.1.withAlphaComponent(1), .paragraphStyle: paragraphStyle])) if index < pairs.count - 1 { bottomLineString.append(NSAttributedString(string: " ")) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinMarkets/CoinMarketsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinMarkets/CoinMarketsView.swift index 8e120d415c..5ca4c4e3e2 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinMarkets/CoinMarketsView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinMarkets/CoinMarketsView.swift @@ -5,8 +5,6 @@ import SwiftUI struct CoinMarketsView: View { @StateObject private var viewModel: CoinMarketsViewModel - @State private var hasAppeared = false - init(coin: Coin) { _viewModel = StateObject(wrappedValue: CoinMarketsViewModel(coin: coin)) } @@ -28,10 +26,7 @@ struct CoinMarketsView: View { } } } - .onAppear { - guard !hasAppeared else { return } - hasAppeared = true - + .onFirstAppear { viewModel.onFirstAppear() } } @@ -52,7 +47,7 @@ struct CoinMarketsView: View { .padding(.vertical, .margin8) ScrollViewReader { proxy in - ThemeList(items: viewItems) { viewItem in + ThemeList(viewItems, bottomSpacing: .margin16) { viewItem in if let tradeUrl = viewItem.tradeUrl { ClickableRow(action: { UrlManager.open(url: tradeUrl) @@ -66,7 +61,6 @@ struct CoinMarketsView: View { } } } - .themeListStyle(.transparent) .onChange(of: viewModel.filterTypeInfo) { _ in withAnimation { proxy.scrollTo(viewItems.first!) @@ -114,8 +108,8 @@ struct CoinMarketsView: View { Spacer() - if let volumeUsdt = viewItem.volumeUsdt { - Text(volumeUsdt) + if let fiatVolume = viewItem.fiatVolume { + Text(fiatVolume) .font(.themeSubhead2) .foregroundColor(.themeGray) .lineLimit(1) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinMarkets/CoinMarketsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinMarkets/CoinMarketsViewModel.swift index 273a9ecf38..5c9f983d41 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinMarkets/CoinMarketsViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinMarkets/CoinMarketsViewModel.swift @@ -26,7 +26,6 @@ class CoinMarketsViewModel: ObservableObject { } @Published var filterTypeInfo = SelectorButtonInfo(text: "", count: 0, selectedIndex: 0) - @Published var volumeTypeInfo = SelectorButtonInfo(text: "", count: 0, selectedIndex: 0) init(coin: Coin) { self.coin = coin @@ -41,9 +40,9 @@ class CoinMarketsViewModel: ObservableObject { state = .loading } - Task { [weak self, marketKit, coin] in + Task { [weak self, marketKit, coin, currency] in do { - let tickers = try await marketKit.marketTickers(coinUid: coin.uid) + let tickers = try await marketKit.marketTickers(coinUid: coin.uid, currencyCode: currency.code) self?.tickers = tickers self?.syncState() } catch { @@ -68,39 +67,26 @@ class CoinMarketsViewModel: ObservableObject { filteredTickers = tickers.filter(\.verified) } - let sortedTickers = filteredTickers.sorted { $0.volume > $1.volume } - let price = marketKit.coinPrice(coinUid: coin.uid, currencyCode: currency.code)?.value - let viewItems = sortedTickers.map { viewItem(ticker: $0, price: price) } + let sortedTickers = filteredTickers.sorted { $0.fiatVolume > $1.fiatVolume } + let viewItems = sortedTickers.map { viewItem(ticker: $0) } DispatchQueue.main.async { [weak self] in self?.state = .loaded(viewItems: viewItems) } } - private func viewItem(ticker: MarketTicker, price: Decimal?) -> ViewItem { + private func viewItem(ticker: MarketTicker) -> ViewItem { ViewItem( market: ticker.marketName, marketImageUrl: ticker.marketImageUrl, - pair: "\(coin.code) / \(ticker.target)", - volume: volume(volumeType: .coin, value: ticker.volume, price: price), - volumeUsdt: volume(volumeType: .currency, value: ticker.volume, price: price), + pair: "\(ticker.base) / \(ticker.target)", + volume: ValueFormatter.instance.formatShort(value: ticker.volume, decimalCount: 8, symbol: ticker.base), + fiatVolume: ValueFormatter.instance.formatShort(currency: currency, value: ticker.fiatVolume), tradeUrl: ticker.tradeUrl, verified: ticker.verified ) } - private func volume(volumeType: VolumeType, value: Decimal, price: Decimal?) -> String? { - switch volumeType { - case .coin: - return ValueFormatter.instance.formatShort(value: value, decimalCount: 8, symbol: coin.code) - case .currency: - guard let price else { - return "n/a".localized - } - return ValueFormatter.instance.formatShort(currency: currency, value: value * price) - } - } - private func syncFilterTypeInfo() { let text: String @@ -142,17 +128,12 @@ extension CoinMarketsViewModel { case verified } - private enum VolumeType: String, CaseIterable { - case coin - case currency - } - struct ViewItem: Hashable { let market: String let marketImageUrl: String? let pair: String let volume: String? - let volumeUsdt: String? + let fiatVolume: String? let tradeUrl: String? let verified: Bool diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewView.swift index 7daa4e35c7..5229414738 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewView.swift @@ -27,12 +27,7 @@ struct CoinOverviewView: View { ScrollView { VStack(spacing: 0) { HStack(spacing: .margin16) { - KFImage.url(URL(string: coin.imageUrl)) - .resizable() - .placeholder { - Circle().fill(Color.themeSteel20) - } - .frame(width: .iconSize32, height: .iconSize32) + CoinIconView(coin: coin) Text(coin.name).themeBody() diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewController.swift index 8ae3320662..280840b70e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewController.swift @@ -225,8 +225,8 @@ extension CoinOverviewViewController { rows: [ tableView.universalRow56( id: "coin-info", - image: .url(viewItem.imageUrl, placeholder: viewItem.imagePlaceholderName), - title: .body(viewItem.name, color: .themeGray), + image: .url(viewItem.coin), + title: .body(viewItem.coin.name, color: .themeGray), value: .subhead1(viewItem.marketCapRank, color: .themeGray), backgroundStyle: .transparent, isFirst: true, diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewItemFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewItemFactory.swift index c866ff440a..cf95a77e0a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewItemFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewItemFactory.swift @@ -12,7 +12,8 @@ class CoinOverviewViewItemFactory { private func roiTitle(timePeriod: HsTimePeriod) -> String { switch timePeriod { - case .day1: return "coin_overview.roi.hour24".localized + case .hour24: return "coin_overview.roi.hour24".localized + case .day1: return "coin_overview.roi.day1".localized case .week1: return "coin_overview.roi.day7".localized case .week2: return "coin_overview.roi.day14".localized case .month1: return "coin_overview.roi.day30".localized @@ -211,10 +212,8 @@ extension CoinOverviewViewItemFactory { return CoinOverviewViewModel.ViewItem( coinViewItem: CoinOverviewViewModel.CoinViewItem( - name: coin.name, marketCapRank: marketCapRank, - imageUrl: coin.imageUrl, - imagePlaceholderName: "placeholder_circle_32" + coin: coin ), marketCapRank: marketCapRank, diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewModel.swift index ead2f4da40..7dab7f575b 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewModel.swift @@ -103,10 +103,8 @@ extension CoinOverviewViewModel { extension CoinOverviewViewModel { struct CoinViewItem { - let name: String let marketCapRank: String? - let imageUrl: String - let imagePlaceholderName: String + let coin: Coin } struct ViewItem { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageModule.swift index 5cc0d82525..f6904bbcd8 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageModule.swift @@ -5,7 +5,7 @@ import UIKit enum CoinPageModule { static func view(fullCoin: FullCoin) -> some View { - let viewModel = CoinPageViewModelNew(fullCoin: fullCoin, favoritesManager: App.shared.favoritesManager) + let viewModel = CoinPageViewModelNew(fullCoin: fullCoin, watchlistManager: App.shared.watchlistManager) let overviewView = CoinOverviewModule.view(coinUid: fullCoin.coin.uid) let analyticsView = CoinAnalyticsModule.view(fullCoin: fullCoin) @@ -26,7 +26,7 @@ enum CoinPageModule { let service = CoinPageService( fullCoin: fullCoin, - favoritesManager: App.shared.favoritesManager + watchlistManager: App.shared.watchlistManager ) let viewModel = CoinPageViewModel(service: service) @@ -64,3 +64,15 @@ extension CoinPageModule { } } } + +struct CoinPageViewNew: UIViewControllerRepresentable { + typealias UIViewControllerType = UIViewController + + let coinUid: String + + func makeUIViewController(context _: Context) -> UIViewController { + CoinPageModule.viewController(coinUid: coinUid) ?? UIViewController() + } + + func updateUIViewController(_: UIViewController, context _: Context) {} +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageService.swift index 0f133c66cd..cb45987411 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageService.swift @@ -1,3 +1,4 @@ +import Combine import Foundation import MarketKit import RxRelay @@ -5,8 +6,8 @@ import RxSwift class CoinPageService { let fullCoin: FullCoin - private let favoritesManager: FavoritesManager - private let disposeBag = DisposeBag() + private let watchlistManager: WatchlistManager + private var cancellables = Set() private let favoriteRelay = PublishRelay() private(set) var favorite: Bool = false { @@ -17,17 +18,19 @@ class CoinPageService { } } - init(fullCoin: FullCoin, favoritesManager: FavoritesManager) { + init(fullCoin: FullCoin, watchlistManager: WatchlistManager) { self.fullCoin = fullCoin - self.favoritesManager = favoritesManager + self.watchlistManager = watchlistManager - subscribe(disposeBag, favoritesManager.coinUidsUpdatedObservable) { [weak self] in self?.syncFavorite() } + watchlistManager.coinUidsPublisher + .sink { [weak self] _ in self?.syncFavorite() } + .store(in: &cancellables) syncFavorite() } private func syncFavorite() { - favorite = favoritesManager.isFavorite(coinUid: fullCoin.coin.uid) + favorite = watchlistManager.isWatched(coinUid: fullCoin.coin.uid) } } @@ -40,10 +43,10 @@ extension CoinPageService { let coinUid = fullCoin.coin.uid if favorite { - favoritesManager.remove(coinUid: coinUid) + watchlistManager.remove(coinUid: coinUid) stat(page: .coinPage, event: .addToWatchlist(coinUid: coinUid)) } else { - favoritesManager.add(coinUid: coinUid) + watchlistManager.add(coinUid: coinUid) stat(page: .coinPage, event: .removeFromWatchlist(coinUid: coinUid)) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageViewModelNew.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageViewModelNew.swift index efdbd0b49a..2e16640f5a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageViewModelNew.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinPageViewModelNew.swift @@ -3,22 +3,22 @@ import MarketKit class CoinPageViewModelNew: ObservableObject { let fullCoin: FullCoin - private let favoritesManager: FavoritesManager + private let watchlistManager: WatchlistManager @Published var isFavorite: Bool { didSet { if isFavorite { - favoritesManager.add(coinUid: fullCoin.coin.uid) + watchlistManager.add(coinUid: fullCoin.coin.uid) } else { - favoritesManager.remove(coinUid: fullCoin.coin.uid) + watchlistManager.remove(coinUid: fullCoin.coin.uid) } } } - init(fullCoin: FullCoin, favoritesManager: FavoritesManager) { + init(fullCoin: FullCoin, watchlistManager: WatchlistManager) { self.fullCoin = fullCoin - self.favoritesManager = favoritesManager + self.watchlistManager = watchlistManager - isFavorite = favoritesManager.isFavorite(coinUid: fullCoin.coin.uid) + isFavorite = watchlistManager.isWatched(coinUid: fullCoin.coin.uid) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Rank/RankView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Rank/RankView.swift new file mode 100644 index 0000000000..81f2ccd678 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Rank/RankView.swift @@ -0,0 +1,194 @@ +import Kingfisher +import MarketKit +import SwiftUI + +struct RankView: View { + @StateObject var viewModel: RankViewModel + @StateObject var watchlistViewModel: WatchlistViewModel + + @Environment(\.presentationMode) private var presentationMode + + @State private var presentedCoin: Coin? + @State private var timePeriodSelectorPresented = false + + init(type: RankViewModel.RankType) { + _viewModel = StateObject(wrappedValue: RankViewModel(type: type)) + _watchlistViewModel = StateObject(wrappedValue: WatchlistViewModel(page: type.statRankType)) + } + + var body: some View { + ThemeNavigationView { + ThemeView { + switch viewModel.state { + case .loading: + VStack(spacing: 0) { + header() + Spacer() + ProgressView() + Spacer() + } + case let .loaded(items): + ScrollViewReader { proxy in + ThemeList(bottomSpacing: .margin16, invisibleTopView: true) { + header() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + + list(items: items) + } + .onChange(of: viewModel.sortOrder) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + .onChange(of: viewModel.timePeriod) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + } + case .failed: + VStack(spacing: 0) { + header() + + SyncErrorView { + viewModel.sync() + } + } + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("button.close".localized) { + presentationMode.wrappedValue.dismiss() + } + } + } + .sheet(item: $presentedCoin) { coin in + CoinPageViewNew(coinUid: coin.uid).ignoresSafeArea() + .onFirstAppear { stat(page: viewModel.type.statRankType, event: .openCoin(coinUid: coin.uid)) } + } + } + } + + @ViewBuilder private func header() -> some View { + VStack(spacing: 0) { + HStack(spacing: .margin32) { + VStack(spacing: .margin8) { + Text(viewModel.type.title.localized).themeHeadline1() + Text(viewModel.type.description.localized).themeSubhead2() + } + .padding(.vertical, .margin12) + + KFImage.url(URL(string: viewModel.type.imageUid.headerImageUrl)) + .resizable() + .frame(width: 76, height: 108) + } + .padding(.leading, .margin16) + + Rectangle() + .fill(Color.themeSteel10) + .frame(height: .heightOneDp) + .frame(maxWidth: .infinity) + } + } + + @ViewBuilder private func listHeader(disabled: Bool = false) -> some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + Button(action: { + viewModel.sortOrder.toggle() + }) { + Text(viewModel.type.sortingField.localized) + } + .buttonStyle(SecondaryButtonStyle(style: .default, rightAccessory: .custom(image: sortIcon()))) + .disabled(disabled) + + if viewModel.timePeriods.count > 1 { + Button(action: { + timePeriodSelectorPresented = true + }) { + Text(viewModel.timePeriod.shortTitle) + } + .buttonStyle(SecondaryButtonStyle(style: .default, rightAccessory: .dropDown)) + .disabled(disabled) + } + } + .padding(.horizontal, .margin16) + .padding(.vertical, .margin8) + } + .alert( + isPresented: $timePeriodSelectorPresented, + title: "market.time_period.title".localized, + viewItems: viewModel.timePeriods.map { .init(text: $0.title, selected: viewModel.timePeriod == $0) }, + onTap: { index in + guard let index else { + return + } + + viewModel.timePeriod = viewModel.timePeriods[index] + } + ) + } + + @ViewBuilder private func list(items: [RankViewModel.Item]) -> some View { + Section { + ListForEach(items) { item in + let coin = item.coin + + ClickableRow(action: { + presentedCoin = item.coin + }) { + itemContent( + index: item.index, + coin: coin, + value: item.value + ) + } + .watchlistSwipeActions(viewModel: watchlistViewModel, coinUid: coin.uid) + } + } header: { + listHeader() + .listRowInsets(EdgeInsets()) + .background(Color.themeTyler) + } + } + + @ViewBuilder private func loadingList() -> some View { + Section { + ListForEach(Array(0 ... 10)) { index in + ListRow { + itemContent( + index: 1, + coin: nil, + value: 12345.45 + ) + .redacted() + } + } + } header: { + listHeader(disabled: true) + .listRowInsets(EdgeInsets()) + .background(Color.themeTyler) + } + } + + @ViewBuilder private func itemContent(index: Int, coin: Coin?, value: Decimal) -> some View { + Text(index.description) + .textCaptionSB() + .frame(minWidth: 24, alignment: .center) + + CoinIconView(coin: coin) + + VStack(alignment: .leading, spacing: 1) { + Text(coin?.code ?? "CODE").textBody() + Text(coin?.name ?? "COIN NAME").textSubhead2() + } + + Spacer() + if let formatted = ValueFormatter.instance.formatShort(currency: viewModel.currency, value: value) { + Text(formatted).textBody() + } + } + + private func sortIcon() -> Image { + switch viewModel.sortOrder { + case .asc: return Image("arrow_medium_2_up_20") + case .desc: return Image("arrow_medium_2_down_20") + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Rank/RankViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Rank/RankViewModel.swift new file mode 100644 index 0000000000..aba377411b --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Rank/RankViewModel.swift @@ -0,0 +1,246 @@ +import Combine +import Foundation +import HsExtensions +import MarketKit + +class RankViewModel: ObservableObject { + private let marketKit = App.shared.marketKit + private let currencyManager = App.shared.currencyManager + let type: RankType + + private var cancellables = Set() + private var tasks = Set() + + private var internalState: InternalState = .loading { + didSet { + syncState() + } + } + + @Published var state: State = .loading + + var sortOrder: MarketModule.SortOrder = .desc { + didSet { + stat(page: type.statRankType, event: .toggleSortDirection) + syncState() + } + } + + var timePeriod: HsTimePeriod = .month1 { + didSet { + stat(page: type.statRankType, event: .switchPeriod(period: timePeriod.statPeriod)) + syncState() + } + } + + init(type: RankType) { + self.type = type + + currencyManager.$baseCurrency + .sink { [weak self] _ in + self?.sync() + } + .store(in: &cancellables) + + sync() + } + + private func syncState() { + switch internalState { + case .loading: + state = .loading + case let .loaded(items): + let items = items.compactMap { item in + switch item { + case let .single(coin, single): return single.value.map { (coin: coin, value: $0) } + case let .multi(coin, multi): return multi.value(timePeriod: timePeriod).map { (coin: coin, value: $0) } + } + } + + let filteredItems = items.sorted { $0.value > $1.value }.prefix(300) + let indexedItems = filteredItems.enumerated().map { index, item in + Item(index: index + 1, coin: item.coin, value: item.value) + } + + let sortedItems = sortOrder.isAsc ? indexedItems.sorted { $0.value < $1.value } : indexedItems + + state = .loaded(items: sortedItems) + case let .failed(error): + state = .failed(error: error) + } + } +} + +extension RankViewModel { + var currency: Currency { + currencyManager.baseCurrency + } + + var timePeriods: [HsTimePeriod] { + switch type { + case .dexLiquidity, .holders: return [.month1] + default: return [.day1, .week1, .month1] + } + } + + func sync() { + tasks = Set() + + if case .failed = internalState { + internalState = .loading + } + + let code = currency.code + + Task { [weak self, marketKit, type] in + do { + let values: [Value] + let coins = try marketKit.allCoins() + + var coinMap = [String: Coin]() + coins.forEach { coinMap[$0.uid] = $0 } + + let multiMap: (RankMultiValue) -> Value? = { multi in + coinMap[multi.uid].map { .multi(coin: $0, value: multi) } + } + + let singleMap: (RankValue) -> Value? = { value in + coinMap[value.uid].map { .single(coin: $0, value: value) } + } + + switch type { + case .cexVolume: values = try await marketKit.cexVolumeRanks(currencyCode: code).compactMap { multiMap($0) } + case .dexVolume: values = try await marketKit.dexVolumeRanks(currencyCode: code).compactMap { multiMap($0) } + case .dexLiquidity: values = try await marketKit.dexLiquidityRanks().compactMap { singleMap($0) } + case .address: values = try await marketKit.activeAddressRanks().compactMap { multiMap($0) } + case .txCount: values = try await marketKit.transactionCountRanks().compactMap { multiMap($0) } + case .holders: values = try await marketKit.holdersRanks().compactMap { singleMap($0) } + case .fee: values = try await marketKit.feeRanks(currencyCode: code).compactMap { multiMap($0) } + case .revenue: values = try await marketKit.revenueRanks(currencyCode: code).compactMap { multiMap($0) } + } + + await MainActor.run { [weak self] in + self?.internalState = .loaded(values: values) + } + } catch { + await MainActor.run { [weak self] in + self?.internalState = .failed(error: error) + } + } + } + .store(in: &tasks) + } +} + +extension RankViewModel { + private enum InternalState { + case loading + case loaded(values: [Value]) + case failed(error: Error) + } + + private enum Value { + case multi(coin: Coin, value: RankMultiValue) + case single(coin: Coin, value: RankValue) + + var coinUid: String { + switch self { + case let .multi(_, value): return value.uid + case let .single(_, value): return value.uid + } + } + } + + enum State { + case loading + case loaded(items: [Item]) + case failed(error: Error) + } + + struct Item: Hashable { + let index: Int + let coin: Coin + let value: Decimal + + public func hash(into hasher: inout Hasher) { + hasher.combine(index) + hasher.combine(coin.uid) + } + } +} + +extension RankMultiValue { + func value(timePeriod: HsTimePeriod) -> Decimal? { + switch timePeriod { + case .day1: return value1d + case .week1: return value7d + case .month1: return value30d + default: return value1d + } + } +} + +extension RankViewModel { + enum RankType { + case cexVolume + case dexVolume + case dexLiquidity + case address + case txCount + case holders + case fee + case revenue + + var title: String { + switch self { + case .cexVolume: return "coin_analytics.cex_volume_rank".localized + case .dexVolume: return "coin_analytics.dex_volume_rank".localized + case .dexLiquidity: return "coin_analytics.dex_liquidity_rank".localized + case .address: return "coin_analytics.active_addresses_rank".localized + case .txCount: return "coin_analytics.transaction_count_rank".localized + case .holders: return "coin_analytics.holders_rank".localized + case .fee: return "coin_analytics.project_fee_rank".localized + case .revenue: return "coin_analytics.project_revenue_rank".localized + } + } + + var description: String { + switch self { + case .cexVolume: return "coin_analytics.cex_volume_rank.description".localized + case .dexVolume: return "coin_analytics.dex_volume_rank.description".localized + case .dexLiquidity: return "coin_analytics.dex_liquidity_rank.description".localized + case .address: return "coin_analytics.active_addresses_rank.description".localized + case .txCount: return "coin_analytics.transaction_count_rank.description".localized + case .holders: return "coin_analytics.holders_rank.description".localized + case .fee: return "coin_analytics.project_fee_rank.description".localized + case .revenue: return "coin_analytics.project_revenue_rank.description".localized + } + } + + var imageUid: String { + switch self { + case .cexVolume: return "cex_volume" + case .dexVolume: return "dex_volume" + case .dexLiquidity: return "dex_liquidity" + case .address: return "active_addresses" + case .txCount: return "trx_count" + case .holders: return "holders" + case .fee: return "fee" + case .revenue: return "revenue" + } + } + + var sortingField: String { + switch self { + case .cexVolume: return "coin_analytics.cex_volume_rank.sorting_field".localized + case .dexVolume: return "coin_analytics.dex_volume_rank.sorting_field".localized + case .dexLiquidity: return "coin_analytics.dex_liquidity_rank.sorting_field".localized + case .address: return "coin_analytics.active_addresses_rank.sorting_field".localized + case .txCount: return "coin_analytics.transaction_count_rank.sorting_field".localized + case .holders: return "coin_analytics.holders_rank.sorting_field".localized + case .fee: return "coin_analytics.project_fee_rank.sorting_field".localized + case .revenue: return "coin_analytics.project_revenue_rank.sorting_field".localized + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Ranks/CoinRankHeaderView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Ranks/CoinRankHeaderView.swift deleted file mode 100644 index dbc358fb33..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Ranks/CoinRankHeaderView.swift +++ /dev/null @@ -1,76 +0,0 @@ -import ComponentKit -import RxCocoa -import RxSwift -import SnapKit -import ThemeKit -import UIExtensions -import UIKit - -class CoinRankHeaderView: UITableViewHeaderFooterView { - static let height: CGFloat = .heightSingleLineCell - - private let viewModel: CoinRankViewModel - private let disposeBag = DisposeBag() - - private let sortButton = SecondaryCircleButton() - - init(viewModel: CoinRankViewModel) { - self.viewModel = viewModel - - super.init(reuseIdentifier: nil) - - backgroundView = UIView() - backgroundView?.backgroundColor = .themeNavigationBarBackground - - let separatorView = UIView() - - contentView.addSubview(separatorView) - separatorView.snp.makeConstraints { maker in - maker.leading.top.trailing.equalToSuperview() - maker.height.equalTo(CGFloat.heightOnePixel) - } - - separatorView.backgroundColor = .themeSteel20 - - contentView.addSubview(sortButton) - sortButton.snp.makeConstraints { maker in - maker.leading.equalToSuperview().inset(CGFloat.margin16) - maker.centerY.equalToSuperview() - } - - sortButton.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - sortButton.addTarget(self, action: #selector(onTapSortButton), for: .touchUpInside) - - if let selectorItems = viewModel.selectorItems { - let selector = SelectorButton() - - contentView.addSubview(selector) - selector.snp.makeConstraints { maker in - maker.trailing.equalToSuperview().inset(CGFloat.margin16) - maker.centerY.equalToSuperview() - maker.height.equalTo(28) - } - - selector.set(items: selectorItems) - selector.setSelected(index: viewModel.selectorIndex) - selector.onSelect = { [weak self] index in - self?.viewModel.onSelectSelector(index: index) - } - } - - subscribe(disposeBag, viewModel.sortDirectionDriver) { [weak self] in self?.syncSortButton(ascending: $0) } - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc private func onTapSortButton() { - viewModel.onToggleSortDirection() - } - - private func syncSortButton(ascending: Bool) { - sortButton.set(image: UIImage(named: ascending ? "sort_l2h_20" : "sort_h2l_20")) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Ranks/CoinRankModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Ranks/CoinRankModule.swift deleted file mode 100644 index 90f55e4944..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Ranks/CoinRankModule.swift +++ /dev/null @@ -1,30 +0,0 @@ -import ThemeKit -import UIKit - -enum CoinRankModule { - static func viewController(type: RankType) -> UIViewController { - let service = CoinRankService( - type: type, - marketKit: App.shared.marketKit, - currencyManager: App.shared.currencyManager - ) - - let viewModel = CoinRankViewModel(service: service) - let viewController = CoinRankViewController(viewModel: viewModel) - - return ThemeNavigationController(rootViewController: viewController) - } -} - -extension CoinRankModule { - enum RankType { - case cexVolume - case dexVolume - case dexLiquidity - case address - case txCount - case holders - case fee - case revenue - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Ranks/CoinRankService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Ranks/CoinRankService.swift deleted file mode 100644 index 6ed426b1f2..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Ranks/CoinRankService.swift +++ /dev/null @@ -1,161 +0,0 @@ -import Foundation -import HsExtensions -import MarketKit - -class CoinRankService { - let type: CoinRankModule.RankType - private let marketKit: MarketKit.Kit - private let currencyManager: CurrencyManager - private var tasks = Set() - - @PostPublished private(set) var state: State = .loading - - var sortDirectionAscending: Bool = false { - didSet { - syncIfPossible(reorder: true) - } - } - - var timePeriod: HsTimePeriod = .month1 { - didSet { - syncIfPossible(reorder: true) - } - } - - private var internalItems: [InternalItem]? - - init(type: CoinRankModule.RankType, marketKit: MarketKit.Kit, currencyManager: CurrencyManager) { - self.type = type - self.marketKit = marketKit - self.currencyManager = currencyManager - - sync() - } - - func sync() { - tasks = Set() - - state = .loading - - Task { [weak self, marketKit, currencyManager, type] in - do { - let currencyCode = currencyManager.baseCurrency.code - let values: [Value] - - switch type { - case .cexVolume: values = try await marketKit.cexVolumeRanks(currencyCode: currencyCode).map { .multi(value: $0) } - case .dexVolume: values = try await marketKit.dexVolumeRanks(currencyCode: currencyCode).map { .multi(value: $0) } - case .dexLiquidity: values = try await marketKit.dexLiquidityRanks().map { .single(value: $0) } - case .address: values = try await marketKit.activeAddressRanks().map { .multi(value: $0) } - case .txCount: values = try await marketKit.transactionCountRanks().map { .multi(value: $0) } - case .holders: values = try await marketKit.holdersRanks().map { .single(value: $0) } - case .fee: values = try await marketKit.feeRanks(currencyCode: currencyCode).map { .multi(value: $0) } - case .revenue: values = try await marketKit.revenueRanks(currencyCode: currencyCode).map { .multi(value: $0) } - } - - self?.handle(values: values) - } catch { - self?.state = .failed(error: error) - } - }.store(in: &tasks) - } - - private func handle(values: [Value]) { - do { - let coins = try marketKit.allCoins() - - var coinMap = [String: Coin]() - coins.forEach { coinMap[$0.uid] = $0 } - - internalItems = values.compactMap { value in - guard let coin = coinMap[value.coinUid] else { - return nil - } - - return InternalItem(coin: coin, value: value) - } - - syncIfPossible(reorder: false) - } catch { - state = .failed(error: error) - } - } - - private func syncIfPossible(reorder: Bool) { - guard let internalItems else { - return - } - - let items = internalItems.compactMap { internalItem -> Item? in - let resolvedValue: Decimal? - - switch internalItem.value { - case let .multi(value): - switch timePeriod { - case .day1: resolvedValue = value.value1d - case .week1: resolvedValue = value.value7d - default: resolvedValue = value.value30d - } - case let .single(value): - resolvedValue = value.value - } - - guard let resolvedValue else { - return nil - } - - return Item(coin: internalItem.coin, value: resolvedValue) - } - - let filteredItems = items.sorted { $0.value > $1.value }.prefix(300) - let indexedItems = filteredItems.enumerated().map { index, item in - IndexedItem(index: index + 1, coin: item.coin, value: item.value) - } - - let sortedIndexedItems = sortDirectionAscending ? indexedItems.sorted { $0.value < $1.value } : indexedItems - - state = .loaded(items: sortedIndexedItems, reorder: reorder) - } -} - -extension CoinRankService { - var currency: Currency { - currencyManager.baseCurrency - } -} - -extension CoinRankService { - private struct InternalItem { - let coin: Coin - let value: Value - } - - private enum Value { - case multi(value: RankMultiValue) - case single(value: RankValue) - - var coinUid: String { - switch self { - case let .multi(value): return value.uid - case let .single(value): return value.uid - } - } - } - - enum State { - case loading - case loaded(items: [IndexedItem], reorder: Bool) - case failed(error: Error) - } - - private struct Item { - let coin: Coin - let value: Decimal - } - - struct IndexedItem { - let index: Int - let coin: Coin - let value: Decimal - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Ranks/CoinRankViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Ranks/CoinRankViewController.swift deleted file mode 100644 index e318e7029b..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Ranks/CoinRankViewController.swift +++ /dev/null @@ -1,171 +0,0 @@ -import ComponentKit -import HUD -import RxSwift -import SectionsTableView -import ThemeKit -import UIKit - -class CoinRankViewController: ThemeViewController { - private let viewModel: CoinRankViewModel - private let headerView: CoinRankHeaderView - private let disposeBag = DisposeBag() - - private let tableView = SectionsTableView(style: .plain) - private let spinner = HUDActivityView.create(with: .medium24) - private let errorView = PlaceholderViewModule.reachabilityView() - - private var viewItems: [CoinRankViewModel.ViewItem]? - - init(viewModel: CoinRankViewModel) { - self.viewModel = viewModel - headerView = CoinRankHeaderView(viewModel: viewModel) - - super.init() - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - navigationItem.largeTitleDisplayMode = .never - navigationItem.rightBarButtonItem = UIBarButtonItem(title: "button.close".localized, style: .plain, target: self, action: #selector(onTapClose)) - - view.addSubview(tableView) - tableView.snp.makeConstraints { maker in - maker.edges.equalToSuperview() - } - - tableView.sectionHeaderTopPadding = 0 - tableView.separatorStyle = .none - tableView.backgroundColor = .clear - tableView.sectionDataSource = self - - tableView.registerCell(forClass: MarketHeaderCell.self) - - view.addSubview(spinner) - spinner.snp.makeConstraints { maker in - maker.center.equalTo(view.safeAreaLayoutGuide) - } - - spinner.startAnimating() - - view.addSubview(errorView) - errorView.snp.makeConstraints { maker in - maker.edges.equalTo(view.safeAreaLayoutGuide) - } - - errorView.configureSyncError(action: { [weak self] in self?.viewModel.onTapRetry() }) - - subscribe(disposeBag, viewModel.viewItemsDriver) { [weak self] in self?.sync(viewItems: $0) } - subscribe(disposeBag, viewModel.loadingDriver) { [weak self] loading in - self?.spinner.isHidden = !loading - } - subscribe(disposeBag, viewModel.syncErrorDriver) { [weak self] visible in - self?.errorView.isHidden = !visible - } - subscribe(disposeBag, viewModel.scrollToTopSignal) { [weak self] in self?.scrollToTop() } - } - - @objc private func onTapClose() { - dismiss(animated: true) - } - - private func sync(viewItems: [CoinRankViewModel.ViewItem]?) { - self.viewItems = viewItems - - tableView.isHidden = viewItems == nil - tableView.reload() - } - - private func scrollToTop() { - tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .bottom, animated: true) - } -} - -extension CoinRankViewController: SectionsDataSource { - private func row(viewItem: CoinRankViewModel.ViewItem, index: Int, isLast: Bool) -> RowProtocol { - let statPage = viewModel.statPage - - return CellBuilderNew.row( - rootElement: .hStack([ - .text { component in - component.font = .captionSB - component.textColor = .themeGray - component.text = viewItem.rank - component.textAlignment = .center - - component.snp.remakeConstraints { maker in - maker.width.equalTo(40) - } - }, - .margin8, - .image32 { component in - component.setImage(urlString: viewItem.imageUrl, placeholder: UIImage(named: "placeholder_circle_32")) - }, - .vStackCentered([ - .textElement(text: .body(viewItem.code)), - .margin(1), - .textElement(text: .subhead2(viewItem.name)), - ]), - .textElement(text: .body(viewItem.value), parameters: .rightAlignment), - ]), - layoutMargins: UIEdgeInsets(top: 0, left: .margin8, bottom: 0, right: CellBuilderNew.defaultMargin), - tableView: tableView, - id: "row-\(index)", - height: .heightDoubleLineCell, - autoDeselect: true, - bind: { cell in - cell.set(backgroundStyle: .transparent, isLast: isLast) - }, - action: { [weak self] in - let coinUid = viewItem.uid - - if let viewController = CoinPageModule.viewController(coinUid: coinUid) { - self?.present(viewController, animated: true) - stat(page: statPage, event: .openCoin(coinUid: coinUid)) - } - } - ) - } - - private func bind(cell: MarketHeaderCell) { - cell.set( - title: viewModel.title, - description: viewModel.description, - imageMode: .remote(imageUrl: viewModel.imageUid.headerImageUrl) - ) - } - - func buildSections() -> [SectionProtocol] { - guard let viewItems else { - return [] - } - - return [ - Section( - id: "header", - rows: [ - Row( - id: "header", - height: MarketHeaderCell.height, - bind: { [weak self] cell, _ in - self?.bind(cell: cell) - } - ), - ] - ), - Section( - id: "coins", - headerState: .static(view: headerView, height: CoinRankHeaderView.height), - footerState: .marginColor(height: .margin32, color: .clear), - rows: viewItems.enumerated().map { index, viewItem in - row(viewItem: viewItem, index: index, isLast: index == viewItems.count - 1) - } - ), - ] - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Ranks/CoinRankViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Ranks/CoinRankViewModel.swift deleted file mode 100644 index e59f213b7b..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Ranks/CoinRankViewModel.swift +++ /dev/null @@ -1,187 +0,0 @@ -import Combine -import Foundation -import MarketKit -import RxCocoa -import RxRelay -import RxSwift - -class CoinRankViewModel { - private let timePeriods: [HsTimePeriod] = [.day1, .week1, .month1] - - private let service: CoinRankService - private var cancellables = Set() - - private let viewItemsRelay = BehaviorRelay<[ViewItem]?>(value: nil) - private let loadingRelay = BehaviorRelay(value: false) - private let syncErrorRelay = BehaviorRelay(value: false) - private let sortDirectionRelay: BehaviorRelay - private let scrollToTopRelay = PublishRelay() - - init(service: CoinRankService) { - self.service = service - sortDirectionRelay = BehaviorRelay(value: service.sortDirectionAscending) - - service.$state - .sink { [weak self] in self?.sync(state: $0) } - .store(in: &cancellables) - - sync(state: service.state) - } - - private func sync(state: CoinRankService.State) { - switch state { - case .loading: - viewItemsRelay.accept(nil) - loadingRelay.accept(true) - syncErrorRelay.accept(false) - case let .loaded(items, reorder): - viewItemsRelay.accept(viewItems(items: items)) - loadingRelay.accept(false) - syncErrorRelay.accept(false) - - if reorder { - scrollToTopRelay.accept(()) - } - case .failed: - viewItemsRelay.accept(nil) - loadingRelay.accept(false) - syncErrorRelay.accept(true) - } - } - - private func viewItems(items: [CoinRankService.IndexedItem]) -> [ViewItem] { - let currency = service.currency - return items.enumerated().map { index, item in - viewItem(index: index, item: item, currency: currency) - } - } - - private func viewItem(index _: Int, item: CoinRankService.IndexedItem, currency: Currency) -> ViewItem { - ViewItem( - uid: item.coin.uid, - rank: "\(item.index)", - imageUrl: item.coin.imageUrl, - code: item.coin.code, - name: item.coin.name, - value: formatted(value: item.value, currency: currency) - ) - } - - private func formatted(value: Decimal, currency: Currency) -> String? { - switch service.type { - case .cexVolume, .dexVolume, .dexLiquidity, .fee, .revenue: - return ValueFormatter.instance.formatShort(currencyValue: CurrencyValue(currency: currency, value: value)) - case .address, .txCount, .holders: - return ValueFormatter.instance.formatShort(value: value) - } - } -} - -extension CoinRankViewModel { - var viewItemsDriver: Driver<[ViewItem]?> { - viewItemsRelay.asDriver() - } - - var loadingDriver: Driver { - loadingRelay.asDriver() - } - - var syncErrorDriver: Driver { - syncErrorRelay.asDriver() - } - - var scrollToTopSignal: Signal { - scrollToTopRelay.asSignal() - } - - var title: String { - switch service.type { - case .cexVolume: return "coin_analytics.cex_volume_rank".localized - case .dexVolume: return "coin_analytics.dex_volume_rank".localized - case .dexLiquidity: return "coin_analytics.dex_liquidity_rank".localized - case .address: return "coin_analytics.active_addresses_rank".localized - case .txCount: return "coin_analytics.transaction_count_rank".localized - case .holders: return "coin_analytics.holders_rank".localized - case .fee: return "coin_analytics.project_fee_rank".localized - case .revenue: return "coin_analytics.project_revenue_rank".localized - } - } - - var description: String { - switch service.type { - case .cexVolume: return "coin_analytics.cex_volume_rank.description".localized - case .dexVolume: return "coin_analytics.dex_volume_rank.description".localized - case .dexLiquidity: return "coin_analytics.dex_liquidity_rank.description".localized - case .address: return "coin_analytics.active_addresses_rank.description".localized - case .txCount: return "coin_analytics.transaction_count_rank.description".localized - case .holders: return "coin_analytics.holders_rank.description".localized - case .fee: return "coin_analytics.project_fee_rank.description".localized - case .revenue: return "coin_analytics.project_revenue_rank.description".localized - } - } - - var imageUid: String { - switch service.type { - case .cexVolume: return "cex_volume" - case .dexVolume: return "dex_volume" - case .dexLiquidity: return "dex_liquidity" - case .address: return "active_addresses" - case .txCount: return "trx_count" - case .holders: return "holders" - case .fee: return "fee" - case .revenue: return "revenue" - } - } - - var sortDirectionDriver: Driver { - sortDirectionRelay.asDriver() - } - - var statPage: StatPage { - switch service.type { - case .cexVolume: return .coinRankCexVolume - case .dexVolume: return .coinRankDexVolume - case .dexLiquidity: return .coinRankDexLiquidity - case .address: return .coinRankAddress - case .txCount: return .coinRankTxCount - case .holders: return .coinRankHolders - case .fee: return .coinRankFee - case .revenue: return .coinRankRevenue - } - } - - func onToggleSortDirection() { - service.sortDirectionAscending = !service.sortDirectionAscending - sortDirectionRelay.accept(service.sortDirectionAscending) - } - - var selectorItems: [String]? { - switch service.type { - case .cexVolume, .dexVolume, .address, .txCount, .fee, .revenue: return timePeriods.map(\.title) - case .dexLiquidity, .holders: return nil - } - } - - var selectorIndex: Int { - timePeriods.firstIndex(of: service.timePeriod) ?? 0 - } - - func onSelectSelector(index: Int) { - service.timePeriod = timePeriods[index] - } - - func onTapRetry() { - service.sync() - } -} - -extension CoinRankViewModel { - struct ViewItem { - let uid: String - let rank: String - let imageUrl: String - let code: String - let name: String - let value: String? - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Treasuries/CoinTreasuriesModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Treasuries/CoinTreasuriesModule.swift deleted file mode 100644 index 994dc13f7a..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Treasuries/CoinTreasuriesModule.swift +++ /dev/null @@ -1,10 +0,0 @@ -import MarketKit -import UIKit - -enum CoinTreasuriesModule { - static func viewController(coin: Coin) -> UIViewController { - let service = CoinTreasuriesService(coin: coin, marketKit: App.shared.marketKit, currencyManager: App.shared.currencyManager) - let viewModel = CoinTreasuriesViewModel(service: service) - return CoinTreasuriesViewController(viewModel: viewModel) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Treasuries/CoinTreasuriesService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Treasuries/CoinTreasuriesService.swift deleted file mode 100644 index 5381f3c1af..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Treasuries/CoinTreasuriesService.swift +++ /dev/null @@ -1,138 +0,0 @@ -import HsExtensions -import MarketKit -import RxRelay -import RxSwift - -class CoinTreasuriesService { - private let coin: Coin - private let marketKit: MarketKit.Kit - private let currencyManager: CurrencyManager - private var tasks = Set() - - private var internalState: DataStatus<[CoinTreasury]> = .loading { - didSet { - syncState() - } - } - - private let stateRelay = PublishRelay() - private(set) var state: State = .loading { - didSet { - stateRelay.accept(state) - } - } - - private let sortDirectionAscendingRelay = PublishRelay() - var sortDirectionAscending: Bool = false { - didSet { - syncIfPossible(reorder: true) - sortDirectionAscendingRelay.accept(sortDirectionAscending) - } - } - - private let typeFilterRelay = PublishRelay() - var typeFilter: TypeFilter = .all { - didSet { - syncIfPossible() - typeFilterRelay.accept(typeFilter) - } - } - - init(coin: Coin, marketKit: MarketKit.Kit, currencyManager: CurrencyManager) { - self.coin = coin - self.marketKit = marketKit - self.currencyManager = currencyManager - - syncTreasuries() - } - - private func syncState(reorder: Bool = false) { - switch internalState { - case .loading: - state = .loading - case let .completed(treasuries): - let treasuries = treasuries - .filter { - switch typeFilter { - case .all: return true - case .public: return $0.type == .public - case .private: return $0.type == .private - case .etf: return $0.type == .etf - } - } - .sorted { lhsTreasury, rhsTreasury in - sortDirectionAscending ? lhsTreasury.amount < rhsTreasury.amount : lhsTreasury.amount > rhsTreasury.amount - } - - state = .loaded(treasuries: treasuries, reorder: reorder) - case let .failed(error): - state = .failed(error: error) - } - } - - private func syncTreasuries() { - tasks = Set() - - if case .failed = state { - internalState = .loading - } - - Task { [weak self, marketKit, coin, currencyManager] in - do { - let treasuries = try await marketKit.treasuries(coinUid: coin.uid, currencyCode: currencyManager.baseCurrency.code) - self?.internalState = .completed(treasuries) - } catch { - self?.internalState = .failed(error) - } - }.store(in: &tasks) - } - - private func syncIfPossible(reorder: Bool = false) { - guard case .completed = internalState else { - return - } - - syncState(reorder: reorder) - } -} - -extension CoinTreasuriesService { - var stateObservable: Observable { - stateRelay.asObservable() - } - - var sortDirectionAscendingObservable: Observable { - sortDirectionAscendingRelay.asObservable() - } - - var typeFilterObservable: Observable { - typeFilterRelay.asObservable() - } - - var currency: Currency { - currencyManager.baseCurrency - } - - var coinCode: String { - coin.code - } - - func refresh() { - syncTreasuries() - } -} - -extension CoinTreasuriesService { - enum State { - case loading - case loaded(treasuries: [CoinTreasury], reorder: Bool) - case failed(error: Error) - } - - enum TypeFilter: CaseIterable { - case all - case `public` - case `private` - case etf - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Treasuries/CoinTreasuriesView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Treasuries/CoinTreasuriesView.swift new file mode 100644 index 0000000000..c39a89a1d9 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Treasuries/CoinTreasuriesView.swift @@ -0,0 +1,153 @@ +import Kingfisher +import MarketKit +import SwiftUI + +struct CoinTreasuriesView: View { + @StateObject var viewModel: CoinTreasuriesViewModel + + @State private var filterSelectorPresented = false + + init(coin: Coin) { + _viewModel = StateObject(wrappedValue: CoinTreasuriesViewModel(coin: coin)) + } + + var body: some View { + ThemeView { + switch viewModel.state { + case .loading: + VStack(spacing: 0) { + header(disabled: true) + loadingList() + } + case let .loaded(treasuries): + VStack(spacing: 0) { + header() + + ScrollViewReader { proxy in + ThemeList(bottomSpacing: .margin32, invisibleTopView: true) { + list(treasuries: treasuries) + footer() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + } + .onChange(of: viewModel.filter) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + .onChange(of: viewModel.sortOrder) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + } + } + case .failed: + SyncErrorView { + Task { + await viewModel.refresh() + } + } + } + } + } + + @ViewBuilder private func header(disabled: Bool = false) -> some View { + HorizontalDivider(color: .themeSteel10) + HStack { + HStack { + Button(action: { + filterSelectorPresented = true + }) { + Text(viewModel.filter.title) + } + .buttonStyle(SecondaryButtonStyle(style: .transparent, rightAccessory: .dropDown)) + .disabled(disabled) + } + .alert( + isPresented: $filterSelectorPresented, + title: "coin_analytics.treasuries.filters".localized, + viewItems: CoinTreasuriesViewModel.Filter.allCases.map { .init(text: $0.title, selected: viewModel.filter == $0) }, + onTap: { index in + guard let index else { + return + } + + viewModel.filter = CoinTreasuriesViewModel.Filter.allCases[index] + } + ) + + Spacer() + + Button(action: { + viewModel.sortOrder.toggle() + }) { + sortIcon().renderingMode(.template) + } + .buttonStyle(SecondaryCircleButtonStyle(style: .default)) + .padding(.trailing, .margin16) + .disabled(disabled) + } + .padding(.vertical, .margin8) + } + + @ViewBuilder private func list(treasuries: [CoinTreasury]) -> some View { + ListForEach(treasuries) { treasury in + ListRow { + itemContent( + imageUrl: URL(string: treasury.fundLogoUrl), + fund: treasury.fund, + amount: ValueFormatter.instance.formatShort(value: treasury.amount, decimalCount: 8, symbol: viewModel.coinCode) ?? "---", + country: treasury.country, + amountInCurrency: ValueFormatter.instance.formatShort(currency: viewModel.currency, value: treasury.amountInCurrency) ?? "---" + ) + } + } + } + + @ViewBuilder private func footer() -> some View { + Text("Powered by Bitcointreasuries.net") + .textCaption(color: .themeGray) + .padding(.top, .margin12) + .padding(.horizontal, .margin24) + .frame(maxWidth: .infinity) + } + + @ViewBuilder private func loadingList() -> some View { + ThemeList(Array(0 ... 10)) { _ in + ListRow { + itemContent( + imageUrl: nil, + fund: "Unstoppable", + amount: "123.45 BTC", + country: "KG", + amountInCurrency: "$123.45" + ) + .redacted() + } + } + .simultaneousGesture(DragGesture(minimumDistance: 0), including: .all) + } + + @ViewBuilder private func itemContent(imageUrl: URL?, fund: String, amount: String, country: String, amountInCurrency: String) -> some View { + KFImage.url(imageUrl) + .resizable() + .placeholder { RoundedRectangle(cornerRadius: .cornerRadius8).fill(Color.themeSteel20) } + .clipShape(RoundedRectangle(cornerRadius: .cornerRadius8)) + .frame(width: .iconSize32, height: .iconSize32) + + VStack(spacing: 1) { + HStack(spacing: .margin8) { + Text(fund).textBody() + Spacer() + Text(amount).textBody() + } + + HStack(spacing: .margin8) { + Text(country).textSubhead2() + Spacer() + Text(amountInCurrency).textSubhead2(color: .themeJacob) + } + } + } + + private func sortIcon() -> Image { + switch viewModel.sortOrder { + case .asc: return Image("sort_l2h_20") + case .desc: return Image("sort_h2l_20") + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Treasuries/CoinTreasuriesViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Treasuries/CoinTreasuriesViewController.swift deleted file mode 100644 index 8a5c89c6fe..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Treasuries/CoinTreasuriesViewController.swift +++ /dev/null @@ -1,180 +0,0 @@ -import ComponentKit -import Foundation -import HUD -import RxCocoa -import RxSwift -import SectionsTableView -import SnapKit -import ThemeKit -import UIKit - -class CoinTreasuriesViewController: ThemeViewController { - private let viewModel: CoinTreasuriesViewModel - private let disposeBag = DisposeBag() - - private let tableView = SectionsTableView(style: .plain) - private let spinner = HUDActivityView.create(with: .medium24) - private let errorView = PlaceholderViewModule.reachabilityView() - - private var viewItems: [CoinTreasuriesViewModel.ViewItem]? - private let headerView: DropdownSortHeaderView - - init(viewModel: CoinTreasuriesViewModel) { - self.viewModel = viewModel - headerView = DropdownSortHeaderView(viewModel: viewModel) - - super.init() - - headerView.viewController = self - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - title = "coin_analytics.treasuries".localized - - view.addSubview(tableView) - tableView.snp.makeConstraints { maker in - maker.edges.equalToSuperview() - } - - tableView.sectionHeaderTopPadding = 0 - tableView.allowsSelection = false - tableView.separatorStyle = .none - tableView.backgroundColor = .clear - - tableView.sectionDataSource = self - tableView.registerCell(forClass: BrandFooterCell.self) - - view.addSubview(spinner) - spinner.snp.makeConstraints { maker in - maker.center.equalToSuperview() - } - - spinner.startAnimating() - - view.addSubview(errorView) - errorView.snp.makeConstraints { maker in - maker.edges.equalTo(view.safeAreaLayoutGuide) - } - - errorView.configureSyncError(action: { [weak self] in self?.onRetry() }) - - subscribe(disposeBag, viewModel.viewItemsDriver) { [weak self] in self?.sync(viewItems: $0) } - subscribe(disposeBag, viewModel.loadingDriver) { [weak self] loading in - self?.spinner.isHidden = !loading - } - subscribe(disposeBag, viewModel.syncErrorDriver) { [weak self] visible in - self?.errorView.isHidden = !visible - } - subscribe(disposeBag, viewModel.scrollToTopSignal) { [weak self] in self?.scrollToTop() } - } - - @objc private func onRetry() { - viewModel.onTapRetry() - } - - private func sync(viewItems: [CoinTreasuriesViewModel.ViewItem]?) { - self.viewItems = viewItems - - if viewItems != nil { - tableView.bounces = true - } else { - tableView.bounces = false - } - - tableView.reload() - } - - private func scrollToTop() { - tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .bottom, animated: true) - } -} - -extension CoinTreasuriesViewController: SectionsDataSource { - private func row(viewItem: CoinTreasuriesViewModel.ViewItem, index: Int, isLast: Bool) -> RowProtocol { - CellBuilderNew.row( - rootElement: .hStack([ - .image32 { component in - component.setImage(urlString: viewItem.logoUrl, placeholder: UIImage(named: "placeholder_circle_32")) - }, - .vStackCentered([ - .hStack([ - .text { component in - component.font = .body - component.textColor = .themeLeah - component.text = viewItem.fund - }, - .text { component in - component.font = .body - component.textColor = .themeLeah - component.textAlignment = .right - component.setContentCompressionResistancePriority(.required, for: .horizontal) - component.text = viewItem.amount - }, - ]), - .margin(1), - .hStack([ - .text { component in - component.font = .subhead2 - component.textColor = .themeGray - component.text = viewItem.country - }, - .text { component in - component.setContentCompressionResistancePriority(.required, for: .horizontal) - component.setContentHuggingPriority(.required, for: .horizontal) - component.textAlignment = .right - component.font = .subhead2 - component.textColor = .themeJacob - component.text = viewItem.amountInCurrency - }, - ]), - ]), - ]), - tableView: tableView, - id: "treasury-\(index)", - height: .heightDoubleLineCell, - bind: { cell in - cell.set(backgroundStyle: .transparent, isLast: isLast) - } - ) - } - - private func poweredBySection(text: String) -> SectionProtocol { - Section( - id: "powered-by", - rows: [ - Row( - id: "powered-by", - dynamicHeight: { containerWidth in - BrandFooterCell.height(containerWidth: containerWidth, title: text) - }, - bind: { cell, _ in - cell.title = text - } - ), - ] - ) - } - - func buildSections() -> [SectionProtocol] { - guard let viewItems else { - return [] - } - - return [ - Section( - id: "treasuries", - headerState: .static(view: headerView, height: .heightSingleLineCell), - footerState: .marginColor(height: .margin32, color: .clear), - rows: viewItems.enumerated().map { row(viewItem: $1, index: $0, isLast: $0 == viewItems.count - 1) } - ), - poweredBySection(text: "Powered by Bitcointreasuries.net"), - ] - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Treasuries/CoinTreasuriesViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Treasuries/CoinTreasuriesViewModel.swift index 72111a9955..beb8d9e417 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Treasuries/CoinTreasuriesViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Treasuries/CoinTreasuriesViewModel.swift @@ -1,137 +1,125 @@ +import Combine +import Foundation +import HsExtensions import MarketKit -import RxCocoa -import RxRelay -import RxSwift -class CoinTreasuriesViewModel { - private let service: CoinTreasuriesService - private let disposeBag = DisposeBag() +class CoinTreasuriesViewModel: ObservableObject { + private let coin: Coin + private let marketKit = App.shared.marketKit + private let currencyManager = App.shared.currencyManager + private var tasks = Set() - private let viewItemsRelay = BehaviorRelay<[ViewItem]?>(value: nil) - private let loadingRelay = BehaviorRelay(value: false) - private let syncErrorRelay = BehaviorRelay(value: false) - private let scrollToTopRelay = PublishRelay() - - private let dropdownValueRelay = BehaviorRelay(value: "") - private let sortDirectionAscendingRelay = BehaviorRelay(value: false) - - init(service: CoinTreasuriesService) { - self.service = service - - subscribe(disposeBag, service.stateObservable) { [weak self] in self?.sync(state: $0) } - subscribe(disposeBag, service.typeFilterObservable) { [weak self] in self?.sync(typeFilter: $0) } - subscribe(disposeBag, service.sortDirectionAscendingObservable) { [weak self] in self?.sync(sortDirectionAscending: $0) } - - sync(state: service.state) - sync(typeFilter: service.typeFilter) - sync(sortDirectionAscending: service.sortDirectionAscending) - } - - private func sync(state: CoinTreasuriesService.State) { - switch state { - case .loading: - viewItemsRelay.accept(nil) - loadingRelay.accept(true) - syncErrorRelay.accept(false) - case let .loaded(treasuries, reorder): - viewItemsRelay.accept(treasuries.map { viewItem(treasury: $0) }) - loadingRelay.accept(false) - syncErrorRelay.accept(false) - - if reorder { - scrollToTopRelay.accept(()) - } - case .failed: - viewItemsRelay.accept(nil) - loadingRelay.accept(false) - syncErrorRelay.accept(true) + private var internalState: State = .loading { + didSet { + syncState() } } - private func sync(typeFilter: CoinTreasuriesService.TypeFilter) { - dropdownValueRelay.accept(title(typeFilter: typeFilter)) - } - - private func sync(sortDirectionAscending: Bool) { - sortDirectionAscendingRelay.accept(sortDirectionAscending) - } + @Published var state: State = .loading - private func viewItem(treasury: CoinTreasury) -> ViewItem { - ViewItem( - logoUrl: treasury.fundLogoUrl, - fund: treasury.fund, - country: treasury.country, - amount: ValueFormatter.instance.formatShort(value: treasury.amount, decimalCount: 8, symbol: service.coinCode) ?? "---", - amountInCurrency: ValueFormatter.instance.formatShort(currency: service.currency, value: treasury.amountInCurrency) ?? "---" - ) + @Published var filter: Filter = .all { + didSet { + syncState() + } } - private func title(typeFilter: CoinTreasuriesService.TypeFilter) -> String { - switch typeFilter { - case .all: return "coin_analytics.treasuries.filter.all".localized - case .public: return "coin_analytics.treasuries.filter.public".localized - case .private: return "coin_analytics.treasuries.filter.private".localized - case .etf: return "coin_analytics.treasuries.filter.etf".localized + @Published var sortOrder: MarketModule.SortOrder = .desc { + didSet { + syncState() } } -} -extension CoinTreasuriesViewModel: IDropdownSortHeaderViewModel { - var dropdownTitle: String { - "coin_analytics.treasuries.filters".localized - } + init(coin: Coin) { + self.coin = coin - var dropdownViewItems: [AlertViewItem] { - CoinTreasuriesService.TypeFilter.allCases.map { typeFilter in - AlertViewItem(text: title(typeFilter: typeFilter), selected: service.typeFilter == typeFilter) - } + sync() } - var dropdownValueDriver: Driver { - dropdownValueRelay.asDriver() - } + private func sync() { + tasks = Set() - func onSelectDropdown(index: Int) { - service.typeFilter = CoinTreasuriesService.TypeFilter.allCases[index] - } + if case .failed = state { + internalState = .loading + } - var sortDirectionAscendingDriver: Driver { - sortDirectionAscendingRelay.asDriver() + Task { [weak self, marketKit, coin, currencyManager] in + do { + let treasuries = try await marketKit.treasuries(coinUid: coin.uid, currencyCode: currencyManager.baseCurrency.code) + + await MainActor.run { [weak self] in + self?.internalState = .loaded(treasuries) + } + } catch { + await MainActor.run { [weak self] in + self?.internalState = .failed(error) + } + } + } + .store(in: &tasks) } - func onToggleSortDirection() { - service.sortDirectionAscending = !service.sortDirectionAscending + private func syncState() { + switch internalState { + case .loading: + state = .loading + case let .loaded(treasuries): + let treasuries = treasuries + .filter { + switch filter { + case .all: return true + case .public: return $0.type == .public + case .private: return $0.type == .private + case .etf: return $0.type == .etf + } + } + .sorted { lhsTreasury, rhsTreasury in + switch sortOrder { + case .asc: lhsTreasury.amount < rhsTreasury.amount + case .desc: lhsTreasury.amount > rhsTreasury.amount + } + } + + state = .loaded(treasuries) + case let .failed(error): + state = .failed(error) + } } } extension CoinTreasuriesViewModel { - var viewItemsDriver: Driver<[ViewItem]?> { - viewItemsRelay.asDriver() - } - - var loadingDriver: Driver { - loadingRelay.asDriver() + var currency: Currency { + currencyManager.baseCurrency } - var syncErrorDriver: Driver { - syncErrorRelay.asDriver() + var coinCode: String { + coin.code } - var scrollToTopSignal: Signal { - scrollToTopRelay.asSignal() - } - - func onTapRetry() { - service.refresh() + func refresh() async { + sync() } } extension CoinTreasuriesViewModel { - struct ViewItem { - let logoUrl: String - let fund: String - let country: String - let amount: String - let amountInCurrency: String + enum State { + case loading + case loaded(_ treasuries: [CoinTreasury]) + case failed(_ error: Error) + } + + enum Filter: String, CaseIterable { + case all + case `public` + case `private` + case etf + + var title: String { + switch self { + case .all: return "coin_analytics.treasuries.filter.all".localized + case .public: return "coin_analytics.treasuries.filter.public".localized + case .private: return "coin_analytics.treasuries.filter.private".localized + case .etf: return "coin_analytics.treasuries.filter.etf".localized + } + } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/EvmNetwork/AddEvmSyncSource/AddEvmSyncSourceService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/EvmNetwork/AddEvmSyncSource/AddEvmSyncSourceService.swift index fa6523c95a..6192db4f4e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/EvmNetwork/AddEvmSyncSource/AddEvmSyncSourceService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/EvmNetwork/AddEvmSyncSource/AddEvmSyncSourceService.swift @@ -5,7 +5,7 @@ import RxRelay import RxSwift class AddEvmSyncSourceService { - private let blockchainType: BlockchainType + let blockchainType: BlockchainType private let evmSyncSourceManager: EvmSyncSourceManager private var disposeBag = DisposeBag() diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/EvmNetwork/AddEvmSyncSource/AddEvmSyncSourceViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/EvmNetwork/AddEvmSyncSource/AddEvmSyncSourceViewModel.swift index 773061b287..0421dd6b7f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/EvmNetwork/AddEvmSyncSource/AddEvmSyncSourceViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/EvmNetwork/AddEvmSyncSource/AddEvmSyncSourceViewModel.swift @@ -35,7 +35,7 @@ extension AddEvmSyncSourceViewModel { func onTapAdd() { do { try service.save() - stat(page: .addEvmSyncSource, event: .add(entity: .evmSyncSource)) + stat(page: .blockchainSettingsEvmAdd, event: .addEvmSource(chainUid: service.blockchainType.uid)) finishRelay.accept(()) } catch AddEvmSyncSourceService.UrlError.alreadyExists { urlCautionRelay.accept(Caution(text: "add_evm_sync_source.warning.url_exists".localized, type: .warning)) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ExtendedKey/ExtendedKeyViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ExtendedKey/ExtendedKeyViewController.swift index a5b3692945..8f43a483f3 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ExtendedKey/ExtendedKeyViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ExtendedKey/ExtendedKeyViewController.swift @@ -137,18 +137,53 @@ class ExtendedKeyViewController: ThemeViewController { present(alertController, animated: true) } + + private func openInfo(title: String, description: String) { + let viewController = BottomSheetModule.description(title: title, text: description) + present(viewController, animated: true) + } } extension ExtendedKeyViewController: SectionsDataSource { private func controlRow(item: ControlItem, isFirst: Bool = false, isLast: Bool = false) -> RowProtocol { - tableView.universalRow48( + var elements = [CellBuilderNew.CellElement]() + + if let description = item.description { + elements.append(contentsOf: [ + .secondaryButton { [weak self] component in + component.button.set(style: .transparent2, image: UIImage(named: "circle_information_20")) + component.button.setTitle(item.title, for: .normal) + component.button.setTitleColor(.themeLeah, for: .normal) + component.button.titleLabel?.font = .body + + component.button.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + maker.centerY.equalToSuperview() + } + + component.onTap = { [weak self] in + self?.openInfo(title: item.title, description: description) + } + }, + .margin0, + .text { _ in }, + ]) + } else { + elements.append(.textElement(text: .body(item.title))) + } + + elements.append(.textElement(text: .subhead1(item.value, color: .themeGray), parameters: .allCompression)) + elements.append(contentsOf: CellBuilderNew.CellElement.accessoryElements(item.action == nil ? .none : .dropdown)) + + return CellBuilderNew.row( + rootElement: .hStack(elements), + tableView: tableView, id: item.id, - title: .body(item.title), - value: .subhead1(item.value, color: .themeGray), - accessoryType: item.action == nil ? .none : .dropdown, + height: .heightCell48, autoDeselect: true, - isFirst: isFirst, - isLast: isLast, + bind: { cell in + cell.set(backgroundStyle: .lawrence, isFirst: isFirst, isLast: isLast) + }, action: item.action ) } @@ -159,6 +194,7 @@ extension ExtendedKeyViewController: SectionsDataSource { id: "derivation", title: "extended_key.purpose".localized, value: viewItem.derivation, + description: nil, action: viewItem.derivationSwitchable ? { [weak self] in self?.onTapDerivation() } : nil @@ -171,6 +207,7 @@ extension ExtendedKeyViewController: SectionsDataSource { id: "blockchain", title: "extended_key.blockchain".localized, value: blockchain, + description: nil, action: viewItem.blockchainSwitchable ? { [weak self] in self?.onTapBlockchain() } : nil @@ -184,6 +221,7 @@ extension ExtendedKeyViewController: SectionsDataSource { id: "account", title: "extended_key.account".localized, value: account, + description: "extended_key.account.description".localized, action: { [weak self] in self?.onTapAccount() } @@ -268,6 +306,7 @@ extension ExtendedKeyViewController { let id: String let title: String let value: String + let description: String? var action: (() -> Void)? } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Faq/FaqViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Faq/FaqViewController.swift index b820b8858c..fd56f8060a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Faq/FaqViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Faq/FaqViewController.swift @@ -147,6 +147,7 @@ extension FaqViewController: SectionsDataSource { return } + stat(page: .faq, event: .openArticle(relativeUrl: url.relativePath)) let module = MarkdownModule.viewController(url: url, handleRelativeUrl: false) self?.navigationController?.pushViewController(module, animated: true) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Favorites/FavoritesManager.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Favorites/FavoritesManager.swift deleted file mode 100644 index 5eda17f1c3..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Favorites/FavoritesManager.swift +++ /dev/null @@ -1,58 +0,0 @@ -import RxCocoa -import RxSwift -import WidgetKit - -class FavoritesManager { - private let storage: FavoriteCoinRecordStorage - private let sharedStorage: SharedLocalStorage - - private let coinUidsUpdatedRelay = PublishRelay() - - init(storage: FavoriteCoinRecordStorage, sharedStorage: SharedLocalStorage) { - self.storage = storage - self.sharedStorage = sharedStorage - - syncSharedStorage() - } - - private func syncSharedStorage() { - sharedStorage.set(value: allCoinUids, for: AppWidgetConstants.keyFavoriteCoinUids) - WidgetCenter.shared.reloadTimelines(ofKind: AppWidgetConstants.watchlistWidgetKind) - } -} - -extension FavoritesManager { - var coinUidsUpdatedObservable: Observable { - coinUidsUpdatedRelay.asObservable() - } - - var allCoinUids: [String] { - storage.favoriteCoinRecords.map(\.coinUid) - } - - func add(coinUid: String) { - storage.save(favoriteCoinRecord: FavoriteCoinRecord(coinUid: coinUid)) - coinUidsUpdatedRelay.accept(()) - syncSharedStorage() - } - - func add(coinUids: [String]) { - storage.save(favoriteCoinRecords: coinUids.map { FavoriteCoinRecord(coinUid: $0) }) - coinUidsUpdatedRelay.accept(()) - syncSharedStorage() - } - - func removeAll() { - storage.deleteAll() - } - - func remove(coinUid: String) { - storage.deleteFavoriteCoinRecord(coinUid: coinUid) - coinUidsUpdatedRelay.accept(()) - syncSharedStorage() - } - - func isFavorite(coinUid: String) -> Bool { - storage.favoriteCoinRecordExists(coinUid: coinUid) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Guides/GuidesViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Guides/GuidesViewController.swift index 517bd17325..92e3585675 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Guides/GuidesViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Guides/GuidesViewController.swift @@ -134,6 +134,8 @@ extension GuidesViewController: UITableViewDataSource, UITableViewDelegate { return } + stat(page: .academy, event: .openArticle(relativeUrl: url.relativePath)) + let module = MarkdownModule.viewController(url: url) navigationController?.pushViewController(module, animated: true) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Info/InfoModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Info/InfoModule.swift index bc9c92af4d..9ccc323199 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Info/InfoModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Info/InfoModule.swift @@ -10,11 +10,20 @@ enum InfoModule { } extension InfoModule { - enum ViewItem { + enum ViewItem: Identifiable { case header1(text: String) case header3(text: String) case text(text: String) case listItem(text: String) + + var id: String { + switch self { + case let .header1(text): return text + case let .header3(text): return text + case let .text(text): return text + case let .listItem(text): return text + } + } } static var feeInfo: UIViewController { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/InfoNew/InfoNewView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/InfoNew/InfoNewView.swift new file mode 100644 index 0000000000..b5849ac0b6 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/InfoNew/InfoNewView.swift @@ -0,0 +1,58 @@ +import SwiftUI + +struct InfoNewView: View { + let viewItems: [InfoModule.ViewItem] + @Binding var isPresented: Bool + + var body: some View { + ScrollableThemeView { + VStack(alignment: .leading, spacing: 0) { + ForEach(viewItems) { viewItem in + switch viewItem { + case let .header1(text): + header1View(text: text) + case let .header3(text): + header3View(text: text) + case let .text(text): + textView(text: text) + case .listItem: + EmptyView() + } + } + } + .padding(.bottom, .margin32) + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button("button.close".localized) { + isPresented = false + } + } + } + } + + @ViewBuilder private func header1View(text: String) -> some View { + VStack(alignment: .leading, spacing: .margin8) { + Text(text) + .foregroundColor(.themeLeah) + .font(.themeTitle1) + + HorizontalDivider(color: .themeGray50) + } + .padding(EdgeInsets(top: .margin12, leading: .margin32, bottom: .margin12, trailing: .margin32)) + } + + @ViewBuilder private func header3View(text: String) -> some View { + Text(text) + .foregroundColor(.themeJacob) + .font(.themeHeadline2) + .padding(EdgeInsets(top: .margin16, leading: .margin32, bottom: .margin4, trailing: .margin32)) + } + + @ViewBuilder private func textView(text: String) -> some View { + Text(text) + .foregroundColor(.themeBran) + .font(.themeBody) + .padding(EdgeInsets(top: .margin12, leading: .margin32, bottom: .margin12, trailing: .margin32)) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/LanguageSettings/LanguageSettingsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/LanguageSettings/LanguageSettingsViewModel.swift index 7b0b29a755..a5eec142d7 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/LanguageSettings/LanguageSettingsViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/LanguageSettings/LanguageSettingsViewModel.swift @@ -5,6 +5,7 @@ class LanguageSettingsViewModel: ObservableObject { @Published var currentLanguage: String { didSet { + stat(page: .language, event: .switchLanguage(language: currentLanguage)) LanguageManager.shared.currentLanguage = currentLanguage } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainViewController.swift index a0a379e17e..faaff8d6c6 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainViewController.swift @@ -84,7 +84,8 @@ class MainViewController: ThemeTabBarController { private func sync(balanceTabState _: MainViewModel.BalanceTabState) { var viewControllers = [UIViewController]() if viewModel.showMarket { - let marketModule = marketModule ?? ThemeNavigationController(rootViewController: MarketModule.viewController()) + let marketModule = marketModule ?? MarketView().toNavigationViewController() + marketModule.tabBarItem = UITabBarItem(title: "market.tab_bar_item".localized, image: UIImage(named: "market_2_24"), tag: 0) self.marketModule = marketModule viewControllers.append(marketModule) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainViewModel.swift index 2e4cb1106a..8e1dbec10f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainViewModel.swift @@ -57,7 +57,7 @@ class MainViewModel { deepLinkService.setDeepLinkShown() Task { do { - try await eventHandler.handle(event: deepLink, eventType: .deepLink) + try await eventHandler.handle(source: .main, event: deepLink, eventType: .deepLink) } catch { print("Can't handle Deep Link \(error)") } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Main/ReleaseNotesViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Main/ReleaseNotesViewController.swift index 03348fb459..31f7c2fd1b 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Main/ReleaseNotesViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Main/ReleaseNotesViewController.swift @@ -53,7 +53,7 @@ class ReleaseNotesViewController: MarkdownViewController { } twitterButton.addTarget(self, action: #selector(onTwitterTap), for: .touchUpInside) - twitterButton.setImage(UIImage(named: "filled_twitter_24"), for: .normal) + twitterButton.setImage(UIImage(named: "filled_twitter_24")?.withTintColor(.themeJacob), for: .normal) let telegramButton = UIButton() bottomHolder.addSubview(telegramButton) @@ -64,18 +64,7 @@ class ReleaseNotesViewController: MarkdownViewController { } telegramButton.addTarget(self, action: #selector(onTelegramTap), for: .touchUpInside) - telegramButton.setImage(UIImage(named: "filled_telegram_24"), for: .normal) - - let redditButton = UIButton() - bottomHolder.addSubview(redditButton) - redditButton.snp.makeConstraints { maker in - maker.leading.equalTo(telegramButton.snp.trailing).offset(CGFloat.margin8) - maker.top.bottom.equalToSuperview() - maker.width.equalTo(52) - } - - redditButton.addTarget(self, action: #selector(onRedditTap), for: .touchUpInside) - redditButton.setImage(UIImage(named: "filled_reddit_24"), for: .normal) + telegramButton.setImage(UIImage(named: "filled_telegram_24")?.withTintColor(.themeJacob), for: .normal) let followUsLabel = UILabel() bottomHolder.addSubview(followUsLabel) @@ -85,7 +74,7 @@ class ReleaseNotesViewController: MarkdownViewController { } followUsLabel.font = .caption - followUsLabel.textColor = .themeGray + followUsLabel.textColor = .themeJacob followUsLabel.text = "release_notes.follow_us".localized } @@ -116,10 +105,6 @@ class ReleaseNotesViewController: MarkdownViewController { @objc private func onTelegramTap() { urlManager.open(url: "https://t.me/\(AppConfig.appTelegramAccount)", from: nil) } - - @objc private func onRedditTap() { - urlManager.open(url: "https://www.reddit.com/r/\(AppConfig.appRedditAccount)", from: nil) - } } extension ReleaseNotesViewController: UIAdaptivePresentationControllerDelegate { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Main/Workers/EventHandler.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Main/Workers/EventHandler.swift index 129d3882d3..5c7b751542 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Main/Workers/EventHandler.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Main/Workers/EventHandler.swift @@ -2,7 +2,7 @@ import ComponentKit import Foundation protocol IEventHandler { - func handle(event: Any, eventType: EventHandler.EventType) async throws + func handle(source: StatPage, event: Any, eventType: EventHandler.EventType) async throws } class EventHandler { @@ -26,11 +26,11 @@ class EventHandler { } extension EventHandler: IEventHandler { - func handle(event: Any, eventType: EventHandler.EventType = .all) async throws { + func handle(source: StatPage, event: Any, eventType: EventHandler.EventType = .all) async throws { var lastError: Error? for handler in eventHandlers { do { - try await handler.handle(event: event, eventType: eventType) + try await handler.handle(source: source, event: event, eventType: eventType) return } catch { lastError = error diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Main/Workers/SendAppShowWorker/AddressAppShowModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Main/Workers/SendAppShowWorker/AddressAppShowModule.swift index 5316569566..1c0fb27956 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Main/Workers/SendAppShowWorker/AddressAppShowModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Main/Workers/SendAppShowWorker/AddressAppShowModule.swift @@ -5,6 +5,7 @@ import UIKit class AddressAppShowModule { private let disposeBag = DisposeBag() private let parentViewController: UIViewController? + private let marketKit = App.shared.marketKit init(parentViewController: UIViewController?) { self.parentViewController = parentViewController @@ -27,19 +28,31 @@ class AddressAppShowModule { } } - private func showSendTokenList(uri: AddressUri, allowedBlockchainTypes: [BlockchainType]? = nil) { + private func showSendTokenList(source: StatPage, eventType: EventHandler.EventType, uri: AddressUri, allowedBlockchainTypes: [BlockchainType]? = nil) { let allowedBlockchainTypes = allowedBlockchainTypes ?? uri.allowedBlockchainTypes - var allowedTokenTypes: [TokenType]? + var allowedTokenType: TokenType? if let tokenUid: String = uri.value(field: .tokenUid), let tokenType = TokenType(id: tokenUid) { - allowedTokenTypes = [tokenType] + allowedTokenType = tokenType } + var token: Token? + if let allowedTokenType, + let blockchainUid: String = uri.value(field: .blockchainUid), + let blockchain = try? marketKit.blockchain(uid: blockchainUid), + let selectedToken = try? marketKit.token(query: .init(blockchainType: blockchain.type, tokenType: allowedTokenType)) + { + token = selectedToken + } + + let event = StatEvent.openSendTokenList(coinUid: token?.coin.uid, chainUid: token?.blockchain.uid) + stat(page: source, section: eventType.contains(.address) ? .qrScan : .deepLink, event: event) + guard let viewController = WalletModule.sendTokenListViewController( allowedBlockchainTypes: allowedBlockchainTypes, - allowedTokenTypes: allowedTokenTypes, + allowedTokenTypes: allowedTokenType.map { [$0] }, mode: .prefilled(address: uri.address, amount: uri.amount) ) else { return @@ -50,14 +63,14 @@ class AddressAppShowModule { extension AddressAppShowModule: IEventHandler { @MainActor - func handle(event: Any, eventType: EventHandler.EventType) async throws { + func handle(source: StatPage, event: Any, eventType: EventHandler.EventType) async throws { // check if we parse deeplink with transfer address if eventType.contains(.deepLink) { if let event = event as? DeepLinkManager.DeepLink { guard case let .transfer(parsed) = event else { throw EventHandler.HandleError.noSuitableHandler } - showSendTokenList(uri: parsed) + showSendTokenList(source: source, eventType: eventType, uri: parsed) } else { return } @@ -70,7 +83,7 @@ extension AddressAppShowModule: IEventHandler { } if let parsed = uri(text: text.trimmingCharacters(in: .whitespacesAndNewlines)) { - showSendTokenList(uri: parsed) + showSendTokenList(source: source, eventType: eventType, uri: parsed) } else { let disposeBag = DisposeBag() let chain = AddressParserFactory.parserChain(blockchainType: nil, withEns: false) @@ -90,7 +103,6 @@ extension AddressAppShowModule: IEventHandler { } var uri = AddressUri(scheme: "") uri.address = text - showSendTokenList(uri: uri, allowedBlockchainTypes: types) return } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Main/Workers/WalletConnectAppShowWorker/WalletConnectAppShowView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Main/Workers/WalletConnectAppShowWorker/WalletConnectAppShowView.swift index 429d2fbf53..9e97889fd1 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Main/Workers/WalletConnectAppShowWorker/WalletConnectAppShowView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Main/Workers/WalletConnectAppShowWorker/WalletConnectAppShowView.swift @@ -96,8 +96,8 @@ class WalletConnectAppShowView { return case let .controller(controller): guard let controller else { return } - let navigationController = ThemeNavigationController(rootViewController: controller) - parentViewController?.visibleController.present(navigationController, animated: true) + stat(page: .main, event: .open(page: .walletConnectRequest)) + parentViewController?.visibleController.present(controller, animated: true) } } } @@ -153,8 +153,9 @@ extension WalletConnectAppShowView { extension WalletConnectAppShowView: IEventHandler { var eventType: EventHandler.EventType { [.deepLink, .walletConnectUri] } - func handle(event: Any, eventType _: EventHandler.EventType) async throws { + func handle(source: StatPage, event: Any, eventType _: EventHandler.EventType) async throws { var uri: String? + switch event { case let event as String: uri = event @@ -175,6 +176,7 @@ extension WalletConnectAppShowView: IEventHandler { throw EventHandler.HandleError.noSuitableHandler } + stat(page: source, event: .walletConnectPair) try viewModel.handleWalletConnect(url: uri) isWaitingForSession = true diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Main/Workers/WidgetCoinAppShowWorker/WidgetCoinAppShowModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Main/Workers/WidgetCoinAppShowWorker/WidgetCoinAppShowModule.swift index e2cccc6c24..4000453215 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Main/Workers/WidgetCoinAppShowWorker/WidgetCoinAppShowModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Main/Workers/WidgetCoinAppShowWorker/WidgetCoinAppShowModule.swift @@ -10,7 +10,7 @@ class WidgetCoinAppShowModule { extension WidgetCoinAppShowModule: IEventHandler { @MainActor - func handle(event: Any, eventType: EventHandler.EventType) async throws { + func handle(source _: StatPage, event: Any, eventType: EventHandler.EventType) async throws { guard eventType.contains(.deepLink) else { throw EventHandler.HandleError.noSuitableHandler } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccounts/ManageAccountsViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccounts/ManageAccountsViewController.swift index 491b3fcd91..d07e80287e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccounts/ManageAccountsViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ManageAccounts/ManageAccountsViewController.swift @@ -173,7 +173,7 @@ extension ManageAccountsViewController: SectionsDataSource { CellBuilderNew.row( rootElement: .hStack([ .image24 { component in - component.imageView.image = viewItem.selected ? UIImage(named: "circle_radioon_24")?.withTintColor(.themeJacob) : UIImage(named: "circle_radiooff_24")?.withTintColor(.themeGray) + component.imageView.image = viewItem.selected ? UIImage(named: "circle_radioon_24") : UIImage(named: "circle_radiooff_24") }, .vStackCentered([ .text { component in diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ManageWallets/ManageWalletsViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ManageWallets/ManageWalletsViewController.swift index 1c2b682854..61730a34a0 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ManageWallets/ManageWalletsViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ManageWallets/ManageWalletsViewController.swift @@ -1,5 +1,6 @@ import Combine import ComponentKit +import MarketKit import RxCocoa import RxSwift import SectionsTableView @@ -133,9 +134,9 @@ class ManageWalletsViewController: ThemeSearchViewController { private func showBottomSheet(viewItem: ManageWalletsViewModel.CoinViewItem, items: [BottomSheetModule.Item]) { let viewController = BottomSheetModule.viewController( - image: .remote(url: viewItem.coinImageUrl, placeholder: viewItem.coinPlaceholderImageName), - title: viewItem.coinCode, - subtitle: viewItem.coinName, + image: .remote(url: viewItem.coin.imageUrl, placeholder: viewItem.coinPlaceholderImageName), + title: viewItem.coin.code, + subtitle: viewItem.coin.name, items: items ) @@ -163,14 +164,11 @@ extension ManageWalletsViewController: SectionsDataSource { private func rootElement(index: Int, viewItem: ManageWalletsViewModel.ViewItem, forceToggleOn: Bool? = nil) -> CellBuilderNew.CellElement { .hStack([ .image32 { component in - component.setImage( - urlString: viewItem.imageUrl, - placeholder: viewItem.placeholderImageName.flatMap { UIImage(named: $0) } - ) + component.imageView.setImage(coin: viewItem.coin, placeholder: viewItem.placeholderImageName) }, .vStackCentered([ .hStack([ - .textElement(text: .body(viewItem.title), parameters: .highHugging), + .textElement(text: .body(viewItem.coin.code), parameters: .highHugging), .margin8, .badge { component in component.isHidden = viewItem.badge == nil @@ -181,7 +179,7 @@ extension ManageWalletsViewController: SectionsDataSource { .text { _ in }, ]), .margin(1), - .textElement(text: .subhead2(viewItem.subtitle)), + .textElement(text: .subhead2(viewItem.coin.name)), ]), .secondaryCircleButton { [weak self] component in component.isHidden = !viewItem.hasInfo diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/ManageWallets/ManageWalletsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/ManageWallets/ManageWalletsViewModel.swift index 3cdfe211de..80ef0fa70f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/ManageWallets/ManageWalletsViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/ManageWallets/ManageWalletsViewModel.swift @@ -29,10 +29,8 @@ class ManageWalletsViewModel { return ViewItem( uid: String(item.token.hashValue), - imageUrl: token.coin.imageUrl, + coin: token.coin, placeholderImageName: token.placeholderImageName, - title: token.coin.code, - subtitle: token.coin.name, badge: item.token.badge, enabled: item.enabled, hasInfo: item.hasInfo @@ -88,10 +86,8 @@ extension ManageWalletsViewModel { } let coinViewItem = CoinViewItem( - coinImageUrl: infoItem.token.coin.imageUrl, - coinPlaceholderImageName: infoItem.token.placeholderImageName, - coinName: infoItem.token.coin.name, - coinCode: infoItem.token.coin.code + coin: infoItem.token.coin, + coinPlaceholderImageName: infoItem.token.placeholderImageName ) switch infoItem.type { @@ -119,20 +115,16 @@ extension ManageWalletsViewModel { extension ManageWalletsViewModel { struct ViewItem { let uid: String - let imageUrl: String + let coin: Coin let placeholderImageName: String? - let title: String - let subtitle: String let badge: String? let enabled: Bool let hasInfo: Bool } struct CoinViewItem { - let coinImageUrl: String + let coin: Coin let coinPlaceholderImageName: String - let coinName: String - let coinCode: String } struct InfoViewItem { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/AdvancedSearch/MarketAdvancedSearchBlockchainsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/AdvancedSearch/MarketAdvancedSearchBlockchainsView.swift new file mode 100644 index 0000000000..f431132e95 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/AdvancedSearch/MarketAdvancedSearchBlockchainsView.swift @@ -0,0 +1,60 @@ +import Kingfisher +import MarketKit +import SwiftUI + +struct MarketAdvancedSearchBlockchainsView: View { + @ObservedObject var viewModel: MarketAdvancedSearchViewModel + @Binding var isPresented: Bool + + var body: some View { + ThemeNavigationView { + ScrollableThemeView { + VStack(spacing: .margin24) { + ListSection { + ClickableRow { + viewModel.blockchains = Set() + } content: { + Text("selector.any".localized).themeBody(color: .themeGray) + + if viewModel.blockchains.isEmpty { + Image("check_1_20").themeIcon(color: .themeJacob) + } + } + + ForEach(viewModel.allBlockchains) { blockchain in + ClickableRow { + if viewModel.blockchains.contains(blockchain) { + viewModel.blockchains.remove(blockchain) + } else { + viewModel.blockchains.insert(blockchain) + } + } content: { + KFImage.url(URL(string: blockchain.type.imageUrl)) + .resizable() + .placeholder { RoundedRectangle(cornerRadius: .cornerRadius8).fill(Color.themeSteel20) } + .clipShape(RoundedRectangle(cornerRadius: .cornerRadius8)) + .frame(width: .iconSize32, height: .iconSize32) + + Text(blockchain.name).themeBody() + + if viewModel.blockchains.contains(blockchain) { + Image("check_1_20").themeIcon(color: .themeJacob) + } + } + } + } + } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) + } + .navigationTitle("market.advanced_search.blockchains".localized) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("button.done".localized) { + isPresented = false + } + } + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/AdvancedSearch/MarketAdvancedSearchResultsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/AdvancedSearch/MarketAdvancedSearchResultsView.swift new file mode 100644 index 0000000000..f10fc9465a --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/AdvancedSearch/MarketAdvancedSearchResultsView.swift @@ -0,0 +1,124 @@ +import Kingfisher +import MarketKit +import SwiftUI + +struct MarketAdvancedSearchResultsView: View { + @StateObject var viewModel: MarketAdvancedSearchResultsViewModel + @StateObject var watchlistViewModel: WatchlistViewModel + @Binding var isParentPresented: Bool + + @State private var sortBySelectorPresented = false + @State private var presentedFullCoin: FullCoin? + + init(marketInfos: [MarketInfo], timePeriod: HsTimePeriod, isParentPresented: Binding) { + _viewModel = StateObject(wrappedValue: MarketAdvancedSearchResultsViewModel(marketInfos: marketInfos, timePeriod: timePeriod)) + _watchlistViewModel = StateObject(wrappedValue: WatchlistViewModel(page: .advancedSearchResults)) + _isParentPresented = isParentPresented + } + + var body: some View { + ThemeView { + VStack(spacing: 0) { + header() + + ScrollViewReader { proxy in + ThemeList(viewModel.marketInfos, bottomSpacing: .margin16, invisibleTopView: true) { marketInfo in + let coin = marketInfo.fullCoin.coin + + ClickableRow(action: { + presentedFullCoin = marketInfo.fullCoin + }) { + itemContent( + coin: coin, + marketCap: marketInfo.marketCap, + price: marketInfo.price.flatMap { ValueFormatter.instance.formatFull(currency: viewModel.currency, value: $0) } ?? "n/a".localized, + rank: marketInfo.marketCapRank, + diff: marketInfo.priceChangeValue(timePeriod: viewModel.timePeriod) + ) + } + .watchlistSwipeActions(viewModel: watchlistViewModel, coinUid: coin.uid) + } + .onChange(of: viewModel.sortBy) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + } + } + } + .navigationTitle("market.advanced_search_results.title".localized) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("button.close".localized) { + isParentPresented = false + } + } + } + .sheet(item: $presentedFullCoin) { fullCoin in + CoinPageViewNew(coinUid: fullCoin.coin.uid).ignoresSafeArea() + .onFirstAppear { stat(page: .advancedSearchResults, event: .openCoin(coinUid: fullCoin.coin.uid)) } + } + .alert( + isPresented: $sortBySelectorPresented, + title: "market.sort_by.title".localized, + viewItems: viewModel.sortBys.map { .init(text: $0.title, selected: viewModel.sortBy == $0) }, + onTap: { index in + guard let index else { + return + } + + viewModel.sortBy = viewModel.sortBys[index] + } + ) + } + + @ViewBuilder private func header() -> some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + Button(action: { + sortBySelectorPresented = true + }) { + Text(viewModel.sortBy.title) + } + .buttonStyle(SecondaryButtonStyle(style: .default, rightAccessory: .dropDown)) + } + .padding(.horizontal, .margin16) + .padding(.vertical, .margin8) + } + .alert( + isPresented: $sortBySelectorPresented, + title: "market.sort_by.title".localized, + viewItems: viewModel.sortBys.map { .init(text: $0.title, selected: viewModel.sortBy == $0) }, + onTap: { index in + guard let index else { + return + } + + viewModel.sortBy = viewModel.sortBys[index] + } + ) + } + + @ViewBuilder private func itemContent(coin: Coin?, marketCap: Decimal?, price: String, rank: Int?, diff: Decimal?) -> some View { + CoinIconView(coin: coin) + + VStack(spacing: 1) { + HStack(spacing: .margin8) { + Text(coin?.code ?? "CODE").textBody() + Spacer() + Text(price).textBody() + } + + HStack(spacing: .margin8) { + HStack(spacing: .margin4) { + if let rank { + BadgeViewNew(text: "\(rank)") + } + + if let marketCap, let formatted = ValueFormatter.instance.formatShort(currency: viewModel.currency, value: marketCap) { + Text(formatted).textSubhead2() + } + } + Spacer() + DiffText(diff) + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/AdvancedSearch/MarketAdvancedSearchResultsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/AdvancedSearch/MarketAdvancedSearchResultsViewModel.swift new file mode 100644 index 0000000000..70fb2438ce --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/AdvancedSearch/MarketAdvancedSearchResultsViewModel.swift @@ -0,0 +1,40 @@ +import Combine +import Foundation +import MarketKit + +class MarketAdvancedSearchResultsViewModel: ObservableObject { + private let currencyManager = App.shared.currencyManager + private var cancellables = Set() + + private let internalMarketInfos: [MarketInfo] + let timePeriod: HsTimePeriod + + @Published var marketInfos: [MarketInfo] = [] + + var sortBy: MarketModule.SortBy = .highestCap { + didSet { + syncState() + } + } + + init(marketInfos: [MarketInfo], timePeriod: HsTimePeriod) { + internalMarketInfos = marketInfos + self.timePeriod = timePeriod + + syncState() + } + + private func syncState() { + marketInfos = internalMarketInfos.sorted(sortBy: sortBy, timePeriod: timePeriod) + } +} + +extension MarketAdvancedSearchResultsViewModel { + var currency: Currency { + currencyManager.baseCurrency + } + + var sortBys: [MarketModule.SortBy] { + [.highestCap, .lowestCap, .gainers, .losers] + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/AdvancedSearch/MarketAdvancedSearchView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/AdvancedSearch/MarketAdvancedSearchView.swift new file mode 100644 index 0000000000..68705b7178 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/AdvancedSearch/MarketAdvancedSearchView.swift @@ -0,0 +1,499 @@ +import MarketKit +import SwiftUI + +struct MarketAdvancedSearchView: View { + @StateObject var viewModel = MarketAdvancedSearchViewModel() + @Binding var isPresented: Bool + + @State var topPresented = false + @State var marketCapPresented = false + @State var volumePresented = false + @State var blockchainsPresented = false + @State var signalsPresented = false + @State var priceChangePresented = false + @State var pricePeriodPresented = false + @State var resultsPresented = false + + var body: some View { + ThemeNavigationView { + ThemeView { + BottomGradientWrapper { + ScrollView { + VStack(spacing: .margin24) { + ListSection { + topRow() + } + + VStack(spacing: 0) { + ListSectionHeader(text: "market.advanced_search.market_parameters".localized) + ListSection { + marketCapRow() + volumeRow() + listedOnTopExchangesRow() + goodCexVolumeRow() + goodDexVolumeRow() + goodDistributionRow() + } + } + + VStack(spacing: 0) { + ListSectionHeader(text: "market.advanced_search.network_parameters".localized) + ListSection { + blockchainsRow() + } + } + + VStack(spacing: 0) { + ListSectionHeader(text: "market.advanced_search.indicators".localized) + ListSection { + signalRow() + } + } + + VStack(spacing: 0) { + ListSectionHeader(text: "market.advanced_search.price_parameters".localized) + ListSection { + priceChangeRow() + pricePeriodRow() + outperformedBtcRow() + outperformedEthRow() + outperformedBnbRow() + priceCloseToAthRow() + priceCloseToAtlRow() + } + } + } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) + } + } bottomContent: { + switch viewModel.state { + case .loading: + Button {} label: { ProgressView() } + .buttonStyle(PrimaryButtonStyle(style: .yellow)) + .disabled(true) + case let .loaded(marketInfos): + Button { + resultsPresented = true + } label: { + Text(marketInfos.isEmpty ? "market.advanced_search.empty_results".localized : "\("market.advanced_search.show_results".localized): \(marketInfos.count)") + } + .buttonStyle(PrimaryButtonStyle(style: .yellow)) + .disabled(marketInfos.isEmpty) + case .failed: + Button { + viewModel.syncMarketInfos() + } label: { + Text("market.advanced_search.retry".localized) + } + .buttonStyle(PrimaryButtonStyle(style: .gray)) + } + } + + NavigationLink( + isActive: $resultsPresented, + destination: { + if case let .loaded(marketInfos) = viewModel.state { + MarketAdvancedSearchResultsView(marketInfos: marketInfos, timePeriod: viewModel.priceChangePeriod, isParentPresented: $isPresented) + } + } + ) { + EmptyView() + } + } + .navigationTitle("market.advanced_search.title".localized) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("market.advanced_search.reset_all".localized) { + viewModel.reset() + } + .disabled(!viewModel.canReset) + } + + ToolbarItem(placement: .navigationBarTrailing) { + Button("button.close".localized) { + isPresented = false + } + } + } + } + } + + @ViewBuilder private func topRow() -> some View { + ClickableRow(spacing: .margin8) { + topPresented = true + } content: { + Text("market.advanced_search.choose_set".localized).textBody() + Spacer() + Text(viewModel.top.title).textSubhead1(color: .themeLeah) + Image("arrow_small_down_20").themeIcon() + } + .bottomSheet(isPresented: $topPresented) { + VStack(spacing: 0) { + HStack(spacing: .margin16) { + Image("circle_coin_24").themeIcon(color: .themeJacob) + Text("market.advanced_search.choose_set".localized).themeHeadline2() + Button(action: { topPresented = false }) { Image("close_3_24").themeIcon() } + } + .padding(.horizontal, .margin32) + .padding(.vertical, .margin24) + + ListSection { + ForEach(viewModel.tops) { top in + ClickableRow { + viewModel.top = top + topPresented = false + } content: { + Text(top.title).themeBody() + + if viewModel.top == top { + Image("check_1_20").themeIcon(color: .themeJacob) + } + } + } + } + .themeListStyle(.bordered) + .padding(EdgeInsets(top: 0, leading: .margin16, bottom: .margin24, trailing: .margin16)) + } + } + } + + @ViewBuilder private func marketCapRow() -> some View { + ClickableRow(spacing: .margin8) { + marketCapPresented = true + } content: { + Text("market.advanced_search.market_cap".localized).textBody() + Spacer() + Text(viewModel.marketCap.title).textSubhead1(color: color(valueFilter: viewModel.marketCap)) + Image("arrow_small_down_20").themeIcon() + } + .bottomSheet(isPresented: $marketCapPresented) { + VStack(spacing: 0) { + HStack(spacing: .margin16) { + Image("usd_24").themeIcon(color: .themeJacob) + Text("market.advanced_search.market_cap".localized).themeHeadline2() + Button(action: { marketCapPresented = false }) { Image("close_3_24").themeIcon() } + } + .padding(.horizontal, .margin32) + .padding(.vertical, .margin24) + + ListSection { + ForEach(viewModel.valueFilters) { filter in + ClickableRow { + viewModel.marketCap = filter + marketCapPresented = false + } content: { + Text(filter.title).themeBody(color: color(valueFilter: filter)) + + if viewModel.marketCap == filter { + Image("check_1_20").themeIcon(color: .themeJacob) + } + } + } + } + .themeListStyle(.bordered) + .padding(EdgeInsets(top: 0, leading: .margin16, bottom: .margin24, trailing: .margin16)) + } + } + } + + @ViewBuilder private func volumeRow() -> some View { + ClickableRow(spacing: .margin8) { + volumePresented = true + } content: { + Text("market.advanced_search.volume".localized).textBody() + Spacer() + Text(viewModel.volume.title).textSubhead1(color: color(valueFilter: viewModel.volume)) + Image("arrow_small_down_20").themeIcon() + } + .bottomSheet(isPresented: $volumePresented) { + VStack(spacing: 0) { + HStack(spacing: .margin16) { + Image("chart_2_24").themeIcon(color: .themeJacob) + Text("market.advanced_search.volume".localized).themeHeadline2() + Button(action: { volumePresented = false }) { Image("close_3_24").themeIcon() } + } + .padding(.horizontal, .margin32) + .padding(.vertical, .margin24) + + ListSection { + ForEach(viewModel.valueFilters) { filter in + ClickableRow { + viewModel.volume = filter + volumePresented = false + } content: { + Text(filter.title).themeBody(color: color(valueFilter: filter)) + + if viewModel.volume == filter { + Image("check_1_20").themeIcon(color: .themeJacob) + } + } + } + } + .themeListStyle(.bordered) + .padding(EdgeInsets(top: 0, leading: .margin16, bottom: .margin24, trailing: .margin16)) + } + } + } + + @ViewBuilder private func listedOnTopExchangesRow() -> some View { + ListRow { + Toggle(isOn: $viewModel.listedOnTopExchanges) { + Text("market.advanced_search.listed_on_top_exchanges".localized).themeBody() + } + .toggleStyle(SwitchToggleStyle(tint: .themeYellow)) + } + } + + @ViewBuilder private func goodCexVolumeRow() -> some View { + ListRow { + Toggle(isOn: $viewModel.goodCexVolume) { + VStack(spacing: 1) { + Text("market.advanced_search.good_cex_volume".localized).themeBody() + Text("market.advanced_search.overall_score_is_good_or_excellent".localized).themeSubhead2() + } + } + .toggleStyle(SwitchToggleStyle(tint: .themeYellow)) + } + } + + @ViewBuilder private func goodDexVolumeRow() -> some View { + ListRow { + Toggle(isOn: $viewModel.goodDexVolume) { + VStack(spacing: 1) { + Text("market.advanced_search.good_dex_volume".localized).themeBody() + Text("market.advanced_search.overall_score_is_good_or_excellent".localized).themeSubhead2() + } + } + .toggleStyle(SwitchToggleStyle(tint: .themeYellow)) + } + } + + @ViewBuilder private func goodDistributionRow() -> some View { + ListRow { + Toggle(isOn: $viewModel.goodDistribution) { + VStack(spacing: 1) { + Text("market.advanced_search.good_distribution".localized).themeBody() + Text("market.advanced_search.overall_score_is_good_or_excellent".localized).themeSubhead2() + } + } + .toggleStyle(SwitchToggleStyle(tint: .themeYellow)) + } + } + + @ViewBuilder private func blockchainsRow() -> some View { + ClickableRow(spacing: .margin8) { + blockchainsPresented = true + } content: { + Text("market.advanced_search.blockchains".localized).textBody() + Spacer() + + if viewModel.blockchains.isEmpty { + Text("selector.any".localized).textSubhead1() + } else if viewModel.blockchains.count == 1, let blockchain = viewModel.blockchains.first { + Text(blockchain.name).textSubhead1(color: .themeLeah) + } else { + Text("\(viewModel.blockchains.count)").textSubhead1(color: .themeLeah) + } + + Image("arrow_small_down_20").themeIcon() + } + .sheet(isPresented: $blockchainsPresented) { + MarketAdvancedSearchBlockchainsView(viewModel: viewModel, isPresented: $blockchainsPresented) + } + } + + @ViewBuilder private func signalRow() -> some View { + ClickableRow(spacing: .margin8) { + signalsPresented = true + } content: { + Text("market.advanced_search.signal".localized).textBody() + Spacer() + + if let signal = viewModel.signal { + Text(signal.shortTitle).textSubhead1(color: .themeLeah) + } else { + Text("selector.any".localized).textSubhead1() + } + + Image("arrow_small_down_20").themeIcon() + } + .bottomSheet(isPresented: $signalsPresented) { + VStack(spacing: 0) { + HStack(spacing: .margin16) { + Image("bell_ring_24").themeIcon(color: .themeJacob) + Text("market.advanced_search.signal".localized).themeHeadline2() + Button(action: { signalsPresented = false }) { Image("close_3_24").themeIcon() } + } + .padding(.horizontal, .margin32) + .padding(.vertical, .margin24) + + ListSection { + ClickableRow { + viewModel.signal = nil + signalsPresented = false + } content: { + Text("selector.any".localized).themeBody(color: .themeGray) + + if viewModel.signal == nil { + Image("check_1_20").themeIcon(color: .themeJacob) + } + } + + ForEach(viewModel.signals) { signal in + ClickableRow { + viewModel.signal = signal + signalsPresented = false + } content: { + Text(signal.shortTitle).themeBody() + + if viewModel.signal == signal { + Image("check_1_20").themeIcon(color: .themeJacob) + } + } + } + } + .themeListStyle(.bordered) + .padding(EdgeInsets(top: 0, leading: .margin16, bottom: .margin24, trailing: .margin16)) + } + } + } + + @ViewBuilder private func priceChangeRow() -> some View { + ClickableRow(spacing: .margin8) { + priceChangePresented = true + } content: { + Text("market.advanced_search.price_change".localized).textBody() + Spacer() + Text(viewModel.priceChange.title).textSubhead1(color: color(priceChangeFilter: viewModel.priceChange)) + Image("arrow_small_down_20").themeIcon() + } + .bottomSheet(isPresented: $priceChangePresented) { + VStack(spacing: 0) { + HStack(spacing: .margin16) { + Image("markets_24").themeIcon(color: .themeJacob) + Text("market.advanced_search.price_change".localized).themeHeadline2() + Button(action: { priceChangePresented = false }) { Image("close_3_24").themeIcon() } + } + .padding(.horizontal, .margin32) + .padding(.vertical, .margin24) + + ListSection { + ForEach(MarketAdvancedSearchViewModel.PriceChangeFilter.allCases) { filter in + ClickableRow { + viewModel.priceChange = filter + priceChangePresented = false + } content: { + Text(filter.title).themeBody(color: color(priceChangeFilter: filter)) + + if viewModel.priceChange == filter { + Image("check_1_20").themeIcon(color: .themeJacob) + } + } + } + } + .themeListStyle(.bordered) + .padding(EdgeInsets(top: 0, leading: .margin16, bottom: .margin24, trailing: .margin16)) + } + } + } + + @ViewBuilder private func pricePeriodRow() -> some View { + ClickableRow(spacing: .margin8) { + pricePeriodPresented = true + } content: { + Text("market.advanced_search.price_period".localized).textBody() + Spacer() + Text(viewModel.priceChangePeriod.title).textSubhead1(color: .themeLeah) + Image("arrow_small_down_20").themeIcon() + } + .bottomSheet(isPresented: $pricePeriodPresented) { + VStack(spacing: 0) { + HStack(spacing: .margin16) { + Image("circle_clock_24").themeIcon(color: .themeJacob) + Text("market.advanced_search.price_period".localized).themeHeadline2() + Button(action: { pricePeriodPresented = false }) { Image("close_3_24").themeIcon() } + } + .padding(.horizontal, .margin32) + .padding(.vertical, .margin24) + + ListSection { + ForEach(viewModel.priceChangePeriods) { period in + ClickableRow { + viewModel.priceChangePeriod = period + pricePeriodPresented = false + } content: { + Text(period.title).themeBody() + + if viewModel.priceChangePeriod == period { + Image("check_1_20").themeIcon(color: .themeJacob) + } + } + } + } + .themeListStyle(.bordered) + .padding(EdgeInsets(top: 0, leading: .margin16, bottom: .margin24, trailing: .margin16)) + } + } + } + + @ViewBuilder private func outperformedBtcRow() -> some View { + ListRow { + Toggle(isOn: $viewModel.outperformedBtc) { + Text("market.advanced_search.outperformed_btc".localized).themeBody() + } + .toggleStyle(SwitchToggleStyle(tint: .themeYellow)) + } + } + + @ViewBuilder private func outperformedEthRow() -> some View { + ListRow { + Toggle(isOn: $viewModel.outperformedEth) { + Text("market.advanced_search.outperformed_eth".localized).themeBody() + } + .toggleStyle(SwitchToggleStyle(tint: .themeYellow)) + } + } + + @ViewBuilder private func outperformedBnbRow() -> some View { + ListRow { + Toggle(isOn: $viewModel.outperformedBnb) { + Text("market.advanced_search.outperformed_bnb".localized).themeBody() + } + .toggleStyle(SwitchToggleStyle(tint: .themeYellow)) + } + } + + @ViewBuilder private func priceCloseToAthRow() -> some View { + ListRow { + Toggle(isOn: $viewModel.priceCloseToAth) { + Text("market.advanced_search.price_close_to_ath".localized).themeBody() + } + .toggleStyle(SwitchToggleStyle(tint: .themeYellow)) + } + } + + @ViewBuilder private func priceCloseToAtlRow() -> some View { + ListRow { + Toggle(isOn: $viewModel.priceCloseToAtl) { + Text("market.advanced_search.price_close_to_atl".localized).themeBody() + } + .toggleStyle(SwitchToggleStyle(tint: .themeYellow)) + } + } + + private func color(valueFilter: MarketAdvancedSearchViewModel.ValueFilter) -> Color { + switch valueFilter { + case .none: return .themeGray + default: return .themeLeah + } + } + + private func color(priceChangeFilter: MarketAdvancedSearchViewModel.PriceChangeFilter) -> Color { + switch priceChangeFilter { + case .none: return .themeGray + case .plus10, .plus25, .plus50, .plus100: return .themeRemus + case .minus10, .minus25, .minus50, .minus75: return .themeLucian + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketAdvancedSearch/MarketAdvancedSearchService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/AdvancedSearch/MarketAdvancedSearchViewModel.swift similarity index 57% rename from UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketAdvancedSearch/MarketAdvancedSearchService.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/Market/AdvancedSearch/MarketAdvancedSearchViewModel.swift index 97cab402f2..826810565a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketAdvancedSearch/MarketAdvancedSearchService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/AdvancedSearch/MarketAdvancedSearchViewModel.swift @@ -1,10 +1,9 @@ +import Combine import Foundation import HsExtensions import MarketKit -import RxRelay -import RxSwift -class MarketAdvancedSearchService { +class MarketAdvancedSearchViewModel: ObservableObject { private let blockchainTypes: [BlockchainType] = [ .ethereum, .binanceSmartChain, @@ -25,12 +24,13 @@ class MarketAdvancedSearchService { .unsupported(uid: "tomochain"), .unsupported(uid: "xdai"), ] - - private var tasks = Set() private let allTimeDeltaPercent: Decimal = 10 - private let marketKit: MarketKit.Kit - private let currencyManager: CurrencyManager + private let marketKit = App.shared.marketKit + private let currencyManager = App.shared.currencyManager + private let priceChangeModeManager = App.shared.priceChangeModeManager + private var cancellables = Set() + private var tasks = Set() private var internalState: State = .loading { didSet { @@ -38,224 +38,115 @@ class MarketAdvancedSearchService { } } - private let stateRelay = PublishRelay() - private(set) var state: State = .loading { - didSet { - stateRelay.accept(state) - } - } + @Published private(set) var state: State = .loading - private let coinListCountRelay = PublishRelay() - var coinListCount: CoinListCount = .top250 { + @Published var top: MarketModule.Top = .top250 { didSet { - guard coinListCount != oldValue else { + guard top != oldValue else { return } - coinListCountRelay.accept(coinListCount) syncMarketInfos() } } - private let marketCapRelay = PublishRelay() - var marketCap: ValueFilter = .none { + @Published var marketCap: ValueFilter = .none { didSet { - guard marketCap != oldValue else { - return - } - - marketCapRelay.accept(marketCap) syncState() } } - private let volumeRelay = PublishRelay() - var volume: ValueFilter = .none { + @Published var volume: ValueFilter = .none { didSet { - guard volume != oldValue else { - return - } - - volumeRelay.accept(volume) syncState() } } - private let listedOnTopExchangesRelay = PublishRelay() - var listedOnTopExchanges: Bool = false { + @Published var listedOnTopExchanges = false { didSet { - guard listedOnTopExchanges != oldValue else { - return - } - - listedOnTopExchangesRelay.accept(listedOnTopExchanges) syncState() } } - private let goodCexVolumeRelay = PublishRelay() - var goodCexVolume: Bool = false { + @Published var goodCexVolume = false { didSet { - guard goodCexVolume != oldValue else { - return - } - - goodCexVolumeRelay.accept(goodCexVolume) syncState() } } - private let goodDexVolumeRelay = PublishRelay() - var goodDexVolume: Bool = false { + @Published var goodDexVolume = false { didSet { - guard goodDexVolume != oldValue else { - return - } - - goodDexVolumeRelay.accept(goodDexVolume) syncState() } } - private let goodDistributionRelay = PublishRelay() - var goodDistribution: Bool = false { + @Published var goodDistribution = false { didSet { - guard goodDistribution != oldValue else { - return - } - - goodDistributionRelay.accept(goodDistribution) syncState() } } - private let blockchainsRelay = PublishRelay<[Blockchain]>() - var blockchains: [Blockchain] = [] { + @Published var blockchains = Set() { didSet { - guard blockchains != oldValue else { - return - } - - blockchainsRelay.accept(blockchains) syncState() } } - private let technicalAdviceRelay = PublishRelay() - var technicalAdvice: TechnicalAdvice.Advice? { + @Published var signal: TechnicalAdvice.Advice? { didSet { - guard technicalAdvice != oldValue else { - return - } - - technicalAdviceRelay.accept(technicalAdvice) syncState() } } - private let priceChangeRelay = PublishRelay() - var priceChange: PriceChangeFilter = .none { + @Published var priceChange: PriceChangeFilter = .none { didSet { - guard priceChange != oldValue else { - return - } - - priceChangeRelay.accept(priceChange) syncState() } } - private let priceChangeTypeRelay = PublishRelay() - var priceChangeType: MarketModule.PriceChangeType = .day { + @Published var priceChangePeriod: HsTimePeriod { didSet { - guard priceChangeType != oldValue else { - return - } - - priceChangeTypeRelay.accept(priceChangeType) syncState() } } - private let outperformedBtcRelay = PublishRelay() - var outperformedBtc: Bool = false { + @Published var outperformedBtc = false { didSet { - guard outperformedBtc != oldValue else { - return - } - - outperformedBtcRelay.accept(outperformedBtc) syncState() } } - private let outperformedEthRelay = PublishRelay() - var outperformedEth: Bool = false { + @Published var outperformedEth = false { didSet { - guard outperformedEth != oldValue else { - return - } - - outperformedEthRelay.accept(outperformedEth) syncState() } } - private let outperformedBnbRelay = PublishRelay() - var outperformedBnb: Bool = false { + @Published var outperformedBnb = false { didSet { - guard outperformedBnb != oldValue else { - return - } - - outperformedBnbRelay.accept(outperformedBnb) syncState() } } - private let priceCloseToAthRelay = PublishRelay() - var priceCloseToAth: Bool = false { + @Published var priceCloseToAth = false { didSet { - guard priceCloseToAth != oldValue else { - return - } - - priceCloseToAthRelay.accept(priceCloseToAth) syncState() } } - private let priceCloseToAtlRelay = PublishRelay() - var priceCloseToAtl: Bool = false { + @Published var priceCloseToAtl = false { didSet { - guard priceCloseToAtl != oldValue else { - return - } - - priceCloseToAtlRelay.accept(priceCloseToAtl) syncState() } } - private var canResetRelay = PublishRelay() - private(set) var canReset: Bool = false { - didSet { - if canReset != oldValue { - canResetRelay.accept(canReset) - } - } - } - - var currencyCode: String { - currencyManager.baseCurrency.code - } + @Published var canReset = false let allBlockchains: [Blockchain] - init(marketKit: MarketKit.Kit, currencyManager: CurrencyManager) { - self.marketKit = marketKit - self.currencyManager = currencyManager + private var syncStateEnabled = true + init() { do { let blockchains = try marketKit.blockchains(uids: blockchainTypes.map(\.uid)) allBlockchains = blockchainTypes.compactMap { type in blockchains.first(where: { $0.type == type }) } @@ -263,25 +154,24 @@ class MarketAdvancedSearchService { allBlockchains = [] } - syncMarketInfos() - } + priceChangePeriod = priceChangeModeManager.day1Period - private func syncMarketInfos() { - tasks = Set() - - internalState = .loading - - Task { [weak self, marketKit, coinListCount, currencyManager] in - do { - let marketInfos = try await marketKit.advancedMarketInfos(top: coinListCount.rawValue, currencyCode: currencyManager.baseCurrency.code) - self?.internalState = .loaded(marketInfos: marketInfos) - } catch { - self?.internalState = .failed(error: error) + priceChangeModeManager.$priceChangeMode + .sink { [weak self] _ in + if let strongSelf = self { + strongSelf.priceChangePeriod = strongSelf.priceChangeModeManager.day1Period + } } - }.store(in: &tasks) + .store(in: &cancellables) + + syncMarketInfos() } private func syncState() { + guard syncStateEnabled else { + return + } + switch internalState { case .loading: state = .loading @@ -291,17 +181,17 @@ class MarketAdvancedSearchService { state = .failed(error: error) } - canReset = coinListCount != .top250 + canReset = top != .top250 || marketCap != .none || volume != .none || listedOnTopExchanges != false || goodCexVolume != false || goodDexVolume != false || goodDistribution != false - || blockchains != [] - || technicalAdvice != nil - || priceChangeType != .day + || !blockchains.isEmpty + || signal != nil || priceChange != .none + || priceChangePeriod != priceChangeModeManager.day1Period || outperformedBtc != false || outperformedEth != false || outperformedBnb != false @@ -309,12 +199,25 @@ class MarketAdvancedSearchService { || priceCloseToAth != false } - private func marketInfo(coinUid: String) -> MarketInfo? { - guard case let .loaded(marketInfos) = internalState else { - return nil - } + private func filtered(marketInfos: [MarketInfo]) -> [MarketInfo] { + marketInfos.filter { marketInfo in + let priceChangeValue = marketInfo.priceChangeValue(timePeriod: priceChangePeriod) - return marketInfos.first { $0.fullCoin.coin.uid == coinUid } + return inBounds(value: marketInfo.marketCap, lower: marketCap.lowerBound, upper: marketCap.upperBound) && + inBounds(value: marketInfo.totalVolume, lower: volume.lowerBound, upper: volume.upperBound) && + (!listedOnTopExchanges || marketInfo.listedOnTopExchanges == true) && + (!goodCexVolume || marketInfo.solidCex == true) && + (!goodDexVolume || marketInfo.solidDex == true) && + (!goodDistribution || marketInfo.goodDistribution == true) && + inBlockchain(tokens: marketInfo.fullCoin.tokens) && + filteredBySignal(marketInfo: marketInfo) && + inBounds(value: priceChangeValue, lower: priceChange.lowerBound, upper: priceChange.upperBound) && + (!outperformedBtc || outperformed(value: priceChangeValue, coinUid: "bitcoin")) && + (!outperformedEth || outperformed(value: priceChangeValue, coinUid: "ethereum")) && + (!outperformedBnb || outperformed(value: priceChangeValue, coinUid: "binancecoin")) && + (!priceCloseToAth || closedToAllTime(value: marketInfo.athPercentage)) && + (!priceCloseToAtl || closedToAllTime(value: marketInfo.atlPercentage)) + } } private func inBounds(value: Decimal?, lower: Decimal, upper: Decimal) -> Bool { @@ -325,25 +228,6 @@ class MarketAdvancedSearchService { return value >= lower && value <= upper } - private func outperformed(value: Decimal?, coinUid: String) -> Bool { - guard let marketInfo = marketInfo(coinUid: coinUid), - let value, - let priceChangeValue = marketInfo.priceChangeValue(type: priceChangeType) - else { - return false - } - - return value > priceChangeValue - } - - private func closedToAllTime(value: Decimal?) -> Bool { - guard let value else { - return false - } - - return abs(value) < allTimeDeltaPercent - } - private func inBlockchain(tokens: [Token]?) -> Bool { guard !blockchains.isEmpty else { return true @@ -362,8 +246,8 @@ class MarketAdvancedSearchService { return false } - private func filteredByAdvice(marketInfo: MarketInfo) -> Bool { - guard let technicalAdvice else { + private func filteredBySignal(marketInfo: MarketInfo) -> Bool { + guard let signal else { return true } @@ -371,147 +255,126 @@ class MarketAdvancedSearchService { return false } - if technicalAdvice.isRisky, infoAdvice.isRisky { + if signal.isRisky, infoAdvice.isRisky { return true } - return technicalAdvice == infoAdvice + return signal == infoAdvice } - private func filtered(marketInfos: [MarketInfo]) -> [MarketInfo] { - marketInfos.filter { marketInfo in - let priceChangeValue = marketInfo.priceChangeValue(type: priceChangeType) - - return inBounds(value: marketInfo.marketCap, lower: marketCap.lowerBound, upper: marketCap.upperBound) && - inBounds(value: marketInfo.totalVolume, lower: volume.lowerBound, upper: volume.upperBound) && - (!listedOnTopExchanges || marketInfo.listedOnTopExchanges == true) && - (!goodCexVolume || marketInfo.solidCex == true) && - (!goodDexVolume || marketInfo.solidDex == true) && - (!goodDistribution || marketInfo.goodDistribution == true) && - inBlockchain(tokens: marketInfo.fullCoin.tokens) && - filteredByAdvice(marketInfo: marketInfo) && - inBounds(value: priceChangeValue, lower: priceChange.lowerBound, upper: priceChange.upperBound) && - (!outperformedBtc || outperformed(value: priceChangeValue, coinUid: "bitcoin")) && - (!outperformedEth || outperformed(value: priceChangeValue, coinUid: "ethereum")) && - (!outperformedBnb || outperformed(value: priceChangeValue, coinUid: "binancecoin")) && - (!priceCloseToAth || closedToAllTime(value: marketInfo.athPercentage)) && - (!priceCloseToAtl || closedToAllTime(value: marketInfo.atlPercentage)) + private func outperformed(value: Decimal?, coinUid: String) -> Bool { + guard let marketInfo = marketInfo(coinUid: coinUid), + let value, + let priceChangeValue = marketInfo.priceChangeValue(timePeriod: priceChangePeriod) + else { + return false } - } -} - -extension MarketAdvancedSearchService { - var stateObservable: Observable { - stateRelay.asObservable() - } - - var coinListObservable: Observable { - coinListCountRelay.asObservable() - } - - var marketCapObservable: Observable { - marketCapRelay.asObservable() - } - - var volumeObservable: Observable { - volumeRelay.asObservable() - } - - var listedOnTopExchangesObservable: Observable { - listedOnTopExchangesRelay.asObservable() - } - var goodCexVolumeObservable: Observable { - goodCexVolumeRelay.asObservable() + return value > priceChangeValue } - var goodDexVolumeObservable: Observable { - goodDexVolumeRelay.asObservable() - } + private func marketInfo(coinUid: String) -> MarketInfo? { + guard case let .loaded(marketInfos) = internalState else { + return nil + } - var goodDistributionObservable: Observable { - goodDistributionRelay.asObservable() + return marketInfos.first { $0.fullCoin.coin.uid == coinUid } } - var blockchainsObservable: Observable<[Blockchain]> { - blockchainsRelay.asObservable() - } + private func closedToAllTime(value: Decimal?) -> Bool { + guard let value else { + return false + } - var technicalAdviceObservable: Observable { - technicalAdviceRelay.asObservable() + return abs(value) < allTimeDeltaPercent } +} - var priceChangeObservable: Observable { - priceChangeRelay.asObservable() +extension MarketAdvancedSearchViewModel { + var tops: [MarketModule.Top] { + [.top100, .top250, .top500, .top1000, .top1500] } - var priceChangeTypeObservable: Observable { - priceChangeTypeRelay.asObservable() + var valueFilters: [ValueFilter] { + switch currencyManager.baseCurrency.code { + case "USD": return [.none, .lessM5, .m5m20, .m20m100, .m100b1, .b1b5, .moreB5] + case "EUR": return [.none, .lessM5, .m5m20, .m20m100, .m100b1, .b1b5, .moreB5] + case "GBP": return [.none, .lessM5, .m5m20, .m20m100, .m100b1, .b1b5, .moreB5] + case "JPY": return [.none, .lessM500, .m500b2, .b2b10, .b10b100, .b100b500, .moreB500] + case "AUD": return [.none, .lessM5, .m5m20, .m20m100, .m100b1, .b1b5, .moreB5] + case "BRL": return [.none, .lessM50, .m50m200, .m200b1, .b1b10, .b10b50, .moreB50] + case "CAD": return [.none, .lessM5, .m5m20, .m20m100, .m100b1, .b1b5, .moreB5] + case "CHF": return [.none, .lessM5, .m5m20, .m20m100, .m100b1, .b1b5, .moreB5] + case "CNY": return [.none, .lessM50, .m50m200, .m200b1, .b1b10, .b10b50, .moreB50] + case "HKD": return [.none, .lessM50, .m50m200, .m200b1, .b1b10, .b10b50, .moreB50] + case "ILS": return [.none, .lessM10, .m10m40, .m40m200, .m200b2, .b2b10, .moreB10] + case "RUB": return [.none, .lessM500, .m500b2, .b2b10, .b10b100, .b100b500, .moreB500] + case "SGD": return [.none, .lessM5, .m5m20, .m20m100, .m100b1, .b1b5, .moreB5] + default: return [] + } } - var outperformedBtcObservable: Observable { - outperformedBtcRelay.asObservable() + var signals: [TechnicalAdvice.Advice] { + [.strongBuy, .buy, .neutral, .sell, .strongSell, .overbought] } - var outperformedEthObservable: Observable { - outperformedEthRelay.asObservable() + var priceChangePeriods: [HsTimePeriod] { + [priceChangeModeManager.day1Period, .week1, .week2, .month1, .month6, .year1] } - var outperformedBnbObservable: Observable { - outperformedBnbRelay.asObservable() - } + func syncMarketInfos() { + tasks = Set() - var priceCloseToAthObservable: Observable { - priceCloseToAthRelay.asObservable() - } + internalState = .loading - var priceCloseToAtlObservable: Observable { - priceCloseToAtlRelay.asObservable() - } + Task { [weak self, marketKit, top, currencyManager] in + do { + let marketInfos = try await marketKit.advancedMarketInfos(top: top.rawValue, currencyCode: currencyManager.baseCurrency.code) - var canResetObservable: Observable { - canResetRelay.asObservable() + await MainActor.run { [weak self] in + self?.internalState = .loaded(marketInfos: marketInfos) + } + } catch { + await MainActor.run { [weak self] in + self?.internalState = .failed(error: error) + } + } + }.store(in: &tasks) } func reset() { - coinListCount = .top250 + syncStateEnabled = false + + top = .top250 marketCap = .none volume = .none - listedOnTopExchanges = false goodDexVolume = false goodCexVolume = false goodDistribution = false - - blockchains = [] - technicalAdvice = nil - priceChangeType = .day + blockchains = Set() + signal = nil priceChange = .none - + priceChangePeriod = priceChangeModeManager.day1Period outperformedBtc = false outperformedEth = false outperformedBnb = false priceCloseToAtl = false priceCloseToAth = false + + syncStateEnabled = true + syncState() } } -extension MarketAdvancedSearchService { +extension MarketAdvancedSearchViewModel { enum State { case loading case loaded(marketInfos: [MarketInfo]) case failed(error: Error) } - enum CoinListCount: Int, CaseIterable { - case top100 = 100 - case top250 = 250 - case top500 = 500 - case top1000 = 1000 - case top1500 = 1500 - } - - enum ValueFilter: CaseIterable { + enum ValueFilter: CaseIterable, Identifiable { case none case lessM5 case lessM10 @@ -537,6 +400,39 @@ extension MarketAdvancedSearchService { case moreB50 case moreB500 + var id: Self { + self + } + + var title: String { + switch self { + case .none: return "selector.any".localized + case .lessM5: return "market.advanced_search.less_5_m".localized + case .lessM10: return "market.advanced_search.less_10_m".localized + case .lessM50: return "market.advanced_search.less_50_m".localized + case .lessM500: return "market.advanced_search.less_500_m".localized + case .m5m20: return "market.advanced_search.m_5_m_20".localized + case .m10m40: return "market.advanced_search.m_10_m_40".localized + case .m40m200: return "market.advanced_search.m_40_m_200".localized + case .m50m200: return "market.advanced_search.m_50_m_200".localized + case .m20m100: return "market.advanced_search.m_20_m_100".localized + case .m100b1: return "market.advanced_search.m_100_b_1".localized + case .m200b1: return "market.advanced_search.m_200_b_1".localized + case .m200b2: return "market.advanced_search.m_200_b_2".localized + case .m500b2: return "market.advanced_search.m_500_b_2".localized + case .b1b5: return "market.advanced_search.b_1_b_5".localized + case .b1b10: return "market.advanced_search.b_1_b_10".localized + case .b2b10: return "market.advanced_search.b_2_b_10".localized + case .b10b50: return "market.advanced_search.b_10_b_50".localized + case .b10b100: return "market.advanced_search.b_10_b_100".localized + case .b100b500: return "market.advanced_search.b_100_b_500".localized + case .moreB5: return "market.advanced_search.more_5_b".localized + case .moreB10: return "market.advanced_search.more_10_b".localized + case .moreB50: return "market.advanced_search.more_50_b".localized + case .moreB500: return "market.advanced_search.more_500_b".localized + } + } + var lowerBound: Decimal { switch self { case .none, .lessM5, .lessM10, .lessM50, .lessM500: return 0 @@ -581,7 +477,7 @@ extension MarketAdvancedSearchService { } } - enum PriceChangeFilter: CaseIterable { + enum PriceChangeFilter: CaseIterable, Identifiable { case none case plus10 case plus25 @@ -592,6 +488,24 @@ extension MarketAdvancedSearchService { case minus50 case minus75 + var id: Self { + self + } + + var title: String { + switch self { + case .none: return "selector.any".localized + case .plus10: return "> +10 %" + case .plus25: return "> +25 %" + case .plus50: return "> +50 %" + case .plus100: return "> +100 %" + case .minus10: return "< -10 %" + case .minus25: return "< -25 %" + case .minus50: return "< -50 %" + case .minus75: return "< -75 %" + } + } + var lowerBound: Decimal { switch self { case .none: return Decimal.leastFiniteMagnitude diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Coins/MarketCoinsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Coins/MarketCoinsView.swift new file mode 100644 index 0000000000..fea10d3364 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Coins/MarketCoinsView.swift @@ -0,0 +1,178 @@ +import Kingfisher +import MarketKit +import SwiftUI + +struct MarketCoinsView: View { + @ObservedObject var viewModel: MarketCoinsViewModel + @ObservedObject var watchlistViewModel: WatchlistViewModel + + @State private var sortBySelectorPresented = false + @State private var topSelectorPresented = false + @State private var timePeriodSelectorPresented = false + + @State private var presentedFullCoin: FullCoin? + + var body: some View { + ThemeView { + switch viewModel.state { + case .loading: + VStack(spacing: 0) { + header(disabled: true) + loadingList() + } + case let .loaded(marketInfos): + VStack(spacing: 0) { + header() + list(marketInfos: marketInfos) + } + case .failed: + SyncErrorView { + Task { + await viewModel.refresh() + } + } + } + } + .sheet(item: $presentedFullCoin) { fullCoin in + CoinPageViewNew(coinUid: fullCoin.coin.uid).ignoresSafeArea() + .onFirstAppear { stat(page: .markets, section: .coins, event: .openCoin(coinUid: fullCoin.coin.uid)) } + } + } + + @ViewBuilder private func header(disabled: Bool = false) -> some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + Button(action: { + sortBySelectorPresented = true + }) { + Text(viewModel.sortBy.title) + } + .buttonStyle(SecondaryButtonStyle(style: .default, rightAccessory: .dropDown)) + .disabled(disabled) + + Button(action: { + topSelectorPresented = true + }) { + Text(viewModel.top.title) + } + .buttonStyle(SecondaryButtonStyle(style: .default, rightAccessory: .dropDown)) + .disabled(disabled) + + Button(action: { + timePeriodSelectorPresented = true + }) { + Text(viewModel.timePeriod.shortTitle) + } + .buttonStyle(SecondaryButtonStyle(style: .default, rightAccessory: .dropDown)) + .disabled(disabled) + } + .padding(.horizontal, .margin16) + .padding(.vertical, .margin8) + } + .alert( + isPresented: $sortBySelectorPresented, + title: "market.sort_by.title".localized, + viewItems: viewModel.sortBys.map { .init(text: $0.title, selected: viewModel.sortBy == $0) }, + onTap: { index in + guard let index else { + return + } + + viewModel.sortBy = viewModel.sortBys[index] + } + ) + .alert( + isPresented: $topSelectorPresented, + title: "market.top_coins.title".localized, + viewItems: viewModel.tops.map { .init(text: $0.title, selected: viewModel.top == $0) }, + onTap: { index in + guard let index else { + return + } + + viewModel.top = viewModel.tops[index] + } + ) + .alert( + isPresented: $timePeriodSelectorPresented, + title: "market.time_period.title".localized, + viewItems: viewModel.timePeriods.map { .init(text: $0.title, selected: viewModel.timePeriod == $0) }, + onTap: { index in + guard let index else { + return + } + + viewModel.timePeriod = viewModel.timePeriods[index] + } + ) + } + + @ViewBuilder private func list(marketInfos: [MarketInfo]) -> some View { + ScrollViewReader { proxy in + ThemeList(marketInfos, invisibleTopView: true) { marketInfo in + let coin = marketInfo.fullCoin.coin + + ClickableRow(action: { + presentedFullCoin = marketInfo.fullCoin + }) { + itemContent( + coin: coin, + marketCap: marketInfo.marketCap, + price: marketInfo.price.flatMap { ValueFormatter.instance.formatFull(currency: viewModel.currency, value: $0) } ?? "n/a".localized, + rank: marketInfo.marketCapRank, + diff: marketInfo.priceChangeValue(timePeriod: viewModel.timePeriod) + ) + } + .watchlistSwipeActions(viewModel: watchlistViewModel, coinUid: coin.uid) + } + .refreshable { + await viewModel.refresh() + } + .onChange(of: viewModel.sortBy) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + .onChange(of: viewModel.top) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + .onChange(of: viewModel.timePeriod) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + } + } + + @ViewBuilder private func loadingList() -> some View { + ThemeList(Array(0 ... 10)) { index in + ListRow { + itemContent( + coin: nil, + marketCap: 123_456, + price: "$123.45", + rank: 12, + diff: index % 2 == 0 ? 12.34 : -12.34 + ) + .redacted() + } + } + .simultaneousGesture(DragGesture(minimumDistance: 0), including: .all) + } + + @ViewBuilder private func itemContent(coin: Coin?, marketCap: Decimal?, price: String, rank: Int?, diff: Decimal?) -> some View { + CoinIconView(coin: coin) + + VStack(spacing: 1) { + HStack(spacing: .margin8) { + Text(coin?.code ?? "CODE").textBody() + Spacer() + Text(price).textBody() + } + + HStack(spacing: .margin8) { + HStack(spacing: .margin4) { + if let rank { + BadgeViewNew(text: "\(rank)") + } + + if let marketCap, let formatted = ValueFormatter.instance.formatShort(currency: viewModel.currency, value: marketCap) { + Text(formatted).textSubhead2() + } + } + Spacer() + DiffText(diff) + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Coins/MarketCoinsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Coins/MarketCoinsViewModel.swift new file mode 100644 index 0000000000..d0a055916e --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Coins/MarketCoinsViewModel.swift @@ -0,0 +1,141 @@ +import Combine +import Foundation +import HsExtensions +import MarketKit + +class MarketCoinsViewModel: ObservableObject { + private let marketKit = App.shared.marketKit + private let currencyManager = App.shared.currencyManager + private let appManager = App.shared.appManager + private let priceChangeModeManager = App.shared.priceChangeModeManager + + private var cancellables = Set() + private var tasks = Set() + + private var internalState: State = .loading { + didSet { + syncState() + } + } + + @Published var state: State = .loading + + var sortBy: MarketModule.SortBy = .gainers { + didSet { + stat(page: .markets, section: .coins, event: .switchSortType(sortType: sortBy.statSortType)) + syncState() + } + } + + var top: MarketModule.Top = .top100 { + didSet { + stat(page: .markets, event: .switchMarketTop(marketTop: top.statMarketTop)) + syncState() + } + } + + var timePeriod: HsTimePeriod { + didSet { + stat(page: .markets, section: .coins, event: .switchPeriod(period: timePeriod.statPeriod)) + syncState() + } + } + + init() { + timePeriod = priceChangeModeManager.day1Period + + priceChangeModeManager.$priceChangeMode + .sink { [weak self] _ in + self?.syncPeriod() + } + .store(in: &cancellables) + } + + private func syncPeriod() { + timePeriod = priceChangeModeManager.convert(period: timePeriod) + } + + private func syncMarketInfos() { + tasks = Set() + + Task { [weak self] in + await self?._syncMarketInfos() + }.store(in: &tasks) + } + + private func _syncMarketInfos() async { + if case .failed = state { + await MainActor.run { [weak self] in + self?.internalState = .loading + } + } + + do { + let marketInfos = try await marketKit.topCoinsMarketInfos(top: MarketModule.Top.top500.rawValue, currencyCode: currency.code) + + await MainActor.run { [weak self] in + self?.internalState = .loaded(marketInfos: marketInfos) + } + } catch { + await MainActor.run { [weak self] in + self?.internalState = .failed(error: error) + } + } + } + + private func syncState() { + switch internalState { + case .loading: + state = .loading + case let .loaded(marketInfos): + let marketInfos: [MarketInfo] = Array(marketInfos.prefix(top.rawValue)) + state = .loaded(marketInfos: marketInfos.sorted(sortBy: sortBy, timePeriod: timePeriod)) + case let .failed(error): + state = .failed(error: error) + } + } +} + +extension MarketCoinsViewModel { + var currency: Currency { + currencyManager.baseCurrency + } + + var sortBys: [MarketModule.SortBy] { + [.highestCap, .lowestCap, .gainers, .losers] + } + + var tops: [MarketModule.Top] { + [.top100, .top200, .top300, .top500] + } + + var timePeriods: [HsTimePeriod] { + [priceChangeModeManager.day1Period, .week1, .month1, .month3] + } + + func load() { + currencyManager.$baseCurrency + .sink { [weak self] _ in + self?.syncMarketInfos() + } + .store(in: &cancellables) + + appManager.willEnterForegroundPublisher + .sink { [weak self] in self?.syncMarketInfos() } + .store(in: &cancellables) + + syncMarketInfos() + } + + func refresh() async { + await _syncMarketInfos() + } +} + +extension MarketCoinsViewModel { + enum State { + case loading + case loaded(marketInfos: [MarketInfo]) + case failed(error: Error) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Etf/MarketEtfView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Etf/MarketEtfView.swift new file mode 100644 index 0000000000..34de46f735 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Etf/MarketEtfView.swift @@ -0,0 +1,209 @@ +import Kingfisher +import MarketKit +import SwiftUI + +struct MarketEtfView: View { + @StateObject var viewModel: MarketEtfViewModel + @StateObject var chartViewModel: MetricChartViewModel + @Binding var isPresented: Bool + + @State private var sortBySelectorPresented = false + @State private var timePeriodSelectorPresented = false + + init(isPresented: Binding) { + _viewModel = StateObject(wrappedValue: MarketEtfViewModel()) + _chartViewModel = StateObject(wrappedValue: MetricChartViewModel.etfInstance) + _isPresented = isPresented + } + + var body: some View { + ThemeNavigationView { + ThemeView { + switch viewModel.state { + case .loading: + VStack(spacing: 0) { + header() + Spacer() + ProgressView() + Spacer() + } + case let .loaded(rankedEtfs): + ScrollViewReader { proxy in + ThemeList(bottomSpacing: .margin16, invisibleTopView: true) { + header() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + + chart() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + + list(rankedEtfs: rankedEtfs) + } + .onChange(of: viewModel.sortBy) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + .onChange(of: viewModel.timePeriod) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + } + case .failed: + VStack(spacing: 0) { + header() + + SyncErrorView { + viewModel.sync() + } + } + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("button.close".localized) { + isPresented = false + } + } + } + } + } + + @ViewBuilder private func header() -> some View { + HStack(spacing: .margin32) { + VStack(spacing: .margin8) { + Text("market.etf.title".localized).themeHeadline1() + Text("market.etf.description".localized).themeSubhead2() + } + .padding(.vertical, .margin12) + + KFImage.url(URL(string: "ETF_bitcoin".headerImageUrl)) + .resizable() + .frame(width: 76, height: 108) + } + .padding(.leading, .margin16) + } + + @ViewBuilder private func chart() -> some View { + ChartView(viewModel: chartViewModel, configuration: .baseHistogramChart) + .frame(maxWidth: .infinity) + .onFirstAppear { + chartViewModel.start() + } + } + + @ViewBuilder private func listHeader(disabled: Bool = false) -> some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + Button(action: { + sortBySelectorPresented = true + }) { + Text(viewModel.sortBy.title) + } + .buttonStyle(SecondaryButtonStyle(style: .default, rightAccessory: .dropDown)) + .disabled(disabled) + + Button(action: { + timePeriodSelectorPresented = true + }) { + Text(viewModel.timePeriod.shortTitle) + } + .buttonStyle(SecondaryButtonStyle(style: .default, rightAccessory: .dropDown)) + .disabled(disabled) + } + .padding(.horizontal, .margin16) + .padding(.vertical, .margin8) + } + .alert( + isPresented: $sortBySelectorPresented, + title: "market.sort_by.title".localized, + viewItems: MarketEtfViewModel.SortBy.allCases.map { .init(text: $0.title, selected: viewModel.sortBy == $0) }, + onTap: { index in + guard let index else { + return + } + + viewModel.sortBy = MarketEtfViewModel.SortBy.allCases[index] + } + ) + .alert( + isPresented: $timePeriodSelectorPresented, + title: "market.time_period.title".localized, + viewItems: viewModel.timePeriods.map { .init(text: $0.title, selected: viewModel.timePeriod == $0) }, + onTap: { index in + guard let index else { + return + } + + viewModel.timePeriod = viewModel.timePeriods[index] + } + ) + } + + @ViewBuilder private func list(rankedEtfs: [RankedEtf]) -> some View { + Section { + ListForEach(rankedEtfs) { rankedEtf in + let etf = rankedEtf.etf + + ListRow { + itemContent( + imageUrl: URL(string: etf.imageUrl), + ticker: etf.ticker, + name: etf.name, + rank: rankedEtf.rank, + totalAssets: etf.totalAssets, + change: etf.inflow(timePeriod: viewModel.timePeriod) + ) + } + } + } header: { + listHeader() + .listRowInsets(EdgeInsets()) + .background(Color.themeTyler) + } + } + + @ViewBuilder private func loadingList() -> some View { + Section { + ListForEach(Array(0 ... 10)) { index in + ListRow { + itemContent( + imageUrl: nil, + ticker: "ABCD", + name: "Ticker Name", + rank: 12, + totalAssets: 123_345_678, + change: index % 2 == 0 ? 123_456 : -123_456 + ) + .redacted() + } + } + } header: { + listHeader() + .listRowInsets(EdgeInsets()) + .background(Color.themeTyler) + } + } + + @ViewBuilder private func itemContent(imageUrl: URL?, ticker: String, name: String, rank: Int, totalAssets: Decimal?, change: Decimal?) -> some View { + KFImage.url(imageUrl) + .resizable() + .placeholder { RoundedRectangle(cornerRadius: .cornerRadius8).fill(Color.themeSteel20) } + .clipShape(RoundedRectangle(cornerRadius: .cornerRadius8)) + .frame(width: .iconSize32, height: .iconSize32) + + VStack(spacing: 1) { + HStack(spacing: .margin8) { + Text(ticker).textBody() + Spacer() + Text(totalAssets.flatMap { ValueFormatter.instance.formatShort(currency: viewModel.currency, value: $0) } ?? "n/a".localized).textBody() + } + + HStack(spacing: .margin8) { + HStack(spacing: .margin4) { + BadgeViewNew(text: "\(rank)") + Text(name).textSubhead2() + } + Spacer() + DiffText(change, currency: viewModel.currency) + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Etf/MarketEtfViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Etf/MarketEtfViewModel.swift new file mode 100644 index 0000000000..6484b58325 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Etf/MarketEtfViewModel.swift @@ -0,0 +1,128 @@ +import Combine +import Foundation +import HsExtensions +import MarketKit + +class MarketEtfViewModel: ObservableObject { + private let marketKit = App.shared.marketKit + private let currencyManager = App.shared.currencyManager + + private var cancellables = Set() + private var tasks = Set() + + private var internalState: State = .loading { + didSet { + syncState() + } + } + + @Published var state: State = .loading + + var sortBy: SortBy = .highestAssets { + didSet { + stat(page: .globalMetricsEtf, event: .switchSortType(sortType: sortBy.statSortBy)) + syncState() + } + } + + var timePeriod: TimePeriod = .period(timePeriod: .day1) { + didSet { + stat(page: .globalMetricsEtf, event: .switchPeriod(period: timePeriod.statPeriod)) + syncState() + } + } + + init() { + currencyManager.$baseCurrency + .sink { [weak self] _ in + self?.sync() + } + .store(in: &cancellables) + + sync() + } + + private func syncState() { + switch internalState { + case .loading: + state = .loading + case let .loaded(rankedEtfs): + state = .loaded(rankedEtfs: rankedEtfs.sorted(sortBy: sortBy, timePeriod: timePeriod)) + case let .failed(error): + state = .failed(error: error) + } + } +} + +extension MarketEtfViewModel { + var currency: Currency { + currencyManager.baseCurrency + } + + var timePeriods: [TimePeriod] { + [.period(timePeriod: .day1), .period(timePeriod: .week1), .period(timePeriod: .month1), .period(timePeriod: .month3), .all] + } + + func sync() { + tasks = Set() + + if case .failed = internalState { + internalState = .loading + } + + Task { [weak self, marketKit, currency] in + do { + let etfs = try await marketKit.etfs(currencyCode: currency.code) + let sortedEtfs = etfs.sorted { $0.totalAssets ?? 0 > $1.totalAssets ?? 0 } + let rankedEtfs = sortedEtfs.enumerated().map { RankedEtf(etf: $1, rank: $0 + 1) } + + await MainActor.run { [weak self] in + self?.internalState = .loaded(rankedEtfs: rankedEtfs) + } + } catch { + await MainActor.run { [weak self] in + self?.internalState = .failed(error: error) + } + } + } + .store(in: &tasks) + } +} + +extension MarketEtfViewModel { + enum State { + case loading + case loaded(rankedEtfs: [RankedEtf]) + case failed(error: Error) + } + + enum SortBy: String, CaseIterable { + case highestAssets = "highest_assets" + case lowestAssets = "lowest_assets" + case inflow + case outflow + + var title: String { + "market.etf.sort_by.\(rawValue)".localized + } + } + + enum TimePeriod: Equatable { + case period(timePeriod: HsTimePeriod) + case all + + var title: String { + switch self { + case let .period(timePeriod): return timePeriod.title + case .all: return "market.etf.period.all".localized + } + } + + var shortTitle: String { + switch self { + case let .period(timePeriod): return timePeriod.shortTitle + case .all: return "market.etf.period.all".localized + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Fetchers/MarketEtfFetcher.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Fetchers/MarketEtfFetcher.swift new file mode 100644 index 0000000000..be4eef18f2 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Fetchers/MarketEtfFetcher.swift @@ -0,0 +1,49 @@ +import Chart +import Combine +import Foundation +import MarketKit + +class MarketEtfFetcher { + private let marketKit: MarketKit.Kit + private let currencyManager: CurrencyManager + + private let needUpdateSubject = PassthroughSubject() + + init(marketKit: MarketKit.Kit, currencyManager: CurrencyManager) { + self.marketKit = marketKit + self.currencyManager = currencyManager + } +} + +extension MarketEtfFetcher: IMetricChartFetcher { + var valueType: MetricChartModule.ValueType { + .compactCurrencyValue(currencyManager.baseCurrency) + } + + var needUpdatePublisher: AnyPublisher { + Empty().eraseToAnyPublisher() + } + + var intervals: [HsPeriodType] { [] } + + func fetch(interval _: HsPeriodType) async throws -> MetricChartModule.ItemData { + let points = try await marketKit.etfPoints(currencyCode: currencyManager.baseCurrency.code) + + var items = [MetricChartModule.Item]() + var totalInflow = [Decimal]() + var totalAssets = [Decimal]() + for point in points.sorted(by: { p1, p2 in p1.date < p2.date }) { + items.append(MetricChartModule.Item(value: point.dailyInflow, timestamp: point.date.timeIntervalSince1970)) + totalInflow.append(point.totalInflow) + totalAssets.append(point.totalAssets) + } + return MetricChartModule.ItemData( + items: items, + indicators: [ + MarketGlobalModule.totalInflow: totalInflow, + MarketGlobalModule.totalAssets: totalAssets, + ], + type: .etf + ) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobal/MarketGlobalFetcher.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Fetchers/MarketGlobalFetcher.swift similarity index 100% rename from UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobal/MarketGlobalFetcher.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/Market/Fetchers/MarketGlobalFetcher.swift diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Fetchers/MarketGlobalModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Fetchers/MarketGlobalModule.swift new file mode 100644 index 0000000000..e2c4d2a875 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Fetchers/MarketGlobalModule.swift @@ -0,0 +1,13 @@ +enum MarketGlobalModule { + static let dominance = "dominance" + static let totalAssets = "total_assets" + static let totalInflow = "total_inflow" + + enum MetricsType: Identifiable { + case totalMarketCap, volume24h, defiCap, tvlInDefi + + var id: Self { + self + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/TopPlatform/TopPlatformMarketCapFetcher.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Fetchers/TopPlatformMarketCapFetcher.swift similarity index 100% rename from UnstoppableWallet/UnstoppableWallet/Modules/Market/TopPlatform/TopPlatformMarketCapFetcher.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/Market/Fetchers/TopPlatformMarketCapFetcher.swift diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/FilteredList/MarketFilteredListService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/FilteredList/MarketFilteredListService.swift deleted file mode 100644 index 7a65ad11ba..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/FilteredList/MarketFilteredListService.swift +++ /dev/null @@ -1,103 +0,0 @@ -import Combine -import HsExtensions -import MarketKit -import RxRelay -import RxSwift - -protocol IMarketFilteredListProvider { - func marketInfos(currencyCode: String) async throws -> [MarketInfo] -} - -class MarketFilteredListService: IMarketMultiSortHeaderService { - private let currencyManager: CurrencyManager - private let provider: IMarketFilteredListProvider - private let statPage: StatPage - private var tasks = Set() - - @PostPublished private(set) var state: MarketListServiceState = .loading - - var sortingField: MarketModule.SortingField = .highestCap { - didSet { - syncIfPossible() - - stat(page: statPage, event: .switchSortType(sortType: sortingField.statSortType)) - } - } - - init(currencyManager: CurrencyManager, provider: IMarketFilteredListProvider, statPage: StatPage) { - self.currencyManager = currencyManager - self.provider = provider - self.statPage = statPage - - syncMarketInfos() - } - - private func syncMarketInfos() { - tasks = Set() - - if case .failed = state { - state = .loading - } - - Task { [weak self, provider, currency] in - do { - let marketInfos = try await provider.marketInfos(currencyCode: currency.code) - self?.sync(marketInfos: marketInfos) - } catch { - self?.state = .failed(error: error) - } - }.store(in: &tasks) - } - - private func sync(marketInfos: [MarketInfo], reorder: Bool = false) { - state = .loaded(items: marketInfos.sorted(sortingField: sortingField, priceChangeType: priceChangeType), softUpdate: false, reorder: reorder) - } - - private func syncIfPossible() { - guard case let .loaded(marketInfos, _, _) = state else { - return - } - - sync(marketInfos: marketInfos, reorder: true) - } -} - -extension MarketFilteredListService: IMarketListService { - var statePublisher: AnyPublisher, Never> { - $state - } - - func refresh() { - syncMarketInfos() - } -} - -extension MarketFilteredListService: IMarketListCoinUidService { - func coinUid(index: Int) -> String? { - guard case let .loaded(marketInfos, _, _) = state, index < marketInfos.count else { - return nil - } - - return marketInfos[index].fullCoin.coin.uid - } -} - -extension MarketFilteredListService: IMarketListDecoratorService { - var initialIndex: Int { - 0 - } - - var currency: Currency { - currencyManager.baseCurrency - } - - var priceChangeType: MarketModule.PriceChangeType { - .day - } - - func onUpdate(index _: Int) { - if case let .loaded(marketInfos, _, _) = state { - state = .loaded(items: marketInfos, softUpdate: false, reorder: false) - } - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Global/MarketGlobalView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Global/MarketGlobalView.swift new file mode 100644 index 0000000000..dc9b1cf27c --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Global/MarketGlobalView.swift @@ -0,0 +1,116 @@ +import MarketKit +import SwiftUI +import ThemeKit + +struct MarketGlobalView: View { + @ObservedObject var viewModel: MarketGlobalViewModel + + @State private var marketCapPresented = false + @State private var volumePresented = false + @State private var etfPresented = false + @State private var tvlPresented = false + + var body: some View { + VStack(spacing: 0) { + if let marketGlobal = viewModel.marketGlobal { + MarqueeView(targetVelocity: 30) { + content(marketGlobal: marketGlobal, redacted: marketGlobal) + } + } else { + ZStack { + HStack(spacing: .margin8) { + content(marketGlobal: nil, redacted: nil) + } + .padding(.leading, .margin8) + .fixedSize() + } + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + } + + Rectangle() + .fill(Color.themeSteel10) + .frame(maxWidth: .infinity) + .frame(height: 1) + } + .animation(.default, value: viewModel.marketGlobal == nil) + .sheet(isPresented: $tvlPresented) { + ThemeNavigationView { MarketTvlView() } + .onFirstAppear { stat(page: .markets, event: .open(page: .globalMetricsTvlInDefi)) } + } + .sheet(isPresented: $marketCapPresented) { + MarketMarketCapView(isPresented: $marketCapPresented) + .onFirstAppear { stat(page: .markets, event: .open(page: .globalMetricsMarketCap)) } + } + .sheet(isPresented: $volumePresented) { + MarketVolumeView(isPresented: $volumePresented) + .onFirstAppear { stat(page: .markets, event: .open(page: .globalMetricsVolume)) } + } + .sheet(isPresented: $etfPresented) { + MarketEtfView(isPresented: $etfPresented) + .onFirstAppear { stat(page: .markets, event: .open(page: .globalMetricsEtf)) } + } + } + + @ViewBuilder private func content(marketGlobal: MarketGlobal?, redacted: Any?) -> some View { + diffView( + title: "market.global.market_cap".localized, + amount: marketGlobal?.marketCap.flatMap { ValueFormatter.instance.formatShort(currency: viewModel.currency, value: $0) }, + diff: marketGlobal?.marketCapChange.map { .percent(value: $0) }, + redacted: redacted + ) { + marketCapPresented = true + } + + diffView( + title: "market.global.volume".localized, + amount: marketGlobal?.volume.flatMap { ValueFormatter.instance.formatShort(currency: viewModel.currency, value: $0) }, + diff: marketGlobal?.volumeChange.map { .percent(value: $0) }, + redacted: redacted + ) { + volumePresented = true + } + + diffView( + title: "market.global.btc_dominance".localized, + amount: marketGlobal?.btcDominance.flatMap { ValueFormatter.instance.format(percentValue: $0, signType: .never) }, + diff: marketGlobal?.btcDominanceChange.map { .percent(value: $0) }, + redacted: redacted + ) { + marketCapPresented = true + } + + diffView( + title: "market.global.etf_inflow".localized, + amount: marketGlobal?.etfTotalInflow.flatMap { ValueFormatter.instance.formatShort(currency: viewModel.currency, value: $0) }, + diff: marketGlobal?.etfDailyInflow.map { .change(value: $0, currency: viewModel.currency) }, + redacted: redacted + ) { + etfPresented = true + } + + diffView( + title: "market.global.tvl_in_defi".localized, + amount: marketGlobal?.tvl.flatMap { ValueFormatter.instance.formatShort(currency: viewModel.currency, value: $0) }, + diff: marketGlobal?.tvlChange.map { .percent(value: $0) }, + redacted: redacted + ) { + tvlPresented = true + } + } + + @ViewBuilder private func diffView(title: String, amount: String?, diff: DiffText.Diff?, redacted: Any?, onTap: @escaping () -> Void) -> some View { + HStack(spacing: .margin4) { + Text(title).textCaption() + + Text(amount ?? "----") + .textCaption(color: .themeBran) + .redacted(value: redacted) + + DiffText(diff, font: .themeCaption) + .redacted(value: redacted) + } + .padding(.horizontal, .margin8) + .padding(.vertical, .margin16) + .onTapGesture(perform: onTap) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Global/MarketGlobalViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Global/MarketGlobalViewModel.swift new file mode 100644 index 0000000000..936f241170 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Global/MarketGlobalViewModel.swift @@ -0,0 +1,50 @@ +import Combine +import Foundation +import HsExtensions +import MarketKit + +class MarketGlobalViewModel: ObservableObject { + private let marketKit = App.shared.marketKit + private let currencyManager = App.shared.currencyManager + private let appManager = App.shared.appManager + + private var cancellables = Set() + private var tasks = Set() + + @Published var marketGlobal: MarketGlobal? + + init() { + currencyManager.$baseCurrency + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.marketGlobal = nil + self?.syncState() + } + .store(in: &cancellables) + + appManager.willEnterForegroundPublisher + .sink { [weak self] in self?.syncState() } + .store(in: &cancellables) + + syncState() + } + + private func syncState() { + tasks = Set() + + Task { [weak self, marketKit, currencyManager] in + let marketGlobal = try await marketKit.marketGlobal(currencyCode: currencyManager.baseCurrency.code) + + await MainActor.run { [weak self] in + self?.marketGlobal = marketGlobal + } + } + .store(in: &tasks) + } +} + +extension MarketGlobalViewModel { + var currency: Currency { + currencyManager.baseCurrency + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketAdvancedSearch/MarketAdvancedSearchModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketAdvancedSearch/MarketAdvancedSearchModule.swift deleted file mode 100644 index e4bc0c8eb8..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketAdvancedSearch/MarketAdvancedSearchModule.swift +++ /dev/null @@ -1,10 +0,0 @@ -import UIKit - -enum MarketAdvancedSearchModule { - static func viewController() -> UIViewController { - let service = MarketAdvancedSearchService(marketKit: App.shared.marketKit, currencyManager: App.shared.currencyManager) - let viewModel = MarketAdvancedSearchViewModel(service: service) - - return MarketAdvancedSearchViewController(viewModel: viewModel) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketAdvancedSearch/MarketAdvancedSearchViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketAdvancedSearch/MarketAdvancedSearchViewController.swift deleted file mode 100644 index 314dcf9294..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketAdvancedSearch/MarketAdvancedSearchViewController.swift +++ /dev/null @@ -1,557 +0,0 @@ -import ComponentKit -import HUD -import RxSwift -import SectionsTableView -import SnapKit -import ThemeKit -import UIKit - -class MarketAdvancedSearchViewController: ThemeViewController { - private let viewModel: MarketAdvancedSearchViewModel - private let disposeBag = DisposeBag() - - private let tableView = SectionsTableView(style: .grouped) - - private let coinListCell = BaseSelectableThemeCell() - private let marketCapCell = BaseSelectableThemeCell() - private let volumeCell = BaseSelectableThemeCell() - - private let listedOnTopExchangesCell = BaseThemeCell() - private let goodCexVolumeCell = BaseThemeCell() - private let goodDexVolumeCell = BaseThemeCell() - private let goodDistributionCell = BaseThemeCell() - - private let blockchainsCell = BaseSelectableThemeCell() - private let technicalAdviceCell = BaseSelectableThemeCell() - private let periodCell = BaseSelectableThemeCell() - private let priceChangeCell = BaseSelectableThemeCell() - - private let outperformedBtcCell = BaseThemeCell() - private let outperformedEthCell = BaseThemeCell() - private let outperformedBnbCell = BaseThemeCell() - private let priceCloseToAthCell = BaseThemeCell() - private let priceCloseToAtlCell = BaseThemeCell() - - private let showResultButtonHolder = BottomGradientHolder() - private let showResultButton = PrimaryButton() - - private let spinner = HUDActivityView.create(with: .small20) - - init(viewModel: MarketAdvancedSearchViewModel) { - self.viewModel = viewModel - - super.init() - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - title = "market.advanced_search.title".localized - - view.addSubview(tableView) - tableView.snp.makeConstraints { maker in - maker.top.equalToSuperview() - maker.leading.trailing.equalToSuperview() - } - - tableView.sectionDataSource = self - - tableView.backgroundColor = .clear - tableView.separatorStyle = .none - - navigationItem.largeTitleDisplayMode = .never - navigationItem.leftBarButtonItem = UIBarButtonItem(title: "market.advanced_search.reset_all".localized, style: .plain, target: self, action: #selector(onTapReset)) - navigationItem.rightBarButtonItem = UIBarButtonItem(title: "button.close".localized, style: .plain, target: self, action: #selector(onTapClose)) - - coinListCell.set(backgroundStyle: .lawrence, isFirst: true, isLast: true) - marketCapCell.set(backgroundStyle: .lawrence, isFirst: true, isLast: false) - volumeCell.set(backgroundStyle: .lawrence, isFirst: false, isLast: false) - - listedOnTopExchangesCell.set(backgroundStyle: .lawrence, isFirst: false, isLast: false) - goodCexVolumeCell.set(backgroundStyle: .lawrence, isFirst: false, isLast: false) - goodDexVolumeCell.set(backgroundStyle: .lawrence, isFirst: false, isLast: false) - goodDistributionCell.set(backgroundStyle: .lawrence, isFirst: false, isLast: true) - - blockchainsCell.set(backgroundStyle: .lawrence, isFirst: true, isLast: true) - technicalAdviceCell.set(backgroundStyle: .lawrence, isFirst: true, isLast: true) - priceChangeCell.set(backgroundStyle: .lawrence, isFirst: true, isLast: false) - periodCell.set(backgroundStyle: .lawrence, isFirst: false, isLast: false) - - outperformedBtcCell.set(backgroundStyle: .lawrence, isFirst: false, isLast: false) - outperformedEthCell.set(backgroundStyle: .lawrence, isFirst: false, isLast: false) - outperformedBnbCell.set(backgroundStyle: .lawrence, isFirst: false, isLast: false) - priceCloseToAthCell.set(backgroundStyle: .lawrence, isFirst: false, isLast: false) - priceCloseToAtlCell.set(backgroundStyle: .lawrence, isFirst: false, isLast: true) - - showResultButtonHolder.add(to: self, under: tableView) - showResultButtonHolder.addSubview(showResultButton) - - showResultButton.set(style: .yellow) - showResultButton.addTarget(self, action: #selector(onTapShowResult), for: .touchUpInside) - - view.addSubview(spinner) - spinner.snp.makeConstraints { maker in - maker.center.equalTo(showResultButton) - } - - tableView.buildSections() - - subscribe(disposeBag, viewModel.coinListViewItemDriver) { [weak self] in - self?.syncCoinList(viewItem: $0) - } - subscribe(disposeBag, viewModel.marketCapViewItemDriver) { [weak self] in - self?.syncMarketCap(viewItem: $0) - } - subscribe(disposeBag, viewModel.volumeViewItemDriver) { [weak self] in - self?.syncVolume(viewItem: $0) - } - subscribe(disposeBag, viewModel.listedOnTopExchangesDriver) { [weak self] in - self?.syncListedOnTopExchanges(isOn: $0) - } - subscribe(disposeBag, viewModel.goodCexVolumeDriver) { [weak self] in - self?.syncGoodCexVolume(isOn: $0) - } - subscribe(disposeBag, viewModel.goodDexVolumeDriver) { [weak self] in - self?.syncGoodDexVolume(isOn: $0) - } - subscribe(disposeBag, viewModel.goodDistributionDriver) { [weak self] in - self?.syncGoodDistribution(isOn: $0) - } - subscribe(disposeBag, viewModel.blockchainsViewItemDriver) { [weak self] in - self?.syncBlockchains(viewItem: $0) - } - subscribe(disposeBag, viewModel.technicalAdviceViewItemDriver) { [weak self] in - self?.syncTechnicalAdvice(viewItem: $0) - } - subscribe(disposeBag, viewModel.priceChangeTypeViewItemDriver) { [weak self] in - self?.syncPeriod(viewItem: $0) - } - subscribe(disposeBag, viewModel.priceChangeViewItemDriver) { [weak self] in - self?.syncPriceChange(viewItem: $0) - } - subscribe(disposeBag, viewModel.outperformedBtcDriver) { [weak self] in - self?.syncOutperformedBtc(isOn: $0) - } - subscribe(disposeBag, viewModel.outperformedEthDriver) { [weak self] in - self?.syncOutperformedEth(isOn: $0) - } - subscribe(disposeBag, viewModel.outperformedBnbDriver) { [weak self] in - self?.syncOutperformedBnb(isOn: $0) - } - subscribe(disposeBag, viewModel.priceCloseToAthDriver) { [weak self] in - self?.syncPriceCloseToAth(isOn: $0) - } - subscribe(disposeBag, viewModel.priceCloseToAtlDriver) { [weak self] in - self?.syncPriceCloseToAtl(isOn: $0) - } - subscribe(disposeBag, viewModel.resetEnabledDriver) { [weak self] enabled in - self?.navigationItem.leftBarButtonItem?.isEnabled = enabled - } - - subscribe(disposeBag, viewModel.buttonStateDriver) { [weak self] in - self?.sync(buttonState: $0) - } - } - - private func buildSelector(cell: BaseThemeCell, title: String? = nil, viewItem: MarketAdvancedSearchViewModel.ViewItem? = nil) { - let elements = tableView.universalImage24Elements( - title: .body(title), - value: viewItem.map { .subhead1($0.value, color: $0.valueStyle.valueTextColor) }, - accessoryType: .dropdown - ) - CellBuilderNew.buildStatic(cell: cell, rootElement: .hStack(elements)) - } - - private func buildToggle(cell: BaseThemeCell, title: String, isOn: Bool, onToggle: @escaping (Bool) -> Void) { - let elements = tableView.universalImage24Elements( - title: .body(title), - accessoryType: .switch(isOn: isOn, onSwitch: onToggle) - ) - CellBuilderNew.buildStatic(cell: cell, rootElement: .hStack(elements)) - } - - private func buildToggleWithDescription(cell: BaseThemeCell, title: String, description: String, isOn: Bool, onToggle: @escaping (Bool) -> Void) { - let elements = tableView.universalImage32Elements( - title: .body(title), - description: .subhead2(description), - accessoryType: .switch(isOn: isOn, onSwitch: onToggle) - ) - CellBuilderNew.buildStatic(cell: cell, rootElement: .hStack(elements)) - } - - private func selectorItems(viewItems: [MarketAdvancedSearchViewModel.FilterViewItem]) -> [SelectorModule.ViewItem] { - viewItems.map { - SelectorModule.ViewItem( - title: $0.title, - titleColor: $0.style.filterTextColor, - selected: $0.selected - ) - } - } - - private func showSelector(image: BottomSheetTitleView.Image, title: String, viewItems: [SelectorModule.ViewItem], onSelect: @escaping (Int) -> Void) { - let viewController = SelectorModule.bottomSingleSelectorViewController( - image: image, - title: title, - viewItems: viewItems, - onSelect: onSelect - ) - - DispatchQueue.main.async { - self.present(viewController, animated: true) - } - } - - private func onTapCoinListCell() { - showSelector( - image: .local(name: "circle_coin_24", tint: .warning), - title: "market.advanced_search.choose_set".localized, - viewItems: selectorItems(viewItems: viewModel.coinListViewItems) - ) { [weak self] index in - self?.viewModel.setCoinList(at: index) - } - } - - private func onTapMarketCapCell() { - showSelector( - image: .local(name: "usd_24", tint: .warning), - title: "market.advanced_search.market_cap".localized, - viewItems: selectorItems(viewItems: viewModel.marketCapViewItems) - ) { [weak self] index in - self?.viewModel.setMarketCap(at: index) - } - } - - private func onTapVolumeCell() { - showSelector( - image: .local(name: "chart_2_24", tint: .warning), - title: "market.advanced_search.volume".localized, - viewItems: selectorItems(viewItems: viewModel.volumeViewItems) - ) { [weak self] index in - self?.viewModel.setVolume(at: index) - } - } - - private func onTapBlockchainsCell() { - let viewController = SelectorModule.multiSelectorViewController( - title: "market.advanced_search.blockchains".localized, - viewItems: viewModel.blockchainViewItems, - onFinish: { [weak self] in - self?.viewModel.setBlockchains(indexes: $0) - } - ) - - present(viewController, animated: true) - } - - private func onTapTechnicalIndicatorCell() { - showSelector( - image: .local(name: "bell_ring_24", tint: .warning), - title: "market.advanced_search.technical_advice".localized, - viewItems: selectorItems(viewItems: viewModel.technicalIndicatorViewItems) - ) { [weak self] index in - self?.viewModel.setTechnicalAdvice(index: index) - } - } - - private func onTapPeriodCell() { - showSelector( - image: .local(name: "circle_clock_24", tint: .warning), - title: "market.advanced_search.price_period".localized, - viewItems: selectorItems(viewItems: viewModel.priceChangeTypeViewItems) - ) { [weak self] index in - self?.viewModel.setPriceChangeType(at: index) - } - } - - private func onTapPriceChangeCell() { - showSelector( - image: .local(name: "markets_24", tint: .warning), - title: "market.advanced_search.price_change".localized, - viewItems: selectorItems(viewItems: viewModel.priceChangeViewItems) - ) { [weak self] index in - self?.viewModel.setPriceChange(at: index) - } - } - - private func onTapOutperformedBtcCell(isOn: Bool) { - viewModel.setOutperformedBtc(isOn: isOn) - } - - private func onTapOutperformedEthCell(isOn: Bool) { - viewModel.setOutperformedEth(isOn: isOn) - } - - private func onTapOutperformedBnbCell(isOn: Bool) { - viewModel.setOutperformedBnb(isOn: isOn) - } - - private func onTapPriceCloseToAthCell(isOn: Bool) { - viewModel.setPriceCloseToATH(isOn: isOn) - } - - private func onTapPriceCloseToAtlCell(isOn: Bool) { - viewModel.setPriceCloseToATL(isOn: isOn) - } - - @objc private func onTapReset() { - viewModel.reset() - } - - @objc private func onTapClose() { - dismiss(animated: true) - } - - @objc private func onTapShowResult() { - let viewController = MarketAdvancedSearchResultModule.viewController(marketInfos: viewModel.marketInfos, priceChangeType: viewModel.priceChangeType) - navigationController?.pushViewController(viewController, animated: true) - } - - private func set(isOn _: Bool, cell _: BaseThemeCell) { -// cell.bind(index: 1) { (component: SwitchComponent) in -// component.switchView.isOn = isOn -// } - } - - private func syncCoinList(viewItem: MarketAdvancedSearchViewModel.ViewItem) { - buildSelector(cell: coinListCell, title: "market.advanced_search.choose_set".localized, viewItem: viewItem) - } - - private func syncMarketCap(viewItem: MarketAdvancedSearchViewModel.ViewItem) { - buildSelector(cell: marketCapCell, title: "market.advanced_search.market_cap".localized, viewItem: viewItem) - } - - private func syncVolume(viewItem: MarketAdvancedSearchViewModel.ViewItem) { - buildSelector(cell: volumeCell, title: "market.advanced_search.volume".localized, viewItem: viewItem) - } - - private func syncListedOnTopExchanges(isOn: Bool) { - buildToggle(cell: listedOnTopExchangesCell, title: "market.advanced_search.listed_on_top_exchanges".localized, isOn: isOn) { [weak self] in - self?.viewModel.setListedOnTopExchanges(isOn: $0) - } - } - - private func syncGoodCexVolume(isOn: Bool) { - buildToggleWithDescription( - cell: goodCexVolumeCell, - title: "market.advanced_search.good_cex_volume".localized, - description: "market.advanced_search.overall_score_is_good_or_excellent".localized, - isOn: isOn - ) { [weak self] in - self?.viewModel.setGoodCexVolume(isOn: $0) - } - } - - private func syncGoodDexVolume(isOn: Bool) { - buildToggleWithDescription( - cell: goodDexVolumeCell, - title: "market.advanced_search.good_dex_volume".localized, - description: "market.advanced_search.overall_score_is_good_or_excellent".localized, - isOn: isOn - ) { [weak self] in - self?.viewModel.setGoodDexVolume(isOn: $0) - } - } - - private func syncGoodDistribution(isOn: Bool) { - buildToggleWithDescription( - cell: goodDistributionCell, - title: "market.advanced_search.good_distribution".localized, - description: "market.advanced_search.overall_score_is_good_or_excellent".localized, - isOn: isOn - ) { [weak self] in - self?.viewModel.setGoodDistribution(isOn: $0) - } - } - - private func syncBlockchains(viewItem: MarketAdvancedSearchViewModel.ViewItem) { - buildSelector(cell: blockchainsCell, title: "market.advanced_search.blockchains".localized, viewItem: viewItem) - } - - private func syncTechnicalAdvice(viewItem: MarketAdvancedSearchViewModel.ViewItem) { - buildSelector(cell: technicalAdviceCell, title: "market.advanced_search.technical_advice".localized, viewItem: viewItem) - } - - private func syncPeriod(viewItem: MarketAdvancedSearchViewModel.ViewItem) { - buildSelector(cell: periodCell, title: "market.advanced_search.price_period".localized, viewItem: viewItem) - } - - private func syncPriceChange(viewItem: MarketAdvancedSearchViewModel.ViewItem) { - buildSelector(cell: priceChangeCell, title: "market.advanced_search.price_change".localized, viewItem: viewItem) - } - - private func syncOutperformedBtc(isOn: Bool) { - buildToggle(cell: outperformedBtcCell, title: "market.advanced_search.outperformed_btc".localized, isOn: isOn) { [weak self] in - self?.onTapOutperformedBtcCell(isOn: $0) - } - } - - private func syncOutperformedEth(isOn: Bool) { - buildToggle(cell: outperformedEthCell, title: "market.advanced_search.outperformed_eth".localized, isOn: isOn) { [weak self] in - self?.onTapOutperformedEthCell(isOn: $0) - } - } - - private func syncOutperformedBnb(isOn: Bool) { - buildToggle(cell: outperformedBnbCell, title: "market.advanced_search.outperformed_bnb".localized, isOn: isOn) { [weak self] in - self?.onTapOutperformedBnbCell(isOn: $0) - } - } - - private func syncPriceCloseToAth(isOn: Bool) { - buildToggle(cell: priceCloseToAthCell, title: "market.advanced_search.price_close_to_ath".localized, isOn: isOn) { [weak self] in - self?.onTapPriceCloseToAthCell(isOn: $0) - } - } - - private func syncPriceCloseToAtl(isOn: Bool) { - buildToggle(cell: priceCloseToAtlCell, title: "market.advanced_search.price_close_to_atl".localized, isOn: isOn) { [weak self] in - self?.onTapPriceCloseToAtlCell(isOn: $0) - } - } - - private func sync(buttonState: MarketAdvancedSearchViewModel.ButtonState) { - switch buttonState { - case .loading: - spinner.isHidden = false - spinner.startAnimating() - showResultButton.setTitle("", for: .normal) - showResultButton.isEnabled = false - case .emptyResults: - spinner.isHidden = true - showResultButton.setTitle("market.advanced_search.empty_results".localized, for: .normal) - showResultButton.isEnabled = false - case let .showResults(count): - spinner.isHidden = true - showResultButton.setTitle("\("market.advanced_search.show_results".localized): \(count)", for: .normal) - showResultButton.isEnabled = true - case let .error(description): - spinner.isHidden = true - showResultButton.setTitle(description, for: .normal) - showResultButton.isEnabled = false - } - } - - private func row(cell: UITableViewCell, id: String, height: CGFloat = .heightCell48, action: (() -> Void)? = nil) -> RowProtocol { - StaticRow( - cell: cell, - id: id, - height: height, - autoDeselect: true, - action: action - ) - } -} - -extension MarketAdvancedSearchViewController: SectionsDataSource { - func buildSections() -> [SectionProtocol] { - var sections = [SectionProtocol]() - - sections.append( - Section( - id: "coin_list", - headerState: .margin(height: .margin12), - footerState: .margin(height: .margin24), - rows: [ - row(cell: coinListCell, id: "coin_list") { [weak self] in - self?.onTapCoinListCell() - }, - ] - ) - ) - - sections.append( - Section( - id: "market_filters", - headerState: tableView.sectionHeader(text: "market.advanced_search.market_parameters".localized.uppercased()), - footerState: .margin(height: .margin24), - rows: [ - row(cell: marketCapCell, id: "market_cap") { [weak self] in - self?.onTapMarketCapCell() - }, - row(cell: volumeCell, id: "volume") { [weak self] in - self?.onTapVolumeCell() - }, - row(cell: listedOnTopExchangesCell, id: "listed_on_top_exchanges", height: .heightCell56), - row(cell: goodCexVolumeCell, id: "good_cex_volume", height: .heightDoubleLineCell), - row(cell: goodDexVolumeCell, id: "good_dex_volume", height: .heightDoubleLineCell), - row(cell: goodDistributionCell, id: "good_distribution", height: .heightDoubleLineCell), - ] - ) - ) - - sections.append( - Section( - id: "price_filters", - headerState: tableView.sectionHeader(text: "market.advanced_search.price_parameters".localized.uppercased()), - footerState: .margin(height: .margin24), - rows: [ - row(cell: priceChangeCell, id: "price_change") { [weak self] in - self?.onTapPriceChangeCell() - }, - row(cell: periodCell, id: "price_period") { [weak self] in - self?.onTapPeriodCell() - }, - row(cell: outperformedBtcCell, id: "outperformed_btc", height: .heightCell56), - row(cell: outperformedEthCell, id: "outperformed_eth", height: .heightCell56), - row(cell: outperformedBnbCell, id: "outperformed_bnb", height: .heightCell56), - row(cell: priceCloseToAthCell, id: "price_close_to_ath", height: .heightCell56), - row(cell: priceCloseToAtlCell, id: "price_close_to_atl", height: .heightCell56), - ] - ) - ) - - sections.append( - Section( - id: "network_filters", - headerState: tableView.sectionHeader(text: "market.advanced_search.network_parameters".localized.uppercased()), - footerState: .margin(height: .margin24), - rows: [ - row(cell: blockchainsCell, id: "blockchains") { [weak self] in - self?.onTapBlockchainsCell() - }, - ] - ) - ) - - sections.append( - Section( - id: "indicators", - headerState: tableView.sectionHeader(text: "market.advanced_search.indicators".localized.uppercased()), - footerState: .margin(height: .margin32), - rows: [ - row(cell: technicalAdviceCell, id: "indicators") { [weak self] in - self?.onTapTechnicalIndicatorCell() - }, - ] - ) - ) - - return sections - } -} - -extension MarketAdvancedSearchViewModel.ValueStyle { - var valueTextColor: UIColor { - switch self { - case .none: return .themeGray - case .positive: return .themeRemus - case .negative: return .themeLucian - case .normal: return .themeLeah - } - } - - var filterTextColor: UIColor { - switch self { - case .none: return .themeGray - case .positive: return .themeRemus - case .negative: return .themeLucian - case .normal: return .themeLeah - } - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketAdvancedSearch/MarketAdvancedSearchViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketAdvancedSearch/MarketAdvancedSearchViewModel.swift deleted file mode 100644 index 9fa74eb7a0..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketAdvancedSearch/MarketAdvancedSearchViewModel.swift +++ /dev/null @@ -1,496 +0,0 @@ -import Foundation -import MarketKit -import RxCocoa -import RxSwift - -class MarketAdvancedSearchViewModel { - private let disposeBag = DisposeBag() - - private let service: MarketAdvancedSearchService - - private let buttonStateRelay = BehaviorRelay(value: .loading) - - private let coinListViewItemRelay = BehaviorRelay(value: ViewItem(value: "", valueStyle: .none)) - private let marketCapViewItemRelay = BehaviorRelay(value: ViewItem(value: "", valueStyle: .none)) - private let volumeViewItemRelay = BehaviorRelay(value: ViewItem(value: "", valueStyle: .none)) - private let listedOnTopExchangesRelay = BehaviorRelay(value: false) - private let goodCexVolumeRelay = BehaviorRelay(value: false) - private let goodDexVolumeRelay = BehaviorRelay(value: false) - private let goodDistributionRelay = BehaviorRelay(value: false) - private let blockchainsViewItemRelay = BehaviorRelay(value: ViewItem(value: "", valueStyle: .none)) - private let technicalAdviceViewItemRelay = BehaviorRelay(value: ViewItem(value: "", valueStyle: .none)) - private let priceChangeTypeViewItemRelay = BehaviorRelay(value: ViewItem(value: "", valueStyle: .none)) - private let priceChangeViewItemRelay = BehaviorRelay(value: ViewItem(value: "", valueStyle: .none)) - private let outperformedBtcRelay = BehaviorRelay(value: false) - private let outperformedEthRelay = BehaviorRelay(value: false) - private let outperformedBnbRelay = BehaviorRelay(value: false) - private let priceCloseToAthRelay = BehaviorRelay(value: false) - private let priceCloseToAtlRelay = BehaviorRelay(value: false) - private let resetEnabledRelay = BehaviorRelay(value: false) - - private var valueFilters: [MarketAdvancedSearchService.ValueFilter] { - MarketAdvancedSearchService.ValueFilter.valuesByCurrencyCode[service.currencyCode] ?? [] - } - - init(service: MarketAdvancedSearchService) { - self.service = service - - subscribe(disposeBag, service.stateObservable) { [weak self] in self?.sync(state: $0) } - - subscribe(disposeBag, service.coinListObservable) { [weak self] in self?.sync(coinList: $0) } - subscribe(disposeBag, service.marketCapObservable) { [weak self] in self?.sync(marketCap: $0) } - subscribe(disposeBag, service.volumeObservable) { [weak self] in self?.sync(volume: $0) } - subscribe(disposeBag, service.listedOnTopExchangesObservable) { [weak self] in self?.listedOnTopExchangesRelay.accept($0) } - subscribe(disposeBag, service.goodCexVolumeObservable) { [weak self] in self?.goodCexVolumeRelay.accept($0) } - subscribe(disposeBag, service.goodDexVolumeObservable) { [weak self] in self?.goodDexVolumeRelay.accept($0) } - subscribe(disposeBag, service.goodDistributionObservable) { [weak self] in self?.goodDistributionRelay.accept($0) } - subscribe(disposeBag, service.blockchainsObservable) { [weak self] in self?.sync(blockchains: $0) } - subscribe(disposeBag, service.technicalAdviceObservable) { [weak self] in self?.sync(technicalAdvice: $0) } - subscribe(disposeBag, service.priceChangeTypeObservable) { [weak self] in self?.sync(priceChangeType: $0) } - subscribe(disposeBag, service.priceChangeObservable) { [weak self] in self?.sync(priceChange: $0) } - subscribe(disposeBag, service.outperformedBtcObservable) { [weak self] in self?.sync(outperformedBtc: $0) } - subscribe(disposeBag, service.outperformedEthObservable) { [weak self] in self?.sync(outperformedEth: $0) } - subscribe(disposeBag, service.outperformedBnbObservable) { [weak self] in self?.sync(outperformedBnb: $0) } - subscribe(disposeBag, service.priceCloseToAthObservable) { [weak self] in self?.sync(priceCloseToAth: $0) } - subscribe(disposeBag, service.priceCloseToAtlObservable) { [weak self] in self?.sync(priceCloseToAtl: $0) } - subscribe(disposeBag, service.canResetObservable) { [weak self] in self?.sync(canReset: $0) } - - sync(coinList: service.coinListCount) - sync(marketCap: service.marketCap) - sync(volume: service.volume) - sync(blockchains: service.blockchains) - sync(technicalAdvice: service.technicalAdvice) - sync(priceChangeType: service.priceChangeType) - sync(priceChange: service.priceChange) - sync(canReset: service.canReset) - - sync(state: service.state) - } - - private func sync(state: MarketAdvancedSearchService.State) { - switch state { - case .loading: - buttonStateRelay.accept(.loading) - case let .loaded(marketInfos): - if marketInfos.isEmpty { - buttonStateRelay.accept(.emptyResults) - } else { - buttonStateRelay.accept(.showResults(count: marketInfos.count)) - } - case .failed: - buttonStateRelay.accept(.error("error")) - } - } - - private func sync(coinList _: MarketAdvancedSearchService.CoinListCount) { - coinListViewItemRelay.accept(ViewItem(value: service.coinListCount.title, valueStyle: .normal)) - } - - private func sync(marketCap _: MarketAdvancedSearchService.ValueFilter) { - marketCapViewItemRelay.accept(ViewItem(value: service.marketCap.title, valueStyle: service.marketCap.valueStyle)) - } - - private func sync(volume _: MarketAdvancedSearchService.ValueFilter) { - volumeViewItemRelay.accept(ViewItem(value: service.volume.title, valueStyle: service.volume.valueStyle)) - } - - private func sync(blockchains _: [Blockchain]) { - let value: String - let valueStyle: ValueStyle - - if service.blockchains.isEmpty { - value = "selector.any".localized - valueStyle = .none - } else if service.blockchains.count == 1 { - value = service.blockchains[0].name - valueStyle = .normal - } else { - value = "\(service.blockchains.count)" - valueStyle = .normal - } - - blockchainsViewItemRelay.accept(ViewItem(value: value, valueStyle: valueStyle)) - } - - private func sync(technicalAdvice: TechnicalAdvice.Advice?) { - guard let technicalAdvice else { - technicalAdviceViewItemRelay.accept(ViewItem(value: "selector.any".localized, valueStyle: .none)) - return - } - - technicalAdviceViewItemRelay.accept(ViewItem(value: technicalAdvice.searchTitle.localized, valueStyle: .normal)) - } - - private func sync(priceChangeType _: MarketModule.PriceChangeType) { - priceChangeTypeViewItemRelay.accept(ViewItem(value: service.priceChangeType.title, valueStyle: .normal)) - } - - private func sync(priceChange _: MarketAdvancedSearchService.PriceChangeFilter) { - priceChangeViewItemRelay.accept(ViewItem(value: service.priceChange.title, valueStyle: service.priceChange.valueStyle)) - } - - private func sync(outperformedBtc: Bool) { - outperformedBtcRelay.accept(outperformedBtc) - } - - private func sync(outperformedEth: Bool) { - outperformedEthRelay.accept(outperformedEth) - } - - private func sync(outperformedBnb: Bool) { - outperformedBnbRelay.accept(outperformedBnb) - } - - private func sync(priceCloseToAth: Bool) { - priceCloseToAthRelay.accept(priceCloseToAth) - } - - private func sync(priceCloseToAtl: Bool) { - priceCloseToAtlRelay.accept(priceCloseToAtl) - } - - private func sync(canReset: Bool) { - resetEnabledRelay.accept(canReset) - } -} - -extension MarketAdvancedSearchViewModel { - var buttonStateDriver: Driver { - buttonStateRelay.asDriver() - } - - var coinListViewItemDriver: Driver { - coinListViewItemRelay.asDriver() - } - - var marketCapViewItemDriver: Driver { - marketCapViewItemRelay.asDriver() - } - - var volumeViewItemDriver: Driver { - volumeViewItemRelay.asDriver() - } - - var listedOnTopExchangesDriver: Driver { - listedOnTopExchangesRelay.asDriver() - } - - var goodCexVolumeDriver: Driver { - goodCexVolumeRelay.asDriver() - } - - var goodDexVolumeDriver: Driver { - goodDexVolumeRelay.asDriver() - } - - var goodDistributionDriver: Driver { - goodDistributionRelay.asDriver() - } - - var blockchainsViewItemDriver: Driver { - blockchainsViewItemRelay.asDriver() - } - - var technicalAdviceViewItemDriver: Driver { - technicalAdviceViewItemRelay.asDriver() - } - - var priceChangeTypeViewItemDriver: Driver { - priceChangeTypeViewItemRelay.asDriver() - } - - var priceChangeViewItemDriver: Driver { - priceChangeViewItemRelay.asDriver() - } - - var outperformedBtcDriver: Driver { - outperformedBtcRelay.asDriver() - } - - var outperformedEthDriver: Driver { - outperformedEthRelay.asDriver() - } - - var outperformedBnbDriver: Driver { - outperformedBnbRelay.asDriver() - } - - var priceCloseToAthDriver: Driver { - priceCloseToAthRelay.asDriver() - } - - var priceCloseToAtlDriver: Driver { - priceCloseToAtlRelay.asDriver() - } - - var resetEnabledDriver: Driver { - resetEnabledRelay.asDriver() - } - - var coinListViewItems: [FilterViewItem] { - MarketAdvancedSearchService.CoinListCount.allCases.map { - FilterViewItem(title: $0.title, style: .normal, selected: service.coinListCount == $0) - } - } - - var marketCapViewItems: [FilterViewItem] { - valueFilters.map { - FilterViewItem(title: $0.title, style: $0.valueStyle, selected: service.marketCap == $0) - } - } - - var volumeViewItems: [FilterViewItem] { - valueFilters.map { - FilterViewItem(title: $0.title, style: $0.valueStyle, selected: service.volume == $0) - } - } - - var blockchainViewItems: [SelectorModule.ViewItem] { - service.allBlockchains.map { blockchain in - SelectorModule.ViewItem( - image: .url(blockchain.type.imageUrl, placeholder: "placeholder_rectangle_32"), - title: blockchain.name, - selected: service.blockchains.contains(blockchain) - ) - } - } - - var technicalIndicatorViewItems: [FilterViewItem] { - var cases = [FilterViewItem( - title: "selector.any".localized, - style: .normal, - selected: service.technicalAdvice == nil - )] - - cases.append(contentsOf: TechnicalAdvice.Advice.searchCases.map { advice in - FilterViewItem( - title: advice.searchTitle, - style: .normal, - selected: service.technicalAdvice == advice - ) - }) - - return cases - } - - var priceChangeTypeViewItems: [FilterViewItem] { - MarketModule.PriceChangeType.allCases.map { - FilterViewItem(title: $0.title, style: .normal, selected: service.priceChangeType == $0) - } - } - - var priceChangeViewItems: [FilterViewItem] { - MarketAdvancedSearchService.PriceChangeFilter.allCases.map { - FilterViewItem(title: $0.title, style: $0.valueStyle, selected: service.priceChange == $0) - } - } - - var marketInfos: [MarketInfo] { - guard case let .loaded(marketInfos) = service.state else { - return [] - } - - return marketInfos - } - - var priceChangeType: MarketModule.PriceChangeType { - service.priceChangeType - } - - func setCoinList(at index: Int) { - service.coinListCount = MarketAdvancedSearchService.CoinListCount.allCases[index] - } - - func setMarketCap(at index: Int) { - service.marketCap = valueFilters[index] - } - - func setVolume(at index: Int) { - service.volume = valueFilters[index] - } - - func setListedOnTopExchanges(isOn: Bool) { - service.listedOnTopExchanges = isOn - } - - func setGoodCexVolume(isOn: Bool) { - service.goodCexVolume = isOn - } - - func setGoodDexVolume(isOn: Bool) { - service.goodDexVolume = isOn - } - - func setGoodDistribution(isOn: Bool) { - service.goodDistribution = isOn - } - - func setBlockchains(indexes: [Int]) { - service.blockchains = indexes.map { service.allBlockchains[$0] } - } - - func setTechnicalAdvice(index: Int) { - if index == 0 { - service.technicalAdvice = nil - } - - service.technicalAdvice = TechnicalAdvice.Advice.searchCases.at(index: index - 1) - } - - func setPriceChangeType(at index: Int) { - service.priceChangeType = MarketModule.PriceChangeType.allCases[index] - } - - func setPriceChange(at index: Int) { - service.priceChange = MarketAdvancedSearchService.PriceChangeFilter.allCases[index] - } - - func setOutperformedBtc(isOn: Bool) { - service.outperformedBtc = isOn - } - - func setOutperformedEth(isOn: Bool) { - service.outperformedEth = isOn - } - - func setOutperformedBnb(isOn: Bool) { - service.outperformedBnb = isOn - } - - func setPriceCloseToATH(isOn: Bool) { - service.priceCloseToAth = isOn - } - - func setPriceCloseToATL(isOn: Bool) { - service.priceCloseToAtl = isOn - } - - func reset() { - service.reset() - } -} - -extension MarketAdvancedSearchViewModel { - struct FilterViewItem { - let title: String - let style: ValueStyle - let selected: Bool - } - - enum ValueStyle { - case normal - case positive - case negative - case none - } - - struct ViewItem { - let value: String - let valueStyle: ValueStyle - } - - enum ButtonState { - case loading - case emptyResults - case showResults(count: Int) - case error(String) - } -} - -extension MarketAdvancedSearchService.CoinListCount { - var title: String { - "market.advanced_search.top".localized(rawValue) - } -} - -extension MarketAdvancedSearchService.ValueFilter { - static let valuesByCurrencyCode: [String: [Self]] = [ - "USD": [.none, .lessM5, .m5m20, .m20m100, .m100b1, .b1b5, .moreB5], - "EUR": [.none, .lessM5, .m5m20, .m20m100, .m100b1, .b1b5, .moreB5], - "GBP": [.none, .lessM5, .m5m20, .m20m100, .m100b1, .b1b5, .moreB5], - "JPY": [.none, .lessM500, .m500b2, .b2b10, .b10b100, .b100b500, .moreB500], - "AUD": [.none, .lessM5, .m5m20, .m20m100, .m100b1, .b1b5, .moreB5], - "BRL": [.none, .lessM50, .m50m200, .m200b1, .b1b10, .b10b50, .moreB50], - "CAD": [.none, .lessM5, .m5m20, .m20m100, .m100b1, .b1b5, .moreB5], - "CHF": [.none, .lessM5, .m5m20, .m20m100, .m100b1, .b1b5, .moreB5], - "CNY": [.none, .lessM50, .m50m200, .m200b1, .b1b10, .b10b50, .moreB50], - "HKD": [.none, .lessM50, .m50m200, .m200b1, .b1b10, .b10b50, .moreB50], - "ILS": [.none, .lessM10, .m10m40, .m40m200, .m200b2, .b2b10, .moreB10], - "RUB": [.none, .lessM500, .m500b2, .b2b10, .b10b100, .b100b500, .moreB500], - "SGD": [.none, .lessM5, .m5m20, .m20m100, .m100b1, .b1b5, .moreB5], - ] - - var title: String { - switch self { - case .none: return "selector.any".localized - case .lessM5: return "market.advanced_search.less_5_m".localized - case .lessM10: return "market.advanced_search.less_10_m".localized - case .lessM50: return "market.advanced_search.less_50_m".localized - case .lessM500: return "market.advanced_search.less_500_m".localized - case .m5m20: return "market.advanced_search.m_5_m_20".localized - case .m10m40: return "market.advanced_search.m_10_m_40".localized - case .m40m200: return "market.advanced_search.m_40_m_200".localized - case .m50m200: return "market.advanced_search.m_50_m_200".localized - case .m20m100: return "market.advanced_search.m_20_m_100".localized - case .m100b1: return "market.advanced_search.m_100_b_1".localized - case .m200b1: return "market.advanced_search.m_200_b_1".localized - case .m200b2: return "market.advanced_search.m_200_b_2".localized - case .m500b2: return "market.advanced_search.m_500_b_2".localized - case .b1b5: return "market.advanced_search.b_1_b_5".localized - case .b1b10: return "market.advanced_search.b_1_b_10".localized - case .b2b10: return "market.advanced_search.b_2_b_10".localized - case .b10b50: return "market.advanced_search.b_10_b_50".localized - case .b10b100: return "market.advanced_search.b_10_b_100".localized - case .b100b500: return "market.advanced_search.b_100_b_500".localized - case .moreB5: return "market.advanced_search.more_5_b".localized - case .moreB10: return "market.advanced_search.more_10_b".localized - case .moreB50: return "market.advanced_search.more_50_b".localized - case .moreB500: return "market.advanced_search.more_500_b".localized - } - } - - var valueStyle: MarketAdvancedSearchViewModel.ValueStyle { - self == .none ? .none : .normal - } -} - -extension MarketAdvancedSearchService.PriceChangeFilter { - var title: String { - switch self { - case .none: return "selector.any".localized - case .plus10: return "> +10 %" - case .plus25: return "> +25 %" - case .plus50: return "> +50 %" - case .plus100: return "> +100 %" - case .minus10: return "< -10 %" - case .minus25: return "< -25 %" - case .minus50: return "< -50 %" - case .minus75: return "< -75 %" - } - } - - var valueStyle: MarketAdvancedSearchViewModel.ValueStyle { - switch self { - case .none: return .none - case .plus10, .plus25, .plus50, .plus100: return .positive - case .minus10, .minus25, .minus50, .minus75: return .negative - } - } -} - -extension TechnicalAdvice.Advice { - static var searchCases: [Self] { - [.strongBuy, .buy, .neutral, .sell, .strongSell, .overbought] - } - - var searchTitle: String { - switch self { - case .oversold, .overbought: return "market.advanced_search.technical_advice.risk_trade".localized - case .strongBuy: return "market.advanced_search.technical_advice.strong_buy".localized - case .buy: return "market.advanced_search.technical_advice.buy".localized - case .neutral: return "market.advanced_search.technical_advice.neutral".localized - case .sell: return "market.advanced_search.technical_advice.sell".localized - case .strongSell: return "market.advanced_search.technical_advice.strong_sell".localized - } - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketAdvancedSearchResults/MarketAdvancedSearchResultModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketAdvancedSearchResults/MarketAdvancedSearchResultModule.swift deleted file mode 100644 index 096096ed1d..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketAdvancedSearchResults/MarketAdvancedSearchResultModule.swift +++ /dev/null @@ -1,15 +0,0 @@ -import MarketKit -import UIKit - -enum MarketAdvancedSearchResultModule { - static func viewController(marketInfos: [MarketInfo], priceChangeType: MarketModule.PriceChangeType) -> UIViewController { - let service = MarketAdvancedSearchResultService(marketInfos: marketInfos, currencyManager: App.shared.currencyManager, priceChangeType: priceChangeType) - let watchlistToggleService = MarketWatchlistToggleService(coinUidService: service, favoritesManager: App.shared.favoritesManager, statPage: .advancedSearchResults) - - let decorator = MarketListMarketFieldDecorator(service: service, statPage: .advancedSearchResults) - let listViewModel = MarketListWatchViewModel(service: service, watchlistToggleService: watchlistToggleService, decorator: decorator) - let headerViewModel = MarketMultiSortHeaderViewModel(service: service, decorator: decorator) - - return MarketAdvancedSearchResultViewController(listViewModel: listViewModel, headerViewModel: headerViewModel) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketAdvancedSearchResults/MarketAdvancedSearchResultService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketAdvancedSearchResults/MarketAdvancedSearchResultService.swift deleted file mode 100644 index 8a5460dba9..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketAdvancedSearchResults/MarketAdvancedSearchResultService.swift +++ /dev/null @@ -1,67 +0,0 @@ -import Combine -import HsExtensions -import MarketKit -import RxRelay -import RxSwift - -class MarketAdvancedSearchResultService: IMarketMultiSortHeaderService { - typealias Item = MarketInfo - - private let marketInfos: [MarketInfo] - private let currencyManager: CurrencyManager - let priceChangeType: MarketModule.PriceChangeType - - @PostPublished private(set) var state: MarketListServiceState = .loading - - var sortingField: MarketModule.SortingField = .highestCap { - didSet { - syncState(reorder: true) - } - } - - init(marketInfos: [MarketInfo], currencyManager: CurrencyManager, priceChangeType: MarketModule.PriceChangeType) { - self.marketInfos = marketInfos - self.currencyManager = currencyManager - self.priceChangeType = priceChangeType - - syncState() - } - - private func syncState(reorder: Bool = false) { - state = .loaded(items: marketInfos.sorted(sortingField: sortingField, priceChangeType: priceChangeType), softUpdate: false, reorder: reorder) - } -} - -extension MarketAdvancedSearchResultService: IMarketListService { - var statePublisher: AnyPublisher, Never> { - $state - } - - func refresh() {} -} - -extension MarketAdvancedSearchResultService: IMarketListCoinUidService { - func coinUid(index: Int) -> String? { - guard case let .loaded(marketInfos, _, _) = state, index < marketInfos.count else { - return nil - } - - return marketInfos[index].fullCoin.coin.uid - } -} - -extension MarketAdvancedSearchResultService: IMarketListDecoratorService { - var initialIndex: Int { - 0 - } - - var currency: Currency { - currencyManager.baseCurrency - } - - func onUpdate(index _: Int) { - if case let .loaded(marketInfos, _, _) = state { - state = .loaded(items: marketInfos, softUpdate: false, reorder: false) - } - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketAdvancedSearchResults/MarketAdvancedSearchResultViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketAdvancedSearchResults/MarketAdvancedSearchResultViewController.swift deleted file mode 100644 index 46d3ab4916..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketAdvancedSearchResults/MarketAdvancedSearchResultViewController.swift +++ /dev/null @@ -1,38 +0,0 @@ -import SectionsTableView -import SnapKit -import ThemeKit -import UIKit - -class MarketAdvancedSearchResultViewController: MarketListViewController { - private let multiSortHeaderView: MarketMultiSortHeaderView - - override var viewController: UIViewController? { self } - override var headerView: UITableViewHeaderFooterView? { multiSortHeaderView } - override var refreshEnabled: Bool { false } - - init(listViewModel: IMarketListViewModel, headerViewModel: MarketMultiSortHeaderViewModel) { - multiSortHeaderView = MarketMultiSortHeaderView(viewModel: headerViewModel) - - super.init(listViewModel: listViewModel, statPage: .advancedSearchResults) - - multiSortHeaderView.viewController = self - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - title = "market.advanced_search_results.title".localized - - navigationItem.largeTitleDisplayMode = .never - navigationItem.rightBarButtonItem = UIBarButtonItem(title: "button.close".localized, style: .plain, target: self, action: #selector(onTapClose)) - } - - @objc private func onTapClose() { - dismiss(animated: true) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketCap/MarketMarketCapView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketCap/MarketMarketCapView.swift new file mode 100644 index 0000000000..96bd014e1a --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketCap/MarketMarketCapView.swift @@ -0,0 +1,190 @@ +import Kingfisher +import MarketKit +import SwiftUI + +struct MarketMarketCapView: View { + @StateObject var viewModel: MarketMarketCapViewModel + @StateObject var chartViewModel: MetricChartViewModel + @StateObject var watchlistViewModel: WatchlistViewModel + @Binding var isPresented: Bool + + @State private var presentedFullCoin: FullCoin? + + init(isPresented: Binding) { + _viewModel = StateObject(wrappedValue: MarketMarketCapViewModel()) + _chartViewModel = StateObject(wrappedValue: MetricChartViewModel.instance(type: .totalMarketCap)) + _watchlistViewModel = StateObject(wrappedValue: WatchlistViewModel(page: .globalMetricsMarketCap)) + _isPresented = isPresented + } + + var body: some View { + ThemeNavigationView { + ThemeView { + switch viewModel.state { + case .loading: + VStack(spacing: 0) { + header() + Spacer() + ProgressView() + Spacer() + } + case let .loaded(marketInfos): + ScrollViewReader { proxy in + ThemeList(bottomSpacing: .margin16, invisibleTopView: true) { + header() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + + chart() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + + list(marketInfos: marketInfos) + } + .onChange(of: viewModel.sortOrder) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + } + case .failed: + VStack(spacing: 0) { + header() + + SyncErrorView { + viewModel.sync() + } + } + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("button.close".localized) { + isPresented = false + } + } + } + .sheet(item: $presentedFullCoin) { fullCoin in + CoinPageViewNew(coinUid: fullCoin.coin.uid).ignoresSafeArea() + .onFirstAppear { stat(page: .globalMetricsMarketCap, event: .openCoin(coinUid: fullCoin.coin.uid)) } + } + } + } + + @ViewBuilder private func header() -> some View { + HStack(spacing: .margin32) { + VStack(spacing: .margin8) { + Text("market.market_cap.title".localized).themeHeadline1() + Text("market.market_cap.description".localized).themeSubhead2() + } + .padding(.vertical, .margin12) + + KFImage.url(URL(string: "total_mcap".headerImageUrl)) + .resizable() + .frame(width: 76, height: 108) + } + .padding(.leading, .margin16) + } + + @ViewBuilder private func chart() -> some View { + ChartView(viewModel: chartViewModel, configuration: .marketCapChart) + .frame(maxWidth: .infinity) + .onFirstAppear { + chartViewModel.start() + } + } + + @ViewBuilder private func listHeader(disabled: Bool = false) -> some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + Button(action: { + viewModel.sortOrder.toggle() + }) { + Text("market.market_cap.market_cap".localized) + } + .buttonStyle(SecondaryButtonStyle(style: .default, rightAccessory: .custom(image: sortIcon()))) + .disabled(disabled) + } + .padding(.horizontal, .margin16) + .padding(.vertical, .margin8) + } + } + + @ViewBuilder private func list(marketInfos: [MarketInfo]) -> some View { + Section { + ListForEach(marketInfos) { marketInfo in + let coin = marketInfo.fullCoin.coin + + ClickableRow(action: { + presentedFullCoin = marketInfo.fullCoin + }) { + itemContent( + coin: coin, + marketCap: marketInfo.marketCap, + price: marketInfo.price.flatMap { ValueFormatter.instance.formatFull(currency: viewModel.currency, value: $0) } ?? "n/a".localized, + rank: marketInfo.marketCapRank, + diff: marketInfo.priceChangeValue(timePeriod: HsTimePeriod.day1) + ) + } + .watchlistSwipeActions(viewModel: watchlistViewModel, coinUid: coin.uid) + } + } header: { + listHeader() + .listRowInsets(EdgeInsets()) + .background(Color.themeTyler) + } + } + + @ViewBuilder private func loadingList() -> some View { + Section { + ListForEach(Array(0 ... 10)) { index in + ListRow { + itemContent( + coin: nil, + marketCap: 123_456, + price: "$123.45", + rank: 12, + diff: index % 2 == 0 ? 12.34 : -12.34 + ) + .redacted() + } + } + } header: { + listHeader(disabled: true) + .listRowInsets(EdgeInsets()) + .background(Color.themeTyler) + } + } + + @ViewBuilder private func itemContent(coin: Coin?, marketCap: Decimal?, price: String, rank: Int?, diff: Decimal?) -> some View { + CoinIconView(coin: coin) + + VStack(spacing: 1) { + HStack(spacing: .margin8) { + Text(coin?.code ?? "CODE").textBody() + Spacer() + Text(price).textBody() + } + + HStack(spacing: .margin8) { + HStack(spacing: .margin4) { + if let rank { + BadgeViewNew(text: "\(rank)") + } + + if let marketCap, let formatted = ValueFormatter.instance.formatShort(currency: viewModel.currency, value: marketCap) { + Text(formatted).textSubhead2() + } + } + Spacer() + DiffText(diff) + } + } + } + + private func sortIcon() -> Image { + switch viewModel.sortOrder { + case .asc: return Image("arrow_medium_2_up_20") + case .desc: return Image("arrow_medium_2_down_20") + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketCap/MarketMarketCapViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketCap/MarketMarketCapViewModel.swift new file mode 100644 index 0000000000..a7ecf2543b --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketCap/MarketMarketCapViewModel.swift @@ -0,0 +1,92 @@ +import Combine +import Foundation +import HsExtensions +import MarketKit + +class MarketMarketCapViewModel: ObservableObject { + private let marketKit = App.shared.marketKit + private let currencyManager = App.shared.currencyManager + + private var cancellables = Set() + private var tasks = Set() + + private var internalState: State = .loading { + didSet { + syncState() + } + } + + @Published var state: State = .loading + + var sortOrder: MarketModule.SortOrder = .desc { + didSet { + stat(page: .globalMetricsMarketCap, event: .toggleSortDirection) + syncState() + } + } + + init() { + currencyManager.$baseCurrency + .sink { [weak self] _ in + self?.sync() + } + .store(in: &cancellables) + + sync() + } + + private func syncState() { + switch internalState { + case .loading: + state = .loading + case let .loaded(marketInfos): + let sortBy: MarketModule.SortBy + + switch sortOrder { + case .asc: sortBy = .lowestCap + case .desc: sortBy = .highestCap + } + + state = .loaded(marketInfos: marketInfos.sorted(sortBy: sortBy, timePeriod: .day1)) + case let .failed(error): + state = .failed(error: error) + } + } +} + +extension MarketMarketCapViewModel { + var currency: Currency { + currencyManager.baseCurrency + } + + func sync() { + tasks = Set() + + if case .failed = internalState { + internalState = .loading + } + + Task { [weak self, marketKit, currency] in + do { + let marketInfos = try await marketKit.marketInfos(top: MarketModule.Top.top100.rawValue, currencyCode: currency.code) + + await MainActor.run { [weak self] in + self?.internalState = .loaded(marketInfos: marketInfos) + } + } catch { + await MainActor.run { [weak self] in + self?.internalState = .failed(error: error) + } + } + } + .store(in: &tasks) + } +} + +extension MarketMarketCapViewModel { + enum State { + case loading + case loaded(marketInfos: [MarketInfo]) + case failed(error: Error) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketCategory/MarketCategoryMarketCapFetcher.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketCategory/MarketCategoryMarketCapFetcher.swift deleted file mode 100644 index e32221d729..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketCategory/MarketCategoryMarketCapFetcher.swift +++ /dev/null @@ -1,39 +0,0 @@ -import Chart -import Foundation -import MarketKit - -class MarketCategoryMarketCapFetcher { - private let marketKit: MarketKit.Kit - private let currencyManager: CurrencyManager - private let category: String - - init(currencyManager: CurrencyManager, marketKit: MarketKit.Kit, category: String) { - self.marketKit = marketKit - self.currencyManager = currencyManager - self.category = category - } -} - -extension MarketCategoryMarketCapFetcher: IMetricChartFetcher { - var valueType: MetricChartModule.ValueType { - .compactCurrencyValue(currencyManager.baseCurrency) - } - - var intervals: [HsPeriodType] { - [HsTimePeriod.day1, .week1, .month1].periodTypes - } - - func fetch(interval: HsPeriodType) async throws -> MetricChartModule.ItemData { - guard case let .byPeriod(interval) = interval else { - throw MetricChartModule.FetchError.onlyHsTimePeriod - } - - let points = try await marketKit.coinCategoryMarketCapChart(category: category, currencyCode: currencyManager.baseCurrency.code, timePeriod: interval) - - let items = points.map { point -> MetricChartModule.Item in - MetricChartModule.Item(value: point.marketCap, timestamp: point.timestamp) - } - - return MetricChartModule.ItemData(items: items, type: .regular) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketCategory/MarketCategoryModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketCategory/MarketCategoryModule.swift deleted file mode 100644 index 0438959068..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketCategory/MarketCategoryModule.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Chart -import MarketKit -import ThemeKit -import UIKit - -enum MarketCategoryModule { - static func viewController(category: CoinCategory) -> UIViewController { - let service = MarketCategoryService( - category: category, - marketKit: App.shared.marketKit, - languageManager: LanguageManager.shared - ) - - let listService = MarketFilteredListService(currencyManager: App.shared.currencyManager, provider: service, statPage: .coinCategory) - let watchlistToggleService = MarketWatchlistToggleService(coinUidService: listService, favoritesManager: App.shared.favoritesManager, statPage: .coinCategory) - - let marketCapFetcher = MarketCategoryMarketCapFetcher(currencyManager: App.shared.currencyManager, marketKit: App.shared.marketKit, category: category.uid) - let chartService = MetricChartService(chartFetcher: marketCapFetcher, interval: .byPeriod(.day1), statPage: .coinCategory) - let factory = MetricChartFactory(currentLocale: LanguageManager.shared.currentLocale) - let chartViewModel = MetricChartViewModel(service: chartService, factory: factory) - - let decorator = MarketListMarketFieldDecorator(service: listService, statPage: .coinCategory) - let viewModel = MarketCategoryViewModel(service: service) - let listViewModel = MarketListWatchViewModel(service: listService, watchlistToggleService: watchlistToggleService, decorator: decorator) - let headerViewModel = MarketMultiSortHeaderViewModel(service: listService, decorator: decorator) - - let viewController = MarketCategoryViewController(viewModel: viewModel, chartViewModel: chartViewModel, listViewModel: listViewModel, headerViewModel: headerViewModel) - - return ThemeNavigationController(rootViewController: viewController) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketCategory/MarketCategoryService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketCategory/MarketCategoryService.swift deleted file mode 100644 index c327b9620a..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketCategory/MarketCategoryService.swift +++ /dev/null @@ -1,25 +0,0 @@ -import MarketKit - -class MarketCategoryService { - let category: CoinCategory - private let marketKit: MarketKit.Kit - private let languageManager: LanguageManager - - init(category: CoinCategory, marketKit: MarketKit.Kit, languageManager: LanguageManager) { - self.category = category - self.marketKit = marketKit - self.languageManager = languageManager - } -} - -extension MarketCategoryService { - var currentLanguage: String { - languageManager.currentLanguage - } -} - -extension MarketCategoryService: IMarketFilteredListProvider { - func marketInfos(currencyCode: String) async throws -> [MarketInfo] { - try await marketKit.marketInfos(categoryUid: category.uid, currencyCode: currencyCode) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketCategory/MarketCategoryViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketCategory/MarketCategoryViewController.swift deleted file mode 100644 index 1187cd1670..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketCategory/MarketCategoryViewController.swift +++ /dev/null @@ -1,86 +0,0 @@ -import SectionsTableView -import SnapKit -import ThemeKit -import UIKit - -class MarketCategoryViewController: MarketListViewController { - private let viewModel: MarketCategoryViewModel - private let multiSortHeaderView: MarketMultiSortHeaderView - - override var viewController: UIViewController? { self } - override var headerView: UITableViewHeaderFooterView? { multiSortHeaderView } - override var refreshEnabled: Bool { false } - - private let chartViewModel: MetricChartViewModel - - /* Chart section */ - private let chartCell: ChartCell - private let chartRow: StaticRow - - init(viewModel: MarketCategoryViewModel, chartViewModel: MetricChartViewModel, listViewModel: IMarketListViewModel, headerViewModel: MarketMultiSortHeaderViewModel) { - self.viewModel = viewModel - self.chartViewModel = chartViewModel - multiSortHeaderView = MarketMultiSortHeaderView(viewModel: headerViewModel) - - chartCell = ChartCell(viewModel: chartViewModel, configuration: .baseChart) - chartRow = StaticRow( - cell: chartCell, - id: "chartView", - height: chartCell.cellHeight - ) - - super.init(listViewModel: listViewModel, statPage: .coinCategory) - - multiSortHeaderView.viewController = self - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - navigationItem.largeTitleDisplayMode = .never - navigationItem.rightBarButtonItem = UIBarButtonItem(title: "button.close".localized, style: .plain, target: self, action: #selector(onTapClose)) - - tableView.registerCell(forClass: MarketHeaderCell.self) - - chartRow.onReady = { [weak chartCell] in chartCell?.onLoad() } - chartViewModel.start() - } - - @objc private func onTapClose() { - dismiss(animated: true) - } - - override func topSections(loaded: Bool) -> [SectionProtocol] { - var sections = [Section( - id: "header", - rows: [ - Row( - id: "header", - height: MarketHeaderCell.height, - bind: { [weak self] cell, _ in - self?.bind(cell: cell) - } - ), - ] - )] - - if loaded { - sections.append(Section(id: "chart", rows: [chartRow])) - } - - return sections - } - - private func bind(cell: MarketHeaderCell) { - cell.set( - title: viewModel.title, - description: viewModel.description, - imageMode: .remote(imageUrl: viewModel.imageUrl) - ) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketCategory/MarketCategoryViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketCategory/MarketCategoryViewModel.swift deleted file mode 100644 index efd7bf43f7..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketCategory/MarketCategoryViewModel.swift +++ /dev/null @@ -1,21 +0,0 @@ -class MarketCategoryViewModel { - private let service: MarketCategoryService - - init(service: MarketCategoryService) { - self.service = service - } -} - -extension MarketCategoryViewModel { - var title: String { - service.category.name - } - - var description: String? { - service.category.descriptions[service.currentLanguage] ?? service.category.descriptions.first?.value - } - - var imageUrl: String { - service.category.imageUrl - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobal/MarketGlobalModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobal/MarketGlobalModule.swift deleted file mode 100644 index 547c84abee..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobal/MarketGlobalModule.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Chart -import RxSwift -import UIKit - -enum MarketGlobalModule { - static let dominance = "dominance" - - enum MetricsType { - case totalMarketCap, volume24h, defiCap, tvlInDefi - - var title: String { - switch self { - case .totalMarketCap: return "market.global.total_market_cap.title".localized - case .volume24h: return "market.global.volume_24h.title".localized - case .defiCap: return "market.global.defi_cap.title".localized - case .tvlInDefi: return "market.global.tvl_in_defi.title".localized - } - } - - var description: String { - switch self { - case .totalMarketCap: return "market.global.total_market_cap.description".localized - case .volume24h: return "market.global.volume_24h.description".localized - case .defiCap: return "market.global.defi_cap.description".localized - case .tvlInDefi: return "market.global.tvl_in_defi.description".localized - } - } - - var imageUid: String { - switch self { - case .totalMarketCap: return "total_mcap" - case .volume24h: return "total_volume" - case .defiCap: return "defi_cap" - case .tvlInDefi: return "tvl" - } - } - - var marketField: MarketModule.MarketField { - switch self { - case .totalMarketCap: return .marketCap - case .volume24h: return .volume - case .defiCap: return .marketCap - case .tvlInDefi: return .price - } - } - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/DefiCap/MarketGlobalDefiMetricService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/DefiCap/MarketGlobalDefiMetricService.swift deleted file mode 100644 index 2b64c74670..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/DefiCap/MarketGlobalDefiMetricService.swift +++ /dev/null @@ -1,120 +0,0 @@ -import Combine -import HsExtensions -import MarketKit -import RxRelay -import RxSwift - -class MarketGlobalDefiMetricService: IMarketSingleSortHeaderService { - typealias Item = DefiItem - - private let marketKit: MarketKit.Kit - private let currencyManager: CurrencyManager - private let disposeBag = DisposeBag() - private var tasks = Set() - - @PostPublished private(set) var state: MarketListServiceState = .loading - - var sortDirectionAscending: Bool = false { - didSet { - syncIfPossible() - - stat(page: .globalMetricsDefiCap, event: .toggleSortDirection) - } - } - - let initialIndex: Int = 1 - - init(marketKit: MarketKit.Kit, currencyManager: CurrencyManager) { - self.marketKit = marketKit - self.currencyManager = currencyManager - - syncMarketInfos() - } - - private func syncMarketInfos() { - tasks = Set() - - if case .failed = state { - state = .loading - } - - Task { [weak self, marketKit, currency] in - do { - let marketInfos = try await marketKit.marketInfos(top: MarketModule.MarketTop.top100.rawValue, currencyCode: currency.code, defi: true) - - let rankedItems = marketInfos.enumerated().map { index, info in - Item(marketInfo: info, tvlRank: index + 1) - } - - self?.sync(items: rankedItems) - } catch { - self?.state = .failed(error: error) - } - }.store(in: &tasks) - } - - private func sync(items: [Item], reorder: Bool = false) { - state = .loaded(items: sorted(items: items), softUpdate: false, reorder: reorder) - } - - private func syncIfPossible() { - guard case let .loaded(items, _, _) = state else { - return - } - - sync(items: items, reorder: true) - } - - func sorted(items: [Item]) -> [Item] { - items.sorted { lhsItem, rhsItem in - if sortDirectionAscending { - return lhsItem.tvlRank > rhsItem.tvlRank - } else { - return lhsItem.tvlRank < rhsItem.tvlRank - } - } - } -} - -extension MarketGlobalDefiMetricService: IMarketListService { - var statePublisher: AnyPublisher, Never> { - $state - } - - func refresh() { - syncMarketInfos() - } -} - -extension MarketGlobalDefiMetricService: IMarketListCoinUidService { - func coinUid(index: Int) -> String? { - guard case let .loaded(items, _, _) = state, index < items.count else { - return nil - } - - return items[index].marketInfo.fullCoin.coin.uid - } -} - -extension MarketGlobalDefiMetricService: IMarketListDecoratorService { - var currency: Currency { - currencyManager.baseCurrency - } - - var priceChangeType: MarketModule.PriceChangeType { - .day - } - - func onUpdate(index _: Int) { - if case let .loaded(items, _, _) = state { - state = .loaded(items: items, softUpdate: false, reorder: false) - } - } -} - -extension MarketGlobalDefiMetricService { - struct DefiItem { - let marketInfo: MarketInfo - let tvlRank: Int - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/DefiCap/MarketListDefiDecorator.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/DefiCap/MarketListDefiDecorator.swift deleted file mode 100644 index 617af21495..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/DefiCap/MarketListDefiDecorator.swift +++ /dev/null @@ -1,64 +0,0 @@ -import MarketKit - -class MarketListDefiDecorator { - typealias Item = MarketGlobalDefiMetricService.DefiItem - - private let service: IMarketListDecoratorService - - var marketField: MarketModule.MarketField { - didSet { - service.onUpdate(index: marketField.rawValue) - - stat(page: .globalMetricsDefiCap, event: .switchField(field: marketField.statField)) - } - } - - init(service: IMarketListDecoratorService) { - self.service = service - marketField = MarketModule.MarketField.allCases[service.initialIndex] - } -} - -extension MarketListDefiDecorator: IMarketSingleSortHeaderDecorator { - var allFields: [String] { - MarketModule.MarketField.allCases.map(\.title) - } - - var currentFieldIndex: Int { - MarketModule.MarketField.allCases.firstIndex(of: marketField) ?? 0 - } - - func setCurrentField(index: Int) { - marketField = MarketModule.MarketField.allCases[index] - } -} - -extension MarketListDefiDecorator: IMarketListDecorator { - func listViewItem(item: MarketGlobalDefiMetricService.DefiItem) -> MarketModule.ListViewItem { - let marketInfo = item.marketInfo - let currency = service.currency - - let price = marketInfo.price.flatMap { ValueFormatter.instance.formatShort(currency: currency, value: $0) } ?? "n/a".localized - - let dataValue: MarketModule.MarketDataValue - - switch marketField { - case .price: dataValue = .diff(marketInfo.priceChangeValue(type: service.priceChangeType)) - case .volume: dataValue = .volume(marketInfo.totalVolume.flatMap { ValueFormatter.instance.formatShort(currency: currency, value: $0) } ?? "n/a".localized) - case .marketCap: dataValue = .marketCap(marketInfo.marketCap.flatMap { ValueFormatter.instance.formatShort(currency: currency, value: $0) } ?? "n/a".localized) - } - - return MarketModule.ListViewItem( - uid: marketInfo.fullCoin.coin.uid, - iconUrl: marketInfo.fullCoin.coin.imageUrl, - iconShape: .square, - iconPlaceholderName: "placeholder_circle_32", - leftPrimaryValue: marketInfo.fullCoin.coin.code, - leftSecondaryValue: marketInfo.fullCoin.coin.name, - badge: "\(item.tvlRank)", - badgeSecondaryValue: nil, - rightPrimaryValue: price, - rightSecondaryValue: dataValue - ) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/MarketGlobalMetricModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/MarketGlobalMetricModule.swift deleted file mode 100644 index 8b396c2c8b..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/MarketGlobalMetricModule.swift +++ /dev/null @@ -1,106 +0,0 @@ -import Chart -import ThemeKit -import UIKit - -enum MarketGlobalMetricModule { - static func viewController(type: MarketGlobalModule.MetricsType) -> UIViewController { - let viewController: UIViewController - - switch type { - case .totalMarketCap, .volume24h: viewController = globalMetricViewController(type: type) - case .defiCap: viewController = defiCapViewController() - case .tvlInDefi: viewController = tvlInDefiViewController() - } - - return ThemeNavigationController(rootViewController: viewController) - } - - private static func globalMetricViewController(type: MarketGlobalModule.MetricsType) -> UIViewController { - let service = MarketGlobalMetricService( - marketKit: App.shared.marketKit, - currencyManager: App.shared.currencyManager, - metricsType: type - ) - - let watchlistToggleService = MarketWatchlistToggleService( - coinUidService: service, - favoritesManager: App.shared.favoritesManager, - statPage: type.statPage - ) - - let decorator = MarketListMarketFieldDecorator(service: service, statPage: type.statPage) - let listViewModel = MarketListWatchViewModel(service: service, watchlistToggleService: watchlistToggleService, decorator: decorator) - let headerViewModel = MarketSingleSortHeaderViewModel(service: service, decorator: decorator) - - let chartFetcher = MarketGlobalFetcher(currencyManager: App.shared.currencyManager, marketKit: App.shared.marketKit, metricsType: type) - let chartService = MetricChartService( - chartFetcher: chartFetcher, - interval: .byPeriod(.day1), - statPage: type.statPage - ) - - let factory = MetricChartFactory(currentLocale: LanguageManager.shared.currentLocale) - let chartViewModel = MetricChartViewModel(service: chartService, factory: factory) - - return MarketGlobalMetricViewController(listViewModel: listViewModel, headerViewModel: headerViewModel, chartViewModel: chartViewModel, metricsType: type) - } - - private static func defiCapViewController() -> UIViewController { - let service = MarketGlobalDefiMetricService( - marketKit: App.shared.marketKit, - currencyManager: App.shared.currencyManager - ) - - let watchlistToggleService = MarketWatchlistToggleService( - coinUidService: service, - favoritesManager: App.shared.favoritesManager, - statPage: .globalMetricsDefiCap - ) - - let decorator = MarketListDefiDecorator(service: service) - let listViewModel = MarketListWatchViewModel(service: service, watchlistToggleService: watchlistToggleService, decorator: decorator) - let headerViewModel = MarketSingleSortHeaderViewModel(service: service, decorator: decorator) - - let chartFetcher = MarketGlobalFetcher(currencyManager: App.shared.currencyManager, marketKit: App.shared.marketKit, metricsType: .defiCap) - let chartService = MetricChartService( - chartFetcher: chartFetcher, - interval: .byPeriod(.day1), - statPage: .globalMetricsDefiCap - ) - - let factory = MetricChartFactory(currentLocale: LanguageManager.shared.currentLocale) - let chartViewModel = MetricChartViewModel(service: chartService, factory: factory) - - return MarketGlobalMetricViewController(listViewModel: listViewModel, headerViewModel: headerViewModel, chartViewModel: chartViewModel, metricsType: MarketGlobalModule.MetricsType.defiCap) - } - - static func tvlInDefiViewController() -> UIViewController { - let service = MarketGlobalTvlMetricService( - marketKit: App.shared.marketKit, - currencyManager: App.shared.currencyManager - ) - - let watchlistToggleService = MarketWatchlistToggleService( - coinUidService: service, - favoritesManager: App.shared.favoritesManager, - statPage: .globalMetricsTvlInDefi - ) - - let decorator = MarketListTvlDecorator(service: service) - let listViewModel = MarketListWatchViewModel(service: service, watchlistToggleService: watchlistToggleService, decorator: decorator) - let headerViewModel = MarketTvlSortHeaderViewModel(service: service, decorator: decorator) - - let chartFetcher = MarketGlobalTvlFetcher(marketKit: App.shared.marketKit, currencyManager: App.shared.currencyManager, marketGlobalTvlPlatformService: service) - let chartService = MetricChartService( - chartFetcher: chartFetcher, - interval: .byPeriod(.day1), - statPage: .globalMetricsTvlInDefi - ) - service.chartService = chartService - - let factory = MetricChartFactory(currentLocale: LanguageManager.shared.currentLocale) - let chartViewModel = MetricChartViewModel(service: chartService, factory: factory) - - return MarketGlobalTvlMetricViewController(listViewModel: listViewModel, headerViewModel: headerViewModel, chartViewModel: chartViewModel) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/MarketGlobalMetricService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/MarketGlobalMetricService.swift deleted file mode 100644 index 4faf88f3be..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/MarketGlobalMetricService.swift +++ /dev/null @@ -1,109 +0,0 @@ -import Combine -import HsExtensions -import MarketKit -import RxRelay -import RxSwift - -class MarketGlobalMetricService: IMarketSingleSortHeaderService { - typealias Item = MarketInfo - - private let marketKit: MarketKit.Kit - private let currencyManager: CurrencyManager - private let disposeBag = DisposeBag() - private var tasks = Set() - - @PostPublished private(set) var state: MarketListServiceState = .loading - - var sortDirectionAscending: Bool = false { - didSet { - syncIfPossible() - - stat(page: metricsType.statPage, event: .toggleSortDirection) - } - } - - private let metricsType: MarketGlobalModule.MetricsType - - let initialIndex: Int - - init(marketKit: MarketKit.Kit, currencyManager: CurrencyManager, metricsType: MarketGlobalModule.MetricsType) { - self.marketKit = marketKit - self.currencyManager = currencyManager - self.metricsType = metricsType - initialIndex = metricsType.marketField.rawValue - - syncMarketInfos() - } - - private func syncMarketInfos() { - tasks = Set() - - if case .failed = state { - state = .loading - } - - Task { [weak self, marketKit, currency] in - do { - let marketInfos = try await marketKit.marketInfos(top: MarketModule.MarketTop.top100.rawValue, currencyCode: currency.code) - self?.sync(marketInfos: marketInfos) - } catch { - self?.state = .failed(error: error) - } - }.store(in: &tasks) - } - - private var sortingField: MarketModule.SortingField { - switch metricsType { - case .volume24h: return sortDirectionAscending ? .lowestVolume : .highestVolume - default: return sortDirectionAscending ? .lowestCap : .highestCap - } - } - - private func sync(marketInfos: [MarketInfo], reorder: Bool = false) { - state = .loaded(items: marketInfos.sorted(sortingField: sortingField, priceChangeType: priceChangeType), softUpdate: false, reorder: reorder) - } - - private func syncIfPossible() { - guard case let .loaded(marketInfos, _, _) = state else { - return - } - - sync(marketInfos: marketInfos, reorder: true) - } -} - -extension MarketGlobalMetricService: IMarketListService { - var statePublisher: AnyPublisher, Never> { - $state - } - - func refresh() { - syncMarketInfos() - } -} - -extension MarketGlobalMetricService: IMarketListCoinUidService { - func coinUid(index: Int) -> String? { - guard case let .loaded(marketInfos, _, _) = state, index < marketInfos.count else { - return nil - } - - return marketInfos[index].fullCoin.coin.uid - } -} - -extension MarketGlobalMetricService: IMarketListDecoratorService { - var currency: Currency { - currencyManager.baseCurrency - } - - var priceChangeType: MarketModule.PriceChangeType { - .day - } - - func onUpdate(index _: Int) { - if case let .loaded(marketInfos, _, _) = state { - state = .loaded(items: marketInfos, softUpdate: false, reorder: false) - } - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/MarketGlobalMetricViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/MarketGlobalMetricViewController.swift deleted file mode 100644 index 3b2317ae57..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/MarketGlobalMetricViewController.swift +++ /dev/null @@ -1,108 +0,0 @@ -import Chart -import ComponentKit -import RxSwift -import SectionsTableView -import SnapKit -import ThemeKit -import UIKit - -class MarketGlobalMetricViewController: MarketListViewController { - private let metricsType: MarketGlobalModule.MetricsType - private let disposeBag = DisposeBag() - private let sortHeaderView: UITableViewHeaderFooterView - - override var headerView: UITableViewHeaderFooterView? { sortHeaderView } - - override var viewController: UIViewController? { self } - override var refreshEnabled: Bool { false } - - private let chartViewModel: MetricChartViewModel - private let chartCell: ChartCell - private let chartRow: StaticRow - - init(listViewModel: IMarketListViewModel, headerViewModel: MarketSingleSortHeaderViewModel, chartViewModel: MetricChartViewModel, metricsType: MarketGlobalModule.MetricsType) { - self.chartViewModel = chartViewModel - self.metricsType = metricsType - sortHeaderView = MarketSingleSortHeaderView(viewModel: headerViewModel) - - let configuration: ChartConfiguration - switch metricsType { - case .totalMarketCap: configuration = .marketCapChart - default: configuration = .baseChart - } - - chartCell = ChartCell(viewModel: chartViewModel, configuration: configuration) - chartRow = StaticRow( - cell: chartCell, - id: "chartView", - height: chartCell.cellHeight - ) - - let statPage: StatPage - - switch metricsType { - case .totalMarketCap: statPage = .globalMetricsMarketCap - case .volume24h: statPage = .globalMetricsVolume - case .defiCap: statPage = .globalMetricsDefiCap - case .tvlInDefi: statPage = .globalMetricsTvlInDefi - } - - super.init(listViewModel: listViewModel, statPage: statPage) - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - navigationItem.largeTitleDisplayMode = .never - navigationItem.rightBarButtonItem = UIBarButtonItem(title: "button.close".localized, style: .plain, target: self, action: #selector(onTapClose)) - - tableView.registerCell(forClass: MarketHeaderCell.self) - - chartRow.onReady = { [weak chartCell] in chartCell?.onLoad() } - - tableView.buildSections() - chartViewModel.start() - } - - @objc private func onTapClose() { - dismiss(animated: true) - } - - override func topSections(loaded: Bool) -> [SectionProtocol] { - guard loaded else { - return [] - } - - return [ - Section( - id: "header", - rows: [ - Row( - id: "header", - height: MarketHeaderCell.height, - bind: { [weak self] cell, _ in - self?.bind(cell: cell) - } - ), - ] - ), - Section( - id: "chart", - rows: [chartRow] - ), - ] - } - - private func bind(cell: MarketHeaderCell) { - cell.set( - title: metricsType.title, - description: metricsType.description, - imageMode: .remote(imageUrl: metricsType.imageUid.headerImageUrl) - ) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/TvlInDefi/MarketGlobalTvlFetcher.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/TvlInDefi/MarketGlobalTvlFetcher.swift deleted file mode 100644 index f10abd77d2..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/TvlInDefi/MarketGlobalTvlFetcher.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Chart -import Combine -import Foundation -import MarketKit - -class MarketGlobalTvlFetcher { - private let marketKit: MarketKit.Kit - private let currencyManager: CurrencyManager - private let service: MarketGlobalTvlMetricService - - private let needUpdateSubject = PassthroughSubject() - - init(marketKit: MarketKit.Kit, currencyManager: CurrencyManager, marketGlobalTvlPlatformService: MarketGlobalTvlMetricService) { - self.marketKit = marketKit - self.currencyManager = currencyManager - service = marketGlobalTvlPlatformService - } -} - -extension MarketGlobalTvlFetcher: IMetricChartFetcher { - var valueType: MetricChartModule.ValueType { - .compactCurrencyValue(currencyManager.baseCurrency) - } - - var needUpdatePublisher: AnyPublisher { - service.$marketPlatformField.map { _ in () }.eraseToAnyPublisher() - } - - func fetch(interval: HsPeriodType) async throws -> MetricChartModule.ItemData { - guard case let .byPeriod(interval) = interval else { - throw MetricChartModule.FetchError.onlyHsTimePeriod - } - - let points = try await marketKit.marketInfoGlobalTvl(platform: service.marketPlatformField.chain, currencyCode: currencyManager.baseCurrency.code, timePeriod: interval) - - let items = points.map { point -> MetricChartModule.Item in - MetricChartModule.Item(value: point.value, timestamp: point.timestamp) - } - - return MetricChartModule.ItemData(items: items, type: .regular) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/TvlInDefi/MarketGlobalTvlMetricService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/TvlInDefi/MarketGlobalTvlMetricService.swift deleted file mode 100644 index 74648f8f8c..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/TvlInDefi/MarketGlobalTvlMetricService.swift +++ /dev/null @@ -1,171 +0,0 @@ -import Combine -import Foundation -import HsExtensions -import MarketKit - -class MarketGlobalTvlMetricService { - typealias Item = DefiCoin - - private let marketKit: MarketKit.Kit - private let currencyManager: CurrencyManager - - private var tasks = Set() - private var cancellables = Set() - - weak var chartService: MetricChartService? { - didSet { - subscribeChart() - } - } - - private var internalState: State = .loading { - didSet { - syncState() - } - } - - @PostPublished private(set) var state: MarketListServiceState = .loading - - var sortDirectionAscending: Bool = false { - didSet { - syncIfPossible(reorder: true) - - stat(page: .globalMetricsTvlInDefi, event: .toggleSortDirection) - } - } - - @PostPublished var marketPlatformField: MarketModule.MarketPlatformField = .all { - didSet { - syncIfPossible(reorder: true) - - stat(page: .globalMetricsTvlInDefi, event: .switchTvlChain(chain: marketPlatformField.statTvlChain)) - } - } - - var marketTvlField: MarketModule.MarketTvlField = .diff { - didSet { - syncIfPossible() - - stat(page: .globalMetricsTvlInDefi, event: .toggleTvlField) - } - } - - private(set) var priceChangePeriod: HsPeriodType = .byPeriod(.day1) { - didSet { - syncIfPossible() - } - } - - init(marketKit: MarketKit.Kit, currencyManager: CurrencyManager) { - self.marketKit = marketKit - self.currencyManager = currencyManager - - syncDefiCoins() - } - - private func syncDefiCoins() { - tasks = Set() - - if case .failed = state { - internalState = .loading - } - - Task { [weak self, marketKit, currency] in - do { - let defiCoins = try await marketKit.defiCoins(currencyCode: currency.code) - self?.internalState = .loaded(defiCoins: defiCoins) - } catch { - self?.internalState = .failed(error: error) - } - }.store(in: &tasks) - } - - private func syncState(reorder: Bool = false) { - switch internalState { - case .loading: - state = .loading - case let .loaded(defiCoins): - let defiCoins = defiCoins - .filter { defiCoin in - switch marketPlatformField { - case .all: return true - default: return defiCoin.chains.contains(marketPlatformField.chain) - } - } - .sorted { lhsDefiCoin, rhsDefiCoin in - let lhsTvl = lhsDefiCoin.tvl(marketPlatformField: marketPlatformField) ?? 0 - let rhsTvl = rhsDefiCoin.tvl(marketPlatformField: marketPlatformField) ?? 0 - return sortDirectionAscending ? lhsTvl < rhsTvl : lhsTvl > rhsTvl - } - state = .loaded(items: defiCoins, softUpdate: false, reorder: reorder) - case let .failed(error): - state = .failed(error: error) - } - } - - private func syncIfPossible(reorder: Bool = false) { - guard case .loaded = internalState else { - return - } - - syncState(reorder: reorder) - } - - private func subscribeChart() { - cancellables = Set() - - guard let chartService else { - return - } - - chartService.$interval - .sink { [weak self] in self?.priceChangePeriod = $0 } - .store(in: &cancellables) - } -} - -extension MarketGlobalTvlMetricService { - var currency: Currency { - currencyManager.baseCurrency - } -} - -extension MarketGlobalTvlMetricService: IMarketListService { - var statePublisher: AnyPublisher, Never> { - $state - } - - func refresh() { - syncDefiCoins() - } -} - -extension MarketGlobalTvlMetricService: IMarketListCoinUidService { - func coinUid(index: Int) -> String? { - guard case let .loaded(defiCoins, _, _) = state, index < defiCoins.count else { - return nil - } - - switch defiCoins[index].type { - case let .fullCoin(fullCoin): return fullCoin.coin.uid - default: return nil - } - } -} - -extension MarketGlobalTvlMetricService { - private enum State { - case loading - case loaded(defiCoins: [DefiCoin]) - case failed(error: Error) - } -} - -extension DefiCoin { - func tvl(marketPlatformField: MarketModule.MarketPlatformField) -> Decimal? { - switch marketPlatformField { - case .all: return tvl - default: return chainTvls[marketPlatformField.chain] - } - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/TvlInDefi/MarketGlobalTvlMetricViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/TvlInDefi/MarketGlobalTvlMetricViewController.swift deleted file mode 100644 index fceef3e06c..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/TvlInDefi/MarketGlobalTvlMetricViewController.swift +++ /dev/null @@ -1,99 +0,0 @@ -import Chart -import ComponentKit -import RxSwift -import SectionsTableView -import SnapKit -import ThemeKit -import UIKit - -class MarketGlobalTvlMetricViewController: MarketListViewController { - private let disposeBag = DisposeBag() - - private let headerViewModel: MarketTvlSortHeaderViewModel - private let sortHeaderView: MarketTvlSortHeaderView - - override var headerView: UITableViewHeaderFooterView? { sortHeaderView } - - override var viewController: UIViewController? { self } - override var refreshEnabled: Bool { false } - - private let chartViewModel: MetricChartViewModel - private let chartCell: ChartCell - private let chartRow: StaticRow - - init(listViewModel: IMarketListViewModel, headerViewModel: MarketTvlSortHeaderViewModel, chartViewModel: MetricChartViewModel) { - self.chartViewModel = chartViewModel - self.headerViewModel = headerViewModel - - sortHeaderView = MarketTvlSortHeaderView(viewModel: headerViewModel) - - chartCell = ChartCell(viewModel: chartViewModel, configuration: .baseChart) - chartRow = StaticRow( - cell: chartCell, - id: "chartView", - height: chartCell.cellHeight - ) - - super.init(listViewModel: listViewModel, statPage: .globalMetricsTvlInDefi) - - sortHeaderView.viewController = self - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - navigationItem.largeTitleDisplayMode = .never - navigationItem.rightBarButtonItem = UIBarButtonItem(title: "button.close".localized, style: .plain, target: self, action: #selector(onTapClose)) - - tableView.registerCell(forClass: MarketHeaderCell.self) - - chartRow.onReady = { [weak chartCell] in chartCell?.onLoad() } - - tableView.buildSections() - chartViewModel.start() - } - - @objc private func onTapClose() { - dismiss(animated: true) - } - - override func topSections(loaded: Bool) -> [SectionProtocol] { - guard loaded else { - return [] - } - - return [ - Section( - id: "header", - rows: [ - Row( - id: "header", - height: MarketHeaderCell.height, - bind: { [weak self] cell, _ in - self?.bind(cell: cell) - } - ), - ] - ), - Section( - id: "chart", - rows: [chartRow] - ), - ] - } - - private func bind(cell: MarketHeaderCell) { - let metricsType: MarketGlobalModule.MetricsType = .tvlInDefi - - cell.set( - title: metricsType.title, - description: metricsType.description, - imageMode: .remote(imageUrl: metricsType.imageUid.headerImageUrl) - ) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/TvlInDefi/MarketListTvlDecorator.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/TvlInDefi/MarketListTvlDecorator.swift deleted file mode 100644 index be232c4a2b..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/TvlInDefi/MarketListTvlDecorator.swift +++ /dev/null @@ -1,76 +0,0 @@ -import Foundation -import MarketKit - -class MarketListTvlDecorator { - typealias Item = DefiCoin - - private let service: MarketGlobalTvlMetricService - - init(service: MarketGlobalTvlMetricService) { - self.service = service - } -} - -extension MarketListTvlDecorator: IMarketListDecorator { - func listViewItem(item defiCoin: DefiCoin) -> MarketModule.ListViewItem { - let currency = service.currency - - var uid: String? - let iconUrl: String - let iconPlaceholderName: String - let name: String - - switch defiCoin.type { - case let .fullCoin(fullCoin): - uid = fullCoin.coin.uid - iconUrl = fullCoin.coin.imageUrl - iconPlaceholderName = "placeholder_circle_32" - name = fullCoin.coin.name - case let .defiCoin(defiName, logo): - iconUrl = logo - iconPlaceholderName = "placeholder_circle_32" - name = defiName - } - - var tvl: Decimal? - let diff: MarketModule.MarketDataValue - - switch service.marketPlatformField { - case .all: - tvl = defiCoin.tvl - - var tvlChange: Decimal? - switch service.priceChangePeriod { - case .byPeriod(.day1): tvlChange = defiCoin.tvlChange1d - case .byPeriod(.week1): tvlChange = defiCoin.tvlChange1w - case .byPeriod(.week2): tvlChange = defiCoin.tvlChange2w - case .byPeriod(.month1): tvlChange = defiCoin.tvlChange1m - case .byPeriod(.month3): tvlChange = defiCoin.tvlChange3m - case .byPeriod(.month6): tvlChange = defiCoin.tvlChange6m - case .byPeriod(.year1): tvlChange = defiCoin.tvlChange1y - default: () - } - - switch service.marketTvlField { - case .diff: diff = .diff(tvlChange) - case .value: diff = .valueDiff(CurrencyValue(currency: currency, value: defiCoin.tvl), tvlChange) - } - default: - tvl = defiCoin.chainTvls[service.marketPlatformField.chain] - diff = .diff(nil) - } - - return MarketModule.ListViewItem( - uid: uid, - iconUrl: iconUrl, - iconShape: .square, - iconPlaceholderName: iconPlaceholderName, - leftPrimaryValue: name, - leftSecondaryValue: defiCoin.chains.count == 1 ? defiCoin.chains[0] : "market.global.tvl_in_defi.multi_chain".localized, - badge: "\(defiCoin.tvlRank)", - badgeSecondaryValue: nil, - rightPrimaryValue: tvl.flatMap { ValueFormatter.instance.formatShort(currency: currency, value: $0) } ?? "n/a".localized, - rightSecondaryValue: diff - ) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketHeaderCell.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketHeaderCell.swift deleted file mode 100644 index 83d17f4e54..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketHeaderCell.swift +++ /dev/null @@ -1,66 +0,0 @@ -import UIKit - -class MarketHeaderCell: UITableViewCell { - static let height: CGFloat = 108 - - private let titleLabel = UILabel() - private let descriptionLabel = UILabel() - private let rightImageView = UIImageView() - - override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - backgroundColor = .clear - selectionStyle = .none - - contentView.addSubview(titleLabel) - titleLabel.snp.makeConstraints { make in - make.leading.equalToSuperview().inset(CGFloat.margin16) - make.top.equalToSuperview().inset(CGFloat.margin12) - } - - titleLabel.font = .headline1 - titleLabel.textColor = .themeLeah - - contentView.addSubview(descriptionLabel) - descriptionLabel.snp.makeConstraints { make in - make.leading.trailing.equalTo(titleLabel) - make.top.equalTo(titleLabel.snp.bottom).offset(CGFloat.margin8) - } - - descriptionLabel.numberOfLines = 0 - descriptionLabel.font = .subhead2 - descriptionLabel.textColor = .themeGray - - contentView.addSubview(rightImageView) - rightImageView.snp.makeConstraints { make in - make.leading.equalTo(titleLabel.snp.trailing).offset(CGFloat.margin16) - make.top.trailing.bottom.equalToSuperview() - make.width.equalTo(76) - } - } - - @available(*, unavailable) - public required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func set(title: String, description: String?, imageMode: ImageMode) { - titleLabel.text = title - descriptionLabel.text = description - - switch imageMode { - case let .local(image): - rightImageView.image = image - case let .remote(imageUrl): - rightImageView.setImage(withUrlString: imageUrl, placeholder: nil) - } - } -} - -extension MarketHeaderCell { - enum ImageMode { - case local(image: UIImage?) - case remote(imageUrl: String) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketList/MarketListMarketFieldDecorator.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketList/MarketListMarketFieldDecorator.swift deleted file mode 100644 index 9c5e425292..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketList/MarketListMarketFieldDecorator.swift +++ /dev/null @@ -1,66 +0,0 @@ -import MarketKit - -class MarketListMarketFieldDecorator { - typealias Item = MarketInfo - - private let service: IMarketListDecoratorService - private let statPage: StatPage - - var marketField: MarketModule.MarketField { - didSet { - service.onUpdate(index: marketField.rawValue) - - stat(page: statPage, event: .switchField(field: marketField.statField)) - } - } - - init(service: IMarketListDecoratorService, statPage: StatPage) { - self.service = service - self.statPage = statPage - - marketField = MarketModule.MarketField.allCases[service.initialIndex] - } -} - -extension MarketListMarketFieldDecorator: IMarketSingleSortHeaderDecorator { - var allFields: [String] { - MarketModule.MarketField.allCases.map(\.title) - } - - var currentFieldIndex: Int { - MarketModule.MarketField.allCases.firstIndex(of: marketField) ?? 0 - } - - func setCurrentField(index: Int) { - marketField = MarketModule.MarketField.allCases[index] - } -} - -extension MarketListMarketFieldDecorator: IMarketListDecorator { - func listViewItem(item marketInfo: MarketInfo) -> MarketModule.ListViewItem { - let currency = service.currency - - let price = marketInfo.price.flatMap { ValueFormatter.instance.formatFull(currency: currency, value: $0) } ?? "n/a".localized - - let dataValue: MarketModule.MarketDataValue - - switch marketField { - case .price: dataValue = .diff(marketInfo.priceChangeValue(type: service.priceChangeType)) - case .volume: dataValue = .volume(marketInfo.totalVolume.flatMap { ValueFormatter.instance.formatShort(currency: currency, value: $0) } ?? "n/a".localized) - case .marketCap: dataValue = .marketCap(marketInfo.marketCap.flatMap { ValueFormatter.instance.formatShort(currency: currency, value: $0) } ?? "n/a".localized) - } - - return MarketModule.ListViewItem( - uid: marketInfo.fullCoin.coin.uid, - iconUrl: marketInfo.fullCoin.coin.imageUrl, - iconShape: .full, - iconPlaceholderName: "placeholder_circle_32", - leftPrimaryValue: marketInfo.fullCoin.coin.code, - leftSecondaryValue: marketInfo.fullCoin.coin.name, - badge: marketInfo.marketCapRank.map { "\($0)" }, - badgeSecondaryValue: nil, - rightPrimaryValue: price, - rightSecondaryValue: dataValue - ) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketList/MarketListViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketList/MarketListViewController.swift deleted file mode 100644 index fbfa742cdf..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketList/MarketListViewController.swift +++ /dev/null @@ -1,241 +0,0 @@ -import ComponentKit -import HUD -import RxCocoa -import RxSwift -import SectionsTableView -import ThemeKit -import UIKit - -protocol IMarketListViewModel { - var viewItemDataDriver: Driver { get } - var loadingDriver: Driver { get } - var syncErrorDriver: Driver { get } - var scrollToTopSignal: Signal { get } - func refresh() -} - -class MarketListViewController: ThemeViewController { - private let listViewModel: IMarketListViewModel - private let statPage: StatPage - private let disposeBag = DisposeBag() - - let tableView = SectionsTableView(style: .plain) - private let spinner = HUDActivityView.create(with: .medium24) - private let errorView = PlaceholderViewModule.reachabilityView() - private let refreshControl = UIRefreshControl() - - private var viewItems: [MarketModule.ListViewItem]? - - var viewController: UIViewController? { self } - var headerView: UITableViewHeaderFooterView? { nil } - var emptyView: UIView? { nil } - var refreshEnabled: Bool { true } - func topSections(loaded _: Bool) -> [SectionProtocol] { [] } - - init(listViewModel: IMarketListViewModel, statPage: StatPage) { - self.listViewModel = listViewModel - self.statPage = statPage - - super.init() - - if let watchViewModel = listViewModel as? IMarketListWatchViewModel { - subscribe(disposeBag, watchViewModel.favoriteDriver) { [weak self] in self?.showAddedToWatchlist() } - subscribe(disposeBag, watchViewModel.unfavoriteDriver) { [weak self] in self?.showRemovedFromWatchlist() } - subscribe(disposeBag, watchViewModel.failDriver) { error in HudHelper.instance.show(banner: .error(string: error.localized)) } - } - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - refreshControl.tintColor = .themeLeah - refreshControl.alpha = 0.6 - refreshControl.addTarget(self, action: #selector(onRefresh), for: .valueChanged) - - view.addSubview(tableView) - tableView.snp.makeConstraints { maker in - maker.edges.equalToSuperview() - } - - tableView.sectionHeaderTopPadding = 0 - tableView.separatorStyle = .none - tableView.backgroundColor = .clear - - tableView.sectionDataSource = self - - if let emptyView { - view.addSubview(emptyView) - emptyView.snp.makeConstraints { maker in - maker.edges.equalTo(view.safeAreaLayoutGuide) - } - } - - view.addSubview(spinner) - spinner.snp.makeConstraints { maker in - maker.center.equalToSuperview() - } - - spinner.startAnimating() - - view.addSubview(errorView) - errorView.snp.makeConstraints { maker in - maker.edges.equalTo(view.safeAreaLayoutGuide) - } - - errorView.configureSyncError(action: { [weak self] in self?.onRetry() }) - - subscribe(disposeBag, listViewModel.viewItemDataDriver) { [weak self] in self?.sync(viewItemData: $0) } - subscribe(disposeBag, listViewModel.loadingDriver) { [weak self] loading in - self?.spinner.isHidden = !loading - } - subscribe(disposeBag, listViewModel.syncErrorDriver) { [weak self] visible in - self?.errorView.isHidden = !visible - } - subscribe(disposeBag, listViewModel.scrollToTopSignal) { [weak self] in self?.scrollToTop() } - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if refreshEnabled { - tableView.refreshControl = refreshControl - } - } - - @objc private func onRetry() { - refresh() - } - - @objc private func onRefresh() { - refresh() - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in - self?.refreshControl.endRefreshing() - } - } - - private func refresh() { - listViewModel.refresh() - } - - private func sync(viewItemData: MarketModule.ListViewItemData?) { - viewItems = viewItemData?.viewItems - - if let viewItems, viewItems.isEmpty { - emptyView?.isHidden = false - } else { - emptyView?.isHidden = true - } - - if let viewItems, !viewItems.isEmpty { - tableView.bounces = true - } else { - tableView.bounces = false - } - - if let viewItemData { - tableView.reload(animated: viewItemData.softUpdate) - } else { - tableView.reload() - } - } - - func onSelect(viewItem: MarketModule.ListViewItem) { - guard let coinUid = viewItem.uid, let module = CoinPageModule.viewController(coinUid: coinUid) else { - HudHelper.instance.show(banner: .attention(string: "market.project_has_no_coin".localized)) - return - } - - viewController?.present(module, animated: true) - stat(page: statPage, event: .openCoin(coinUid: coinUid)) - } - - private func rowActions(index: Int) -> [RowAction] { - guard let watchListViewModel = listViewModel as? IMarketListWatchViewModel else { - return [] - } - - guard let isFavorite = watchListViewModel.isFavorite(index: index) else { - return [] - } - - let type: RowActionType - let iconName: String - let action: (UITableViewCell?) -> Void - - if isFavorite { - type = .destructive - iconName = "star_off_24" - action = { _ in - watchListViewModel.unfavorite(index: index) - } - } else { - type = .additive - iconName = "star_24" - action = { _ in - watchListViewModel.favorite(index: index) - } - } - - return [ - RowAction( - pattern: .icon(image: UIImage(named: iconName)?.withTintColor(type.iconColor), background: type.backgroundColor), - action: action - ), - ] - } - - private func scrollToTop() { - tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .bottom, animated: true) - } - - func showAddedToWatchlist() { - HudHelper.instance.show(banner: .addedToWatchlist) - } - - func showRemovedFromWatchlist() { - HudHelper.instance.show(banner: .removedFromWatchlist) - } -} - -extension MarketListViewController: SectionsDataSource { - func buildSections() -> [SectionProtocol] { - let headerState: ViewState - - if let headerView, let viewItems, !viewItems.isEmpty { - headerState = .static(view: headerView, height: .heightSingleLineCell) - } else { - headerState = .margin(height: 0) - } - - return topSections(loaded: viewItems != nil) + [ - Section( - id: "coins", - headerState: headerState, - footerState: .marginColor(height: .margin32, color: .clear), - rows: viewItems.map { viewItems in - viewItems.enumerated().map { index, viewItem in - MarketModule.marketListCell( - tableView: tableView, - backgroundStyle: .transparent, - listViewItem: viewItem, - isFirst: false, - isLast: index == viewItems.count - 1, - rowActionProvider: { [weak self] in - self?.rowActions(index: index) ?? [] - }, - action: { [weak self] in - self?.onSelect(viewItem: viewItem) - } - ) - } - } ?? [] - ), - ] - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketList/MarketListViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketList/MarketListViewModel.swift deleted file mode 100644 index d579e67ab1..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketList/MarketListViewModel.swift +++ /dev/null @@ -1,116 +0,0 @@ -import Combine -import HsExtensions -import MarketKit -import RxCocoa -import RxRelay -import RxSwift - -protocol IMarketListService { - associatedtype Item - - var state: MarketListServiceState { get } - var statePublisher: AnyPublisher, Never> { get } - func refresh() -} - -protocol IMarketListCoinUidService { - func coinUid(index: Int) -> String? -} - -protocol IMarketListDecoratorService { - var initialIndex: Int { get } - var currency: Currency { get } - var priceChangeType: MarketModule.PriceChangeType { get } - func onUpdate(index: Int) -} - -protocol IMarketListDecorator { - associatedtype Item - - func listViewItem(item: Item) -> MarketModule.ListViewItem -} - -enum MarketListServiceState { - case loading - case loaded(items: [T], softUpdate: Bool, reorder: Bool) - case failed(error: Error) -} - -class MarketListViewModel { - private let service: Service - private let decorator: Decorator - private let itemLimit: Int? - private var cancellables = Set() - - private let viewItemDataRelay = BehaviorRelay(value: nil) - private let loadingRelay = BehaviorRelay(value: false) - private let syncErrorRelay = BehaviorRelay(value: false) - private let scrollToTopRelay = PublishRelay() - - init(service: Service, decorator: Decorator, itemLimit: Int? = nil) { - self.service = service - self.decorator = decorator - self.itemLimit = itemLimit - - service.statePublisher - .sink { [weak self] in self?.sync(state: $0) } - .store(in: &cancellables) - - sync(state: service.state) - } - - private func sync(state: MarketListServiceState) { - switch state { - case .loading: - viewItemDataRelay.accept(nil) - loadingRelay.accept(true) - syncErrorRelay.accept(false) - case let .loaded(items, softUpdate, reorder): - let limitedItems = itemLimit.map { Array(items.prefix(upTo: $0)) } ?? items - let data = MarketModule.ListViewItemData(viewItems: viewItems(items: Array(limitedItems)), softUpdate: softUpdate) - viewItemDataRelay.accept(data) - loadingRelay.accept(false) - syncErrorRelay.accept(false) - - if reorder { - scrollToTopRelay.accept(()) - } - case .failed: - viewItemDataRelay.accept(nil) - loadingRelay.accept(false) - syncErrorRelay.accept(true) - } - } - - private func viewItems(items: [Service.Item]) -> [MarketModule.ListViewItem] { - items.compactMap { item in - guard let item = item as? Decorator.Item else { - return nil - } - - return decorator.listViewItem(item: item) - } - } -} - -extension MarketListViewModel: IMarketListViewModel { - var viewItemDataDriver: Driver { - viewItemDataRelay.asDriver() - } - - var loadingDriver: Driver { - loadingRelay.asDriver() - } - - var syncErrorDriver: Driver { - syncErrorRelay.asDriver() - } - - var scrollToTopSignal: Signal { - scrollToTopRelay.asSignal() - } - - func refresh() { - service.refresh() - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketList/MarketListWatchViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketList/MarketListWatchViewModel.swift deleted file mode 100644 index 3e9c80407e..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketList/MarketListWatchViewModel.swift +++ /dev/null @@ -1,69 +0,0 @@ -import MarketKit -import RxCocoa -import RxRelay -import RxSwift - -protocol IMarketListWatchViewModel: IMarketListViewModel { - var favoriteDriver: Driver { get } - var unfavoriteDriver: Driver { get } - var failDriver: Driver { get } - - func isFavorite(index: Int) -> Bool? - func favorite(index: Int) - func unfavorite(index: Int) -} - -class MarketListWatchViewModel: MarketListViewModel { - private let disposeBag = DisposeBag() - - private let watchlistToggleService: MarketWatchlistToggleService - - private let favoriteRelay = PublishRelay() - private let unfavoriteRelay = PublishRelay() - private let failRelay = PublishRelay() - - init(service: Service, watchlistToggleService: MarketWatchlistToggleService, decorator: Decorator) { - self.watchlistToggleService = watchlistToggleService - - super.init(service: service, decorator: decorator) - - subscribe(disposeBag, watchlistToggleService.statusObservable) { [weak self] in self?.handle(status: $0) } - } - - private func handle(status: MarketWatchlistToggleService.State) { - switch status { - case .favorite: - favoriteRelay.accept(()) - case .unfavorite: - unfavoriteRelay.accept(()) - case .fail: - failRelay.accept("watch_coin.fail_to_find_uuid") - } - } -} - -extension MarketListWatchViewModel: IMarketListWatchViewModel { - var favoriteDriver: Driver { - favoriteRelay.asDriver(onErrorJustReturn: ()) - } - - var unfavoriteDriver: Driver { - unfavoriteRelay.asDriver(onErrorJustReturn: ()) - } - - var failDriver: Driver { - failRelay.asDriver(onErrorJustReturn: "") - } - - func isFavorite(index: Int) -> Bool? { - watchlistToggleService.isFavorite(index: index) - } - - func favorite(index: Int) { - watchlistToggleService.favorite(index: index) - } - - func unfavorite(index: Int) { - watchlistToggleService.unfavorite(index: index) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketList/MarketWatchlistToggleService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketList/MarketWatchlistToggleService.swift deleted file mode 100644 index ae81b8083f..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketList/MarketWatchlistToggleService.swift +++ /dev/null @@ -1,63 +0,0 @@ -import RxSwift - -class MarketWatchlistToggleService { - private let coinUidService: IMarketListCoinUidService - private let favoritesManager: FavoritesManager - private let statPage: StatPage - - private let statusSubject = PublishSubject() - - init(coinUidService: IMarketListCoinUidService, favoritesManager: FavoritesManager, statPage: StatPage) { - self.coinUidService = coinUidService - self.favoritesManager = favoritesManager - self.statPage = statPage - } -} - -extension MarketWatchlistToggleService { - var statusObservable: Observable { - statusSubject.asObservable() - } - - func isFavorite(index: Int) -> Bool? { - guard let coinUid = coinUidService.coinUid(index: index) else { - return nil - } - - return favoritesManager.isFavorite(coinUid: coinUid) - } - - func favorite(index: Int) { - guard let coinUid = coinUidService.coinUid(index: index) else { - statusSubject.onNext(.fail) - return - } - - favoritesManager.add(coinUid: coinUid) - - statusSubject.onNext(.favorite) - - stat(page: statPage, event: .addToWatchlist(coinUid: coinUid)) - } - - func unfavorite(index: Int) { - guard let coinUid = coinUidService.coinUid(index: index) else { - statusSubject.onNext(.fail) - return - } - - favoritesManager.remove(coinUid: coinUid) - - statusSubject.onNext(.unfavorite) - - stat(page: statPage, event: .removeFromWatchlist(coinUid: coinUid)) - } -} - -extension MarketWatchlistToggleService { - enum State { - case favorite - case unfavorite - case fail - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketModule.swift index 489383468e..80957ce408 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketModule.swift @@ -1,376 +1,241 @@ -import ComponentKit import Foundation -import Kingfisher import MarketKit -import SectionsTableView -import ThemeKit -import UIKit - -enum RowActionType { - case additive - case destructive - - var iconColor: UIColor { - switch self { - case .additive: return .themeDark - case .destructive: return .themeClaude - } - } - - var backgroundColor: UIColor { - switch self { - case .additive: return .themeYellowD - case .destructive: return .themeRedD - } - } -} enum MarketModule { - static func viewController() -> UIViewController { - MarketViewController() - } - - static func marketListCell(tableView: UITableView, backgroundStyle: BaseThemeCell.BackgroundStyle, listViewItem: MarketModule.ListViewItem, isFirst: Bool, isLast: Bool, rowActionProvider: (() -> [RowAction])?, action: (() -> Void)?) -> RowProtocol { - CellBuilderNew.row( - rootElement: .hStack([ - .image32 { component in - component.imageView.contentMode = .scaleAspectFill - component.imageView.clipsToBounds = true - component.imageView.cornerRadius = listViewItem.iconShape.radius - component.imageView.layer.cornerCurve = .continuous - component.imageView.kf.setImage( - with: listViewItem.iconUrl.flatMap { URL(string: $0) }, - placeholder: UIImage(named: listViewItem.iconPlaceholderName), - options: [.onlyLoadFirstFrame] - ) - }, - .vStackCentered([ - .hStack([ - .text { component in - component.font = .body - component.textColor = .themeLeah - component.text = listViewItem.leftPrimaryValue - }, - .text { component in - component.font = .body - component.textColor = .themeLeah - component.textAlignment = .right - component.setContentCompressionResistancePriority(.required, for: .horizontal) - component.text = listViewItem.rightPrimaryValue - }, - ]), - .margin(1), - .hStack([ - .badge { component in - if let badge = listViewItem.badge { - component.isHidden = false - component.badgeView.set(style: .small) - component.badgeView.text = badge - component.badgeView.change = listViewItem.badgeSecondaryValue - } else { - component.isHidden = true - } - }, - .margin4, - .text { component in - component.font = .subhead2 - component.textColor = .themeGray - component.text = listViewItem.leftSecondaryValue - }, - .text { component in - component.setContentCompressionResistancePriority(.required, for: .horizontal) - component.setContentHuggingPriority(.required, for: .horizontal) - component.textAlignment = .right - let marketFieldData = marketFieldPreference(dataValue: listViewItem.rightSecondaryValue) - component.font = .subhead2 - component.textColor = marketFieldData.color - component.text = marketFieldData.value - }, - ]), - ]), - ]), - tableView: tableView, - id: "\(listViewItem.uid ?? "")-\(listViewItem.leftPrimaryValue)", - height: .heightDoubleLineCell, - autoDeselect: true, - rowActionProvider: rowActionProvider, - bind: { cell in - cell.set(backgroundStyle: backgroundStyle, isFirst: isFirst, isLast: isLast) - }, - action: action - ) - } - - static func marketFieldPreference(dataValue: MarketDataValue) -> (title: String?, value: String?, color: UIColor) { - let title: String? - let value: String? - let color: UIColor - - switch dataValue { - case let .valueDiff(currencyValue, diff): - title = nil - - if let currencyValue, let diff { - let valueDiff = diff * currencyValue.value / 100 - value = ValueFormatter.instance.formatShort(currency: currencyValue.currency, value: valueDiff, showSign: true) ?? "----" - color = valueDiff.isSignMinus ? .themeLucian : .themeRemus - } else { - value = "----" - color = .themeGray50 - } - case let .diff(diff): - title = nil - value = diff.flatMap { ValueFormatter.instance.format(percentValue: $0) } ?? "----" - if let diff { - color = diff.isSignMinus ? .themeLucian : .themeRemus - } else { - color = .themeGray50 - } - case let .volume(volume): - title = "market.top.volume.title".localized - value = volume - color = .themeGray - case let .marketCap(marketCap): - title = "market.top.market_cap.title".localized - value = marketCap - color = .themeGray - } - - return (title: title, value: value, color: color) - } -} - -extension MarketModule { - enum Tab: String, CaseIterable { - case overview - case posts - case watchlist - - var title: String { - switch self { - case .overview: return "market.category.overview".localized - case .posts: return "market.category.posts".localized - case .watchlist: return "market.category.watchlist".localized - } - } - } - - enum SortingField: Int, CaseIterable { + enum SortBy: String, CaseIterable { case highestCap case lowestCap + case gainers + case losers case highestVolume case lowestVolume - case topGainers - case topLosers var title: String { switch self { - case .highestCap: return "market.top.highest_cap".localized - case .lowestCap: return "market.top.lowest_cap".localized - case .highestVolume: return "market.top.highest_volume".localized - case .lowestVolume: return "market.top.lowest_volume".localized - case .topGainers: return "market.top.top_gainers".localized - case .topLosers: return "market.top.top_losers".localized - } - } - - var raw: String { - switch self { - case .highestCap: return "highestCap" - case .lowestCap: return "lowestCap" - case .highestVolume: return "highestVolume" - case .lowestVolume: return "lowestVolume" - case .topGainers: return "topGainers" - case .topLosers: return "topLosers" + case .highestCap: return "market.sort_by.highest_cap".localized + case .lowestCap: return "market.sort_by.lowest_cap".localized + case .gainers: return "market.sort_by.gainers".localized + case .losers: return "market.sort_by.losers".localized + case .highestVolume: return "market.sort_by.highest_volume".localized + case .lowestVolume: return "market.sort_by.lowest_volume".localized } } } - enum MarketField: Int, CaseIterable { - case price - case marketCap - case volume + enum SortOrder { + case asc + case desc - var title: String { + mutating func toggle() { switch self { - case .price: return "price".localized - case .marketCap: return "market.market_field.mcap".localized - case .volume: return "market.market_field.vol".localized + case .asc: self = .desc + case .desc: self = .asc } } - var raw: String { - switch self { - case .price: return "price" - case .marketCap: return "marketCap" - case .volume: return "volume" - } - } + var isAsc: Bool { self == .asc } } - enum MarketTop: Int, CaseIterable { + enum Top: Int, CaseIterable, Identifiable { case top100 = 100 case top200 = 200 + case top250 = 250 case top300 = 300 + case top500 = 500 + case top1000 = 1000 + case top1500 = 1500 var title: String { - "\(rawValue)" + "market.top_coins".localized("\(rawValue)") } - } - enum PriceChangeType: Int, CaseIterable { - static let sortingTypes: [Self] = [.day, .week, .month] + var id: Int { + rawValue + } + } - case day - case week - case week2 - case month - case month6 - case year + enum Tab: String, CaseIterable { + case coins + case watchlist + case news + case platforms + case pairs + // case sectors var title: String { switch self { - case .day: return "market.advanced_search.day".localized - case .week: return "market.advanced_search.week".localized - case .week2: return "market.advanced_search.week2".localized - case .month: return "market.advanced_search.month".localized - case .month6: return "market.advanced_search.month6".localized - case .year: return "market.advanced_search.year".localized + case .coins: return "market.tab.coins".localized + case .watchlist: return "market.tab.watchlist".localized + case .news: return "market.tab.news".localized + case .platforms: return "market.tab.platforms".localized + case .pairs: return "market.tab.pairs".localized + // case .sectors: return "market.tab.sectors".localized } } + } +} - var shortTitle: String { - switch self { - case .week: return "market.advanced_search.week.short".localized - case .month: return "market.advanced_search.month.short".localized - default: return "market.advanced_search.day.short".localized - } +extension MarketKit.MarketInfo { + func priceChangeValue(timePeriod: HsTimePeriod) -> Decimal? { + switch timePeriod { + case .hour24: return priceChange24h + case .day1: return priceChange1d + case .week1: return priceChange7d + case .week2: return priceChange14d + case .month1: return priceChange30d + case .month3: return priceChange90d + case .month6: return priceChange200d + case .year1: return priceChange1y + default: return nil } } - enum MarketTvlField: Int, CaseIterable { - case diff - case value + func priceChangeValue(timePeriod: WatchlistTimePeriod) -> Decimal? { + switch timePeriod { + case .hour24: return priceChange24h + case .day1: return priceChange1d + case .week1: return priceChange7d + case .month1: return priceChange30d + case .month3: return priceChange90d + } + } +} - var title: String { - switch self { - case .value: return "market.tvl.market_field.value".localized - case .diff: return "market.tvl.market_field.diff".localized - } +extension MarketKit.DefiCoin { + func tvlChangeValue(timePeriod: HsTimePeriod) -> Decimal? { + switch timePeriod { + case .day1: return tvlChange1d + case .week1: return tvlChange1w + case .week2: return tvlChange2w + case .month1: return tvlChange1m + case .month3: return tvlChange6m + case .month6: return tvlChange6m + case .year1: return tvlChange1y + default: return nil } } +} - enum MarketPlatformField: Int, CaseIterable { - case all - case ethereum - case solana - case binance - case avalanche - case terra - case fantom - case arbitrum - case polygon +extension [MarketKit.MarketInfo] { + func sorted(sortBy: WatchlistSortBy, timePeriod: WatchlistTimePeriod) -> [MarketKit.MarketInfo] { + switch sortBy { + case .manual: return self + case .highestCap: return sorted { $0.marketCap ?? 0 > $1.marketCap ?? 0 } + case .lowestCap: return sorted { $0.marketCap ?? 0 < $1.marketCap ?? 0 } + case .gainers, .losers: return sorted { + guard let lhsPriceChange = $0.priceChangeValue(timePeriod: timePeriod) else { + return false + } + guard let rhsPriceChange = $1.priceChangeValue(timePeriod: timePeriod) else { + return true + } - var chain: String { - switch self { - case .all: return "" - case .ethereum: return "Ethereum" - case .solana: return "Solana" - case .binance: return "Binance" - case .avalanche: return "Avalanche" - case .terra: return "Terra" - case .fantom: return "Fantom" - case .arbitrum: return "Arbitrum" - case .polygon: return "Polygon" + return sortBy == .gainers ? lhsPriceChange > rhsPriceChange : lhsPriceChange < rhsPriceChange } } + } - var title: String { - switch self { - case .all: return "market.tvl.platform_field.all".localized - default: return chain + func sorted(sortBy: MarketModule.SortBy, timePeriod: HsTimePeriod) -> [MarketKit.MarketInfo] { + switch sortBy { + case .highestCap: return sorted { $0.marketCap ?? 0 > $1.marketCap ?? 0 } + case .lowestCap: return sorted { $0.marketCap ?? 0 < $1.marketCap ?? 0 } + case .highestVolume: return sorted { $0.totalVolume ?? 0 > $1.totalVolume ?? 0 } + case .lowestVolume: return sorted { $0.totalVolume ?? 0 < $1.totalVolume ?? 0 } + case .gainers, .losers: return sorted { + guard let lhsPriceChange = $0.priceChangeValue(timePeriod: timePeriod) else { + return false + } + guard let rhsPriceChange = $1.priceChangeValue(timePeriod: timePeriod) else { + return true + } + + return sortBy == .gainers ? lhsPriceChange > rhsPriceChange : lhsPriceChange < rhsPriceChange } } } } -extension MarketKit.MarketInfo { - func priceChangeValue(type: MarketModule.PriceChangeType) -> Decimal? { - switch type { - case .day: return priceChange24h - case .week: return priceChange7d - case .week2: return priceChange14d - case .month: return priceChange30d - case .month6: return priceChange200d - case .year: return priceChange1y - } - } -} +extension [MarketKit.TopPlatform] { + func sorted(sortBy: MarketModule.SortBy, timePeriod: HsTimePeriod) -> [TopPlatform] { + sorted { lhsPlatform, rhsPlatform in + let lhsCap = lhsPlatform.marketCap + let rhsCap = rhsPlatform.marketCap -extension [MarketKit.MarketInfo] { - func sorted(sortingField: MarketModule.SortingField, priceChangeType: MarketModule.PriceChangeType) -> [MarketKit.MarketInfo] { - sorted { lhsMarketInfo, rhsMarketInfo in - switch sortingField { - case .highestCap: return lhsMarketInfo.marketCap ?? 0 > rhsMarketInfo.marketCap ?? 0 - case .lowestCap: return lhsMarketInfo.marketCap ?? 0 < rhsMarketInfo.marketCap ?? 0 - case .highestVolume: return lhsMarketInfo.totalVolume ?? 0 > rhsMarketInfo.totalVolume ?? 0 - case .lowestVolume: return lhsMarketInfo.totalVolume ?? 0 < rhsMarketInfo.totalVolume ?? 0 - case .topGainers, .topLosers: - guard let rhsPriceChange = rhsMarketInfo.priceChangeValue(type: priceChangeType) else { + let lhsChange = lhsPlatform.changes[timePeriod] + let rhsChange = rhsPlatform.changes[timePeriod] + + switch sortBy { + case .highestCap, .lowestCap: + guard let lhsCap else { return true } - guard let lhsPriceChange = lhsMarketInfo.priceChangeValue(type: priceChangeType) else { + guard let rhsCap else { return false } - return sortingField == .topGainers ? lhsPriceChange > rhsPriceChange : lhsPriceChange < rhsPriceChange + return sortBy == .highestCap ? lhsCap > rhsCap : lhsCap < rhsCap + case .gainers, .losers: + guard let lhsChange else { + return false + } + guard let rhsChange else { + return true + } + + return sortBy == .gainers ? lhsChange > rhsChange : lhsChange < rhsChange + default: return true } } } } -extension MarketModule { // ViewModel Items - enum MarketDataValue { - case valueDiff(CurrencyValue?, Decimal?) - case diff(Decimal?) - case volume(String) - case marketCap(String) +extension HsTimePeriod { + var title: String { + switch self { + case .hour24: return "market.time_period.1d".localized + default: return "market.time_period.\(rawValue)".localized + } } - struct ListViewItem { - let uid: String? - let iconUrl: String? - let iconShape: IconShape - let iconPlaceholderName: String - let leftPrimaryValue: String - let leftSecondaryValue: String - let badge: String? - let badgeSecondaryValue: BadgeView.Change? - let rightPrimaryValue: String - let rightSecondaryValue: MarketDataValue + var shortTitle: String { + switch self { + case .hour24: return "market.time_period.1d.short".localized + default: return "market.time_period.\(rawValue).short".localized + } } - struct ListViewItemData { - let viewItems: [ListViewItem] - let softUpdate: Bool - let scrollToTop: Bool + init?(_ periodType: HsPeriodType) { + guard case let .byPeriod(timePeriod) = periodType else { + return nil + } + self = timePeriod + } +} - init(viewItems: [ListViewItem], softUpdate: Bool = false, scrollToTop: Bool = false) { - self.viewItems = viewItems - self.softUpdate = softUpdate - self.scrollToTop = scrollToTop +extension WatchlistTimePeriod { + var title: String { + switch self { + case .hour24: return "market.time_period.1d".localized + default: return "market.time_period.\(rawValue)".localized } } - enum IconShape { - case square, round, full + var shortTitle: String { + switch self { + case .hour24: return "market.time_period.1d.short".localized + default: return "market.time_period.\(rawValue).short".localized + } + } +} - var radius: CGFloat { - switch self { - case .square: return .cornerRadius8 - case .round: return .cornerRadius16 - case .full: return 0 - } +extension WatchlistSortBy { + var title: String { + switch self { + case .manual: return "market.sort_by.manual".localized + case .highestCap: return "market.sort_by.highest_cap".localized + case .lowestCap: return "market.sort_by.lowest_cap".localized + case .gainers: return "market.sort_by.gainers".localized + case .losers: return "market.sort_by.losers".localized } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketNftTopCollections/MarketListNftCollectionDecorator.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketNftTopCollections/MarketListNftCollectionDecorator.swift deleted file mode 100644 index a1f401883c..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketNftTopCollections/MarketListNftCollectionDecorator.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Foundation -import MarketKit - -protocol IMarketListNftTopCollectionDecoratorService { - var timePeriod: HsTimePeriod { get } -} - -class MarketListNftCollectionDecorator { - typealias Item = NftCollectionItem - - private let service: IMarketListNftTopCollectionDecoratorService - - init(service: IMarketListNftTopCollectionDecoratorService) { - self.service = service - } -} - -extension MarketListNftCollectionDecorator: IMarketListDecorator { - func listViewItem(item: NftCollectionItem) -> MarketModule.ListViewItem { - let collection = item.collection - - var floorPriceString = "---" - - if let floorPrice = collection.floorPrice { - let coinValue = CoinValue(kind: .token(token: floorPrice.token), value: floorPrice.value) - if let value = ValueFormatter.instance.formatShort(coinValue: coinValue) { - floorPriceString = "market.top.floor_price".localized + " " + value - } - } - - var volumeString = "n/a".localized - let volume = collection.volumes[service.timePeriod] - let diff = collection.changes[service.timePeriod] - - if let volume, let value = ValueFormatter.instance.formatShort(coinValue: CoinValue(kind: .token(token: volume.token), value: volume.value)) { - volumeString = value - } - let dataValue: MarketModule.MarketDataValue = .diff(diff) - - return MarketModule.ListViewItem( - uid: collection.uid, - iconUrl: collection.thumbnailImageUrl ?? "", - iconShape: .square, - iconPlaceholderName: "placeholder_rectangle_32", - leftPrimaryValue: collection.name, - leftSecondaryValue: floorPriceString, - badge: "\(item.index)", - badgeSecondaryValue: nil, - rightPrimaryValue: volumeString, - rightSecondaryValue: dataValue - ) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketNftTopCollections/MarketNftTopCollectionsModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketNftTopCollections/MarketNftTopCollectionsModule.swift deleted file mode 100644 index 370119733d..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketNftTopCollections/MarketNftTopCollectionsModule.swift +++ /dev/null @@ -1,79 +0,0 @@ -import MarketKit -import ThemeKit -import UIKit - -enum MarketNftTopCollectionsModule { - static func viewController(timePeriod: HsTimePeriod) -> UIViewController { - let service = MarketNftTopCollectionsService(marketKit: App.shared.marketKit, currencyManager: App.shared.currencyManager, timePeriod: timePeriod) - - let decorator = MarketListNftCollectionDecorator(service: service) - let viewModel = MarketNftTopCollectionsViewModel(service: service) - let listViewModel = MarketListViewModel(service: service, decorator: decorator, itemLimit: 100) - let headerViewModel = NftCollectionsMultiSortHeaderViewModel(service: service, decorator: decorator) - - let viewController = MarketNftTopCollectionsViewController(viewModel: viewModel, listViewModel: listViewModel, headerViewModel: headerViewModel) - - return ThemeNavigationController(rootViewController: viewController) - } - - enum SortType: Int, CaseIterable { - case highestVolume - case lowestVolume - case topGainers - case topLosers - - var title: String { - switch self { - case .highestVolume: return "market.top.highest_volume".localized - case .lowestVolume: return "market.top.lowest_volume".localized - case .topGainers: return "market.top.top_gainers".localized - case .topLosers: return "market.top.top_losers".localized - } - } - } - - static var selectorValues: [HsTimePeriod] { - [HsTimePeriod.day1, - HsTimePeriod.week1, - HsTimePeriod.month1] - } -} - -extension NftTopCollection { - var uid: String { - "\(blockchainType.uid)-\(providerUid)" - } -} - -extension [NftTopCollection] { - func sorted(sortType: MarketNftTopCollectionsModule.SortType, timePeriod: HsTimePeriod) -> [NftTopCollection] { - sorted { lhsCollection, rhsCollection in - let lhsVolume = lhsCollection.volumes[timePeriod]?.value - let rhsVolume = rhsCollection.volumes[timePeriod]?.value - - let lhsChange = lhsCollection.changes[timePeriod] - let rhsChange = rhsCollection.changes[timePeriod] - - switch sortType { - case .highestVolume, .lowestVolume: - guard let lhsVolume else { - return true - } - guard let rhsVolume else { - return false - } - - return sortType == .highestVolume ? lhsVolume > rhsVolume : lhsVolume < rhsVolume - case .topGainers, .topLosers: - guard let lhsChange else { - return true - } - guard let rhsChange else { - return false - } - - return sortType == .topGainers ? lhsChange > rhsChange : lhsChange < rhsChange - } - } - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketNftTopCollections/MarketNftTopCollectionsService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketNftTopCollections/MarketNftTopCollectionsService.swift deleted file mode 100644 index 9c886791ff..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketNftTopCollections/MarketNftTopCollectionsService.swift +++ /dev/null @@ -1,87 +0,0 @@ -import Combine -import HsExtensions -import MarketKit -import RxRelay -import RxSwift - -struct NftCollectionItem { - let index: Int - let collection: NftTopCollection -} - -class MarketNftTopCollectionsService { - typealias Item = NftCollectionItem - - private let disposeBag = DisposeBag() - private var tasks = Set() - - private let marketKit: MarketKit.Kit - private let currencyManager: CurrencyManager - - private var internalState: MarketListServiceState = .loading - - @PostPublished private(set) var state: MarketListServiceState = .loading - - var sortType: MarketNftTopCollectionsModule.SortType = .highestVolume { didSet { syncIfPossible() } } - var timePeriod: HsTimePeriod { didSet { syncIfPossible() } } - - init(marketKit: MarketKit.Kit, currencyManager: CurrencyManager, timePeriod: HsTimePeriod) { - self.marketKit = marketKit - self.currencyManager = currencyManager - self.timePeriod = timePeriod - - sync() - } - - private func sync() { - tasks = Set() - - if case .failed = state { - state = .loading - } - - Task { [weak self, marketKit] in - do { - let collections = try await marketKit.nftTopCollections() - self?.internalState = .loaded(items: collections, softUpdate: false, reorder: false) - self?.sync(collections: collections) - } catch { - self?.state = .failed(error: error) - } - }.store(in: &tasks) - } - - private func sync(collections: [NftTopCollection], reorder: Bool = false) { - let sortedCollections = collections.sorted(sortType: sortType, timePeriod: timePeriod) - let items = sortedCollections.enumerated().map { NftCollectionItem(index: $0 + 1, collection: $1) } - state = .loaded(items: items, softUpdate: false, reorder: reorder) - } - - private func syncIfPossible() { - guard case let .loaded(collections, _, _) = internalState else { - return - } - - sync(collections: collections, reorder: true) - } -} - -extension MarketNftTopCollectionsService: IMarketListService { - var statePublisher: AnyPublisher, Never> { - $state - } - - func topCollection(uid: String) -> NftTopCollection? { - guard case let .loaded(collections, _, _) = internalState else { - return nil - } - - return collections.first { $0.uid == uid } - } - - func refresh() { - sync() - } -} - -extension MarketNftTopCollectionsService: IMarketListNftTopCollectionDecoratorService {} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketNftTopCollections/MarketNftTopCollectionsViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketNftTopCollections/MarketNftTopCollectionsViewController.swift deleted file mode 100644 index 3860c9eeba..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketNftTopCollections/MarketNftTopCollectionsViewController.swift +++ /dev/null @@ -1,71 +0,0 @@ -import SectionsTableView -import SnapKit -import ThemeKit -import UIKit - -class MarketNftTopCollectionsViewController: MarketListViewController { - private let viewModel: MarketNftTopCollectionsViewModel - private let multiSortHeaderView: MarketMultiSortHeaderView - - override var viewController: UIViewController? { self } - override var headerView: UITableViewHeaderFooterView? { multiSortHeaderView } - override var refreshEnabled: Bool { false } - - init(viewModel: MarketNftTopCollectionsViewModel, listViewModel: IMarketListViewModel, headerViewModel: NftCollectionsMultiSortHeaderViewModel) { - self.viewModel = viewModel - multiSortHeaderView = MarketMultiSortHeaderView(viewModel: headerViewModel) - - super.init(listViewModel: listViewModel, statPage: .topNftCollections) - - multiSortHeaderView.viewController = self - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - navigationItem.largeTitleDisplayMode = .never - navigationItem.rightBarButtonItem = UIBarButtonItem(title: "button.close".localized, style: .plain, target: self, action: #selector(onTapClose)) - - tableView.registerCell(forClass: MarketHeaderCell.self) - } - - @objc private func onTapClose() { - dismiss(animated: true) - } - - override func topSections(loaded _: Bool) -> [SectionProtocol] { - [ - Section( - id: "header", - rows: [ - Row( - id: "header", - height: MarketHeaderCell.height, - bind: { cell, _ in - cell.set( - title: "top_nft_collections.title".localized, - description: "top_nft_collections.description".localized, - imageMode: .remote(imageUrl: "nft".headerImageUrl) - ) - } - ), - ] - ), - ] - } - - override func onSelect(viewItem: MarketModule.ListViewItem) { - guard let uid = viewItem.uid, let topCollection = viewModel.topCollection(uid: uid) else { - return - } - - if let module = NftCollectionModule.viewController(blockchainType: topCollection.blockchainType, providerCollectionUid: topCollection.providerUid) { - present(ThemeNavigationController(rootViewController: module), animated: true) - } - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketNftTopCollections/MarketNftTopCollectionsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketNftTopCollections/MarketNftTopCollectionsViewModel.swift deleted file mode 100644 index a4c47bf378..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketNftTopCollections/MarketNftTopCollectionsViewModel.swift +++ /dev/null @@ -1,13 +0,0 @@ -import MarketKit - -class MarketNftTopCollectionsViewModel { - private let service: MarketNftTopCollectionsService - - init(service: MarketNftTopCollectionsService) { - self.service = service - } - - func topCollection(uid: String) -> NftTopCollection? { - service.topCollection(uid: uid) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketNftTopCollections/NftCollectionsMultiSortHeaderViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketNftTopCollections/NftCollectionsMultiSortHeaderViewModel.swift deleted file mode 100644 index 54f71c9e6a..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketNftTopCollections/NftCollectionsMultiSortHeaderViewModel.swift +++ /dev/null @@ -1,47 +0,0 @@ -import MarketKit - -class NftCollectionsMultiSortHeaderViewModel { - private let service: MarketNftTopCollectionsService - private let decorator: MarketListNftCollectionDecorator - - init(service: MarketNftTopCollectionsService, decorator: MarketListNftCollectionDecorator) { - self.service = service - self.decorator = decorator - } -} - -extension NftCollectionsMultiSortHeaderViewModel: IMarketMultiSortHeaderViewModel { - var sortItems: [String] { - MarketNftTopCollectionsModule.SortType.allCases.map(\.title) - } - - var sortIndex: Int { - MarketNftTopCollectionsModule.SortType.allCases.firstIndex(of: service.sortType) ?? 0 - } - - var leftSelectorItems: [String] { - [] - } - - var leftSelectorIndex: Int { - 0 - } - - var rightSelectorItems: [String] { - MarketNftTopCollectionsModule.selectorValues.map(\.title) - } - - var rightSelectorIndex: Int { - MarketNftTopCollectionsModule.selectorValues.firstIndex(of: service.timePeriod) ?? 0 - } - - func onSelectSort(index: Int) { - service.sortType = MarketNftTopCollectionsModule.SortType.allCases[index] - } - - func onSelectLeft(index _: Int) {} - - func onSelectRight(index: Int) { - service.timePeriod = MarketNftTopCollectionsModule.selectorValues[index] - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/BaseMarketOverviewTopListDataSource.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/BaseMarketOverviewTopListDataSource.swift deleted file mode 100644 index 176df3590a..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/BaseMarketOverviewTopListDataSource.swift +++ /dev/null @@ -1,133 +0,0 @@ -import Chart -import ComponentKit -import RxCocoa -import RxSwift -import SectionsTableView -import UIKit - -protocol IBaseMarketOverviewTopListViewModel { - var listViewItemsDriver: Driver<[MarketModule.ListViewItem]?> { get } - var selectorTitles: [String] { get } - var selectorIndex: Int { get } - func onSelect(selectorIndex: Int) -} - -class BaseMarketOverviewTopListDataSource { - private let topListViewModel: IBaseMarketOverviewTopListViewModel - weak var presentDelegate: IPresentDelegate? - private let rightSelectorMode: MarketOverviewHeaderCell.ButtonMode - private let imageName: String - private let title: String - private let disposeBag = DisposeBag() - - private let listViewItemsRelay = BehaviorRelay<[MarketModule.ListViewItem]?>(value: nil) - - init(topListViewModel: IBaseMarketOverviewTopListViewModel, presentDelegate: IPresentDelegate, rightSelectorMode: MarketOverviewHeaderCell.ButtonMode, imageName: String, title: String) { - self.topListViewModel = topListViewModel - self.presentDelegate = presentDelegate - self.rightSelectorMode = rightSelectorMode - self.imageName = imageName - self.title = title - - subscribe(disposeBag, topListViewModel.listViewItemsDriver) { [weak self] listViewItems in - self?.listViewItemsRelay.accept(listViewItems) - } - } - - private func rows(tableView: SectionsTableView, listViewItems: [MarketModule.ListViewItem]) -> [RowProtocol] { - listViewItems.enumerated().map { index, listViewItem in - MarketModule.marketListCell( - tableView: tableView, - backgroundStyle: .lawrence, - listViewItem: listViewItem, - isFirst: index == 0, - isLast: false, - rowActionProvider: nil, - action: { [weak self] in - self?.onSelect(listViewItem: listViewItem) - } - ) - } - } - - private func seeAllRow(tableView: SectionsTableView, id: String, action: @escaping () -> Void) -> RowProtocol { - tableView.universalRow48( - id: id, - title: .body("market.top.section.header.see_all".localized), - accessoryType: .disclosure, - autoDeselect: true, - isLast: true, - action: action - ) - } - - func didTapSeeAll() {} - - func onSelect(listViewItem _: MarketModule.ListViewItem) {} -} - -extension BaseMarketOverviewTopListDataSource: IMarketOverviewDataSource { - var isReady: Bool { - listViewItemsRelay.value != nil - } - - var updateObservable: Observable { - listViewItemsRelay.map { _ in () } - } - - private func bind(cell: MarketOverviewHeaderCell) { - cell.set(backgroundStyle: .transparent) - - cell.buttonMode = rightSelectorMode - cell.set(values: topListViewModel.selectorTitles) - cell.setSelected(index: topListViewModel.selectorIndex) - cell.onSelect = { [weak self] index in - self?.topListViewModel.onSelect(selectorIndex: index) - } - cell.onTapTitle = { [weak self] in self?.didTapSeeAll() } - - cell.titleImage = UIImage(named: imageName) - cell.title = title - } - - func sections(tableView: SectionsTableView) -> [SectionProtocol] { - guard let listViewItems = listViewItemsRelay.value else { - return [] - } - - var sections = [SectionProtocol]() - - let headerSection = Section( - id: "header_\(title)", - footerState: .margin(height: .margin8), - rows: [ - Row( - id: "header_\(title)", - height: .heightCell48, - bind: { [weak self] cell, _ in - self?.bind(cell: cell) - } - ), - ] - ) - - let listSection = Section( - id: title, - footerState: .margin(height: .margin24), - rows: rows(tableView: tableView, listViewItems: listViewItems) + [ - seeAllRow( - tableView: tableView, - id: "\(title)-see-all", - action: { [weak self] in - self?.didTapSeeAll() - } - ), - ] - ) - - sections.append(headerSection) - sections.append(listSection) - - return sections - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/Category/MarketOverviewCategoryCell.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/Category/MarketOverviewCategoryCell.swift deleted file mode 100644 index 79a892a069..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/Category/MarketOverviewCategoryCell.swift +++ /dev/null @@ -1,100 +0,0 @@ -import MarketKit -import SnapKit -import UIKit - -class MarketOverviewCategoryCell: UITableViewCell { - static let cellHeight: CGFloat = MarketCategoryView.height + 2 * .margin16 - - var onSelect: ((String) -> Void)? - - var viewItems = [MarketOverviewCategoryViewModel.ViewItem]() { - didSet { - build() - } - } - - private let scrollView = UIScrollView() - private let stackView = UIStackView() - private let leadingView = UIView() - private let trailingView = UIView() - - override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - backgroundColor = .clear - selectionStyle = .none - - contentView.addSubview(scrollView) - scrollView.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview().inset(CGFloat.margin12) - make.top.bottom.equalToSuperview().inset(CGFloat.margin16) - } - - scrollView.isPagingEnabled = true - scrollView.clipsToBounds = false - scrollView.showsHorizontalScrollIndicator = false - - scrollView.addSubview(stackView) - stackView.snp.makeConstraints { make in - make.leading.trailing.equalTo(scrollView).inset(-CGFloat.margin4) - make.top.bottom.equalTo(scrollView) - make.height.equalTo(scrollView) - } - - stackView.spacing = .margin8 - - leadingView.snp.makeConstraints { make in - make.width.equalTo(0) - } - trailingView.snp.makeConstraints { make in - make.width.equalTo(0) - } - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func build() { - for view in stackView.arrangedSubviews { - stackView.removeArrangedSubview(view) - view.removeFromSuperview() - } - - stackView.addArrangedSubview(leadingView) - - var bufferView: UIView? - - for (index, viewItem) in viewItems.enumerated() { - let view = MarketCategoryView() - view.set(viewItem: viewItem) - view.onTap = { [weak self] in - self?.onSelect?(viewItem.uid) - } - - if let _bufferView = bufferView { - addPage(views: [_bufferView, view]) - bufferView = nil - } else if index == viewItems.count - 1 { - addPage(views: [view, UIView()]) - } else { - bufferView = view - } - } - - stackView.addArrangedSubview(trailingView) - } - - private func addPage(views: [UIView]) { - let stackView = UIStackView(arrangedSubviews: views) - stackView.spacing = .margin8 - stackView.distribution = .fillEqually - - self.stackView.addArrangedSubview(stackView) - - stackView.snp.makeConstraints { make in - make.width.equalTo(scrollView).offset(-CGFloat.margin8) - } - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/Category/MarketOverviewCategoryDataSource.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/Category/MarketOverviewCategoryDataSource.swift deleted file mode 100644 index ceebf367ac..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/Category/MarketOverviewCategoryDataSource.swift +++ /dev/null @@ -1,84 +0,0 @@ -import RxCocoa -import RxSwift -import SectionsTableView -import UIKit - -class MarketOverviewCategoryDataSource { - private let viewModel: MarketOverviewCategoryViewModel - weak var presentDelegate: IPresentDelegate? - private let disposeBag = DisposeBag() - - private let viewItemsRelay = BehaviorRelay<[MarketOverviewCategoryViewModel.ViewItem]?>(value: nil) - - private let categoryCell = MarketOverviewCategoryCell() - - init(viewModel: MarketOverviewCategoryViewModel, presentDelegate: IPresentDelegate) { - self.viewModel = viewModel - self.presentDelegate = presentDelegate - - subscribe(disposeBag, viewModel.viewItemsDriver) { [weak self] viewItems in - self?.viewItemsRelay.accept(viewItems) - - if let viewItems { - self?.categoryCell.viewItems = viewItems - } - } - - categoryCell.onSelect = { [weak self] uid in - guard let category = viewModel.category(uid: uid) else { - return - } - - let viewController = MarketCategoryModule.viewController(category: category) - self?.presentDelegate?.present(viewController: viewController) - - stat(page: .marketOverview, event: .openCategory(categoryUid: uid)) - } - } - - private func bind(cell: MarketOverviewHeaderCell) { - cell.set(backgroundStyle: .transparent) - cell.buttonMode = .none - cell.titleImage = UIImage(named: "categories_20") - cell.title = "market.top.section.header.sectors".localized - cell.onTapTitle = nil - } -} - -extension MarketOverviewCategoryDataSource: IMarketOverviewDataSource { - var isReady: Bool { - viewItemsRelay.value != nil - } - - var updateObservable: Observable { - viewItemsRelay.map { _ in () } - } - - func sections(tableView _: SectionsTableView) -> [SectionProtocol] { - [ - Section( - id: "categories_header", - rows: [ - Row( - id: "categories_header_cell", - height: .heightCell48, - bind: { [weak self] cell, _ in - self?.bind(cell: cell) - } - ), - ] - ), - Section( - id: "categories", - footerState: .margin(height: .margin32), - rows: [ - StaticRow( - cell: categoryCell, - id: "categories", - height: MarketOverviewCategoryCell.cellHeight - ), - ] - ), - ] - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/Category/MarketOverviewCategoryService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/Category/MarketOverviewCategoryService.swift deleted file mode 100644 index b5c66a5e2a..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/Category/MarketOverviewCategoryService.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Combine -import Foundation -import MarketKit -import RxRelay -import RxSwift - -class MarketOverviewCategoryService { - private let baseService: MarketOverviewService - private var cancellables = Set() - - let marketTop: MarketModule.MarketTop = .top100 - let listType: MarketOverviewTopCoinsService.ListType - - private let categoriesRelay = PublishRelay<[CoinCategory]?>() - private(set) var categories: [CoinCategory]? { - didSet { - categoriesRelay.accept(categories) - } - } - - init(listType: MarketOverviewTopCoinsService.ListType, baseService: MarketOverviewService) { - self.listType = listType - self.baseService = baseService - - baseService.$state - .sink { [weak self] in self?.sync(state: $0) } - .store(in: &cancellables) - - sync() - } - - private func sync(state: DataStatus? = nil) { - let state = state ?? baseService.state - - categories = state.data.map { item in - item.marketOverview.coinCategories - } - } -} - -extension MarketOverviewCategoryService { - var categoriesObservable: Observable<[CoinCategory]?> { - categoriesRelay.asObservable() - } - - var currency: Currency { - baseService.currency - } - - func category(uid: String) -> CoinCategory? { - categories?.first { $0.uid == uid } - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/Category/MarketOverviewCategoryViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/Category/MarketOverviewCategoryViewModel.swift deleted file mode 100644 index d7d81ce344..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/Category/MarketOverviewCategoryViewModel.swift +++ /dev/null @@ -1,85 +0,0 @@ -import MarketKit -import RxCocoa -import RxRelay -import RxSwift -import UIKit - -class MarketOverviewCategoryViewModel { - private let service: MarketOverviewCategoryService - private let disposeBag = DisposeBag() - - private let viewItemsRelay = BehaviorRelay<[ViewItem]?>(value: nil) - - init(service: MarketOverviewCategoryService) { - self.service = service - - subscribe(disposeBag, service.categoriesObservable) { [weak self] in self?.sync(categories: $0) } - - sync(categories: service.categories) - } - - private func sync(categories: [CoinCategory]?) { - viewItemsRelay.accept(categories.map { $0.map { viewItem(category: $0) } }) - } - - private func viewItem(category: CoinCategory) -> ViewItem { - var marketCap: String? - if let amount = category.marketCap { - marketCap = ValueFormatter.instance.formatShort(currency: service.currency, value: amount) - } else { - marketCap = "----" - } - - let diff = category.diff(timePeriod: .day1) - let diffString: String? = diff.flatMap { - ValueFormatter.instance.format(percentValue: $0) - } - - let diffType: DiffType = (diff?.isSignMinus ?? true) ? .down : .up - - return ViewItem( - uid: category.uid, - imageUrl: category.imageUrl, - name: category.name, - marketCap: marketCap, - diff: diffString, - diffType: diffType - ) - } -} - -extension MarketOverviewCategoryViewModel { - var viewItemsDriver: Driver<[ViewItem]?> { - viewItemsRelay.asDriver() - } - - func category(uid: String) -> CoinCategory? { - service.category(uid: uid) - } - - var marketTop: MarketModule.MarketTop { service.marketTop } - var listType: MarketOverviewTopCoinsService.ListType { service.listType } -} - -extension MarketOverviewCategoryViewModel { - struct ViewItem { - let uid: String - let imageUrl: String - let name: String - let marketCap: String? - let diff: String? - let diffType: DiffType - } - - enum DiffType { - case down - case up - - var textColor: UIColor { - switch self { - case .up: return .themeRemus - case .down: return .themeLucian - } - } - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/GlobalMarket/MarketOverviewGlobalDataSource.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/GlobalMarket/MarketOverviewGlobalDataSource.swift deleted file mode 100644 index 8116c07e43..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/GlobalMarket/MarketOverviewGlobalDataSource.swift +++ /dev/null @@ -1,60 +0,0 @@ -import Chart -import ComponentKit -import RxCocoa -import RxSwift -import SectionsTableView -import UIKit - -class MarketOverviewGlobalDataSource { - private let viewModel: MarketOverviewGlobalViewModel - weak var presentDelegate: IPresentDelegate? - private let disposeBag = DisposeBag() - - private let marketMetricsCell: MarketOverviewMetricsCell - private let marketMetricsRow: StaticRow - - private let viewItemRelay = BehaviorRelay(value: nil) - - init(viewModel: MarketOverviewGlobalViewModel, presentDelegate: IPresentDelegate) { - self.viewModel = viewModel - self.presentDelegate = presentDelegate - - marketMetricsCell = MarketOverviewMetricsCell(chartConfiguration: ChartConfiguration.smallPreviewChart, presentDelegate: presentDelegate) - marketMetricsRow = StaticRow( - cell: marketMetricsCell, - id: "metrics", - height: MarketOverviewMetricsCell.cellHeight - ) - - subscribe(disposeBag, viewModel.viewItemDriver) { [weak self] viewItem in - self?.viewItemRelay.accept(viewItem) - } - } -} - -extension MarketOverviewGlobalDataSource: IMarketOverviewDataSource { - var isReady: Bool { - viewItemRelay.value != nil - } - - var updateObservable: Observable { - viewItemRelay.map { _ in () } - } - - func sections(tableView _: SectionsTableView) -> [SectionProtocol] { - guard let viewItem = viewItemRelay.value else { - return [] - } - - marketMetricsRow.onReady = { [weak self] in - self?.marketMetricsCell.set(viewItem: viewItem) - } - - return [ - Section( - id: "market_metrics", - rows: [marketMetricsRow] - ), - ] - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/GlobalMarket/MarketOverviewGlobalService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/GlobalMarket/MarketOverviewGlobalService.swift deleted file mode 100644 index f2509689cb..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/GlobalMarket/MarketOverviewGlobalService.swift +++ /dev/null @@ -1,111 +0,0 @@ -import Combine -import Foundation -import MarketKit -import RxRelay -import RxSwift - -class MarketOverviewGlobalService { - private let baseService: MarketOverviewService - private var cancellables = Set() - - private let globalMarketDataRelay = PublishRelay() - private(set) var globalMarketData: GlobalMarketData? { - didSet { - globalMarketDataRelay.accept(globalMarketData) - } - } - - init(baseService: MarketOverviewService) { - self.baseService = baseService - - baseService.$state - .sink { [weak self] in self?.sync(state: $0) } - .store(in: &cancellables) - - sync() - } - - private func sync(state: DataStatus? = nil) { - let state = state ?? baseService.state - - globalMarketData = state.data.map { item in - globalMarketData(globalMarketPoints: item.marketOverview.globalMarketPoints) - } - } - - private func globalMarketData(globalMarketPoints: [GlobalMarketPoint]) -> GlobalMarketData { - let marketCapPointItems = globalMarketPoints.map { - GlobalMarketPointItem(timestamp: $0.timestamp, amount: $0.marketCap) - } - let volume24hPointItems = globalMarketPoints.map { - GlobalMarketPointItem(timestamp: $0.timestamp, amount: $0.volume24h) - } - let defiMarketCapPointItems = globalMarketPoints.map { - GlobalMarketPointItem(timestamp: $0.timestamp, amount: $0.defiMarketCap) - } - let tvlPointItems = globalMarketPoints.map { - GlobalMarketPointItem(timestamp: $0.timestamp, amount: $0.tvl) - } - - return GlobalMarketData( - marketCap: globalMarketItem(pointItems: marketCapPointItems), - volume24h: globalMarketItem(pointItems: volume24hPointItems), - defiMarketCap: globalMarketItem(pointItems: defiMarketCapPointItems), - defiTvl: globalMarketItem(pointItems: tvlPointItems) - ) - } - - private func globalMarketItem(pointItems: [GlobalMarketPointItem]) -> GlobalMarketItem { - GlobalMarketItem( - amount: amount(pointItems: pointItems), - diff: diff(pointItems: pointItems), - pointItems: pointItems - ) - } - - private func amount(pointItems: [GlobalMarketPointItem]) -> CurrencyValue? { - guard let lastAmount = pointItems.last?.amount else { - return nil - } - - return CurrencyValue(currency: currency, value: lastAmount) - } - - private func diff(pointItems: [GlobalMarketPointItem]) -> Decimal? { - guard let firstAmount = pointItems.first?.amount, let lastAmount = pointItems.last?.amount, firstAmount != 0 else { - return nil - } - - return (lastAmount - firstAmount) * 100 / firstAmount - } -} - -extension MarketOverviewGlobalService { - var globalMarketDataObservable: Observable { - globalMarketDataRelay.asObservable() - } - - var currency: Currency { - baseService.currency - } -} - -extension MarketOverviewGlobalService { - struct GlobalMarketData { - let marketCap: GlobalMarketItem - let volume24h: GlobalMarketItem - let defiMarketCap: GlobalMarketItem - let defiTvl: GlobalMarketItem - } - - struct GlobalMarketItem { - let amount: CurrencyValue? - let diff: Decimal? - let pointItems: [GlobalMarketPointItem] - } - - struct GlobalMarketPointItem { - let timestamp: TimeInterval - let amount: Decimal - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/GlobalMarket/MarketOverviewGlobalViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/GlobalMarket/MarketOverviewGlobalViewModel.swift deleted file mode 100644 index 12e93973ca..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/GlobalMarket/MarketOverviewGlobalViewModel.swift +++ /dev/null @@ -1,92 +0,0 @@ -import Chart -import Foundation -import MarketKit -import RxCocoa -import RxRelay -import RxSwift - -class MarketOverviewGlobalViewModel { - private let service: MarketOverviewGlobalService - private let disposeBag = DisposeBag() - - private let viewItemRelay = BehaviorRelay(value: nil) - - init(service: MarketOverviewGlobalService) { - self.service = service - - subscribe(disposeBag, service.globalMarketDataObservable) { [weak self] in self?.sync(globalMarketData: $0) } - - sync(globalMarketData: service.globalMarketData) - } - - private func sync(globalMarketData: MarketOverviewGlobalService.GlobalMarketData?) { - viewItemRelay.accept(globalMarketData.map { viewItem(globalMarketData: $0) }) - } - - private func viewItem(globalMarketData: MarketOverviewGlobalService.GlobalMarketData) -> GlobalMarketViewItem { - GlobalMarketViewItem( - totalMarketCap: chartViewItem(item: globalMarketData.marketCap), - volume24h: chartViewItem(item: globalMarketData.volume24h), - defiCap: chartViewItem(item: globalMarketData.defiMarketCap), - defiTvl: chartViewItem(item: globalMarketData.defiTvl) - ) - } - - private func chartViewItem(item: MarketOverviewGlobalService.GlobalMarketItem) -> ChartViewItem { - let value = item.amount.flatMap { ValueFormatter.instance.formatShort(currency: $0.currency, value: $0.value) } - - var chartData: ChartData? - var trend: MovementTrend = .neutral - - let pointItems = item.pointItems - - if let firstPointItem = pointItems.first, let lastPointItem = pointItems.last { - let chartItems: [ChartItem] = pointItems.map { - let item = ChartItem(timestamp: $0.timestamp) - item.added(name: ChartData.rate, value: $0.amount) - return item - } - - if firstPointItem.amount > lastPointItem.amount { - trend = .down - } else if firstPointItem.amount < lastPointItem.amount { - trend = .up - } - - chartData = ChartData( - items: chartItems, - startWindow: firstPointItem.timestamp, - endWindow: lastPointItem.timestamp - ) - } - - return ChartViewItem( - value: value, - diff: item.diff, - chartData: chartData, - chartTrend: trend - ) - } -} - -extension MarketOverviewGlobalViewModel { - var viewItemDriver: Driver { - viewItemRelay.asDriver() - } -} - -extension MarketOverviewGlobalViewModel { - struct GlobalMarketViewItem { - let totalMarketCap: ChartViewItem - let volume24h: ChartViewItem - let defiCap: ChartViewItem - let defiTvl: ChartViewItem - } - - struct ChartViewItem { - let value: String? - let diff: Decimal? - let chartData: ChartData? - let chartTrend: MovementTrend - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/MarketCategoryView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/MarketCategoryView.swift deleted file mode 100644 index d1e998096f..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/MarketCategoryView.swift +++ /dev/null @@ -1,100 +0,0 @@ -import UIKit - -class MarketCategoryView: UIView { - static let height: CGFloat = 140 - - private let imageView = UIImageView() - private let nameLabel = UILabel() - private let stackView = UIStackView() - private let marketCapLabel = UILabel() - private let diffLabel = UILabel() - - private let button = UIButton() - - var onTap: (() -> Void)? { - didSet { - button.isUserInteractionEnabled = onTap != nil - } - } - - override init(frame: CGRect) { - super.init(frame: frame) - - backgroundColor = .themeLawrence - cornerRadius = .cornerRadius12 - layer.cornerCurve = .continuous - - addSubview(button) - button.snp.makeConstraints { maker in - maker.edges.equalToSuperview() - } - - button.addTarget(self, action: #selector(didTapButton), for: .touchUpInside) - button.isUserInteractionEnabled = false - - addSubview(imageView) - imageView.snp.makeConstraints { maker in - maker.top.trailing.equalToSuperview() - } - - addSubview(stackView) - stackView.snp.makeConstraints { maker in - maker.leading.bottom.equalToSuperview().inset(CGFloat.margin12) - } - - stackView.axis = .horizontal - stackView.spacing = .margin6 - - stackView.addArrangedSubview(marketCapLabel) - marketCapLabel.font = .caption - marketCapLabel.textColor = .themeGray - - stackView.addArrangedSubview(diffLabel) - diffLabel.font = .caption - - addSubview(nameLabel) - nameLabel.snp.makeConstraints { maker in - maker.leading.trailing.equalToSuperview().inset(CGFloat.margin12) - maker.bottom.equalTo(stackView.snp.top).offset(-CGFloat.margin8) - } - - nameLabel.numberOfLines = 0 - nameLabel.font = .subhead1 - nameLabel.textColor = .themeLeah - - updateUI() - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - updateUI() - } - - @objc private func didTapButton() { - onTap?() - } - - private func updateUI() { - button.setBackgroundColor(color: .themeLawrencePressed, forState: .highlighted) - } - - func set(viewItem: MarketOverviewCategoryViewModel.ViewItem) { - imageView.setImage(withUrlString: viewItem.imageUrl, placeholder: nil) - - nameLabel.text = viewItem.name - - marketCapLabel.text = viewItem.marketCap - diffLabel.text = viewItem.diff - diffLabel.textColor = viewItem.diffType.textColor - - nameLabel.snp.updateConstraints { maker in - maker.bottom.equalTo(stackView.snp.top).offset(viewItem.marketCap == nil ? 0 : -CGFloat.margin8) - } - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/MarketOverviewHeaderCell.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/MarketOverviewHeaderCell.swift deleted file mode 100644 index b4f5c112ab..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/MarketOverviewHeaderCell.swift +++ /dev/null @@ -1,141 +0,0 @@ -import ComponentKit -import SnapKit -import UIKit - -class MarketOverviewHeaderCell: BaseThemeCell { - private let leftImage = ImageComponent(size: .iconSize24) - private let titleText = TextComponent() - private let buttonWrapper = UIView() - private let rightButton = SelectorButton() - private let seeAllButton = SecondaryButton() - - var onSelect: ((Int) -> Void)? { - didSet { - rightButton.onSelect = onSelect - } - } - - var onSeeAll: (() -> Void)? - var onTapTitle: (() -> Void)? - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - wrapperView.addSubview(leftImage) - leftImage.snp.makeConstraints { maker in - maker.leading.equalToSuperview().inset(CGFloat.margin16) - maker.centerY.equalToSuperview() - } - - wrapperView.addSubview(titleText) - titleText.snp.makeConstraints { maker in - maker.leading.equalTo(leftImage.snp.trailing).offset(CGFloat.margin16) - maker.centerY.equalToSuperview() - } - - titleText.font = .body - titleText.textColor = .themeLeah - - wrapperView.addSubview(buttonWrapper) - buttonWrapper.snp.makeConstraints { maker in - maker.leading.equalTo(titleText.snp.trailing).offset(CGFloat.margin16) - maker.trailing.equalToSuperview().inset(CGFloat.margin16) - maker.top.bottom.equalToSuperview() - } - - let leftButton = UIButton() - wrapperView.addSubview(leftButton) - leftButton.snp.makeConstraints { maker in - maker.edges.equalTo(titleText) - } - - leftButton.addTarget(self, action: #selector(onTapLeftView), for: .touchUpInside) - - buttonWrapper.addSubview(rightButton) - rightButton.snp.makeConstraints { maker in - maker.leading.trailing.equalToSuperview() - maker.centerY.equalToSuperview() - maker.height.equalTo(28) - } - - buttonWrapper.addSubview(seeAllButton) - seeAllButton.snp.makeConstraints { maker in - maker.leading.trailing.equalToSuperview() - maker.centerY.equalToSuperview() - maker.height.equalTo(28) - } - - seeAllButton.isHidden = true - seeAllButton.set(style: .default) - seeAllButton.setTitle("market.top.section.header.see_all".localized, for: .normal) - seeAllButton.addTarget(self, action: #selector(onTapSeeAll), for: .touchUpInside) - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc private func onTapLeftView() { - onTapTitle?() - } - - var title: String? { - get { titleText.text } - set { - titleText.text = newValue - } - } - - var titleImage: UIImage? { - get { leftImage.imageView.image } - set { leftImage.imageView.image = newValue } - } - - var currentIndex: Int { - rightButton.currentIndex - } - - func set(values: [String]) { - rightButton.set(items: values) - } - - func setSelected(index: Int) { - rightButton.setSelected(index: index) - } - - @objc func onTapSeeAll() { - onSeeAll?() - } - - var buttonMode: ButtonMode = .seeAll { - didSet { - rightButton.isHidden = true - seeAllButton.isHidden = true - rightButton.setContentHuggingPriority(.defaultLow, for: .horizontal) - seeAllButton.setContentHuggingPriority(.defaultLow, for: .horizontal) - rightButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - seeAllButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - - switch buttonMode { - case .selector: - rightButton.isHidden = false - rightButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) - seeAllButton.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - case .seeAll: - seeAllButton.isHidden = false - seeAllButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) - rightButton.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - case .none: return - } - } - } -} - -extension MarketOverviewHeaderCell { - enum ButtonMode { - case selector - case seeAll - case none - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/MarketOverviewMetricsCell.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/MarketOverviewMetricsCell.swift deleted file mode 100644 index e515eba40e..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/MarketOverviewMetricsCell.swift +++ /dev/null @@ -1,123 +0,0 @@ -import Chart -import ComponentKit -import HUD -import SnapKit -import ThemeKit -import UIKit - -class MarketOverviewMetricsCell: UITableViewCell { - static let cellHeight: CGFloat = MarketCardView.height + 2 * .margin16 - - private weak var presentDelegate: IPresentDelegate? - - private let totalMarketCapView: MarketCardView - private let volume24hView: MarketCardView - private let deFiCapView: MarketCardView - private let deFiTvlView: MarketCardView - - init(chartConfiguration: ChartConfiguration, presentDelegate: IPresentDelegate) { - self.presentDelegate = presentDelegate - - totalMarketCapView = MarketCardView(configuration: chartConfiguration) - volume24hView = MarketCardView(configuration: chartConfiguration) - deFiCapView = MarketCardView(configuration: chartConfiguration) - deFiTvlView = MarketCardView(configuration: chartConfiguration) - - super.init(style: .default, reuseIdentifier: nil) - - backgroundColor = .clear - selectionStyle = .none - - let scrollView = UIScrollView() - - contentView.addSubview(scrollView) - scrollView.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview().inset(CGFloat.margin12) - make.top.bottom.equalToSuperview().inset(CGFloat.margin16) - } - - scrollView.isPagingEnabled = true - scrollView.clipsToBounds = false - scrollView.showsHorizontalScrollIndicator = false - - let firstStack = UIStackView(arrangedSubviews: [totalMarketCapView, volume24hView]) - firstStack.spacing = .margin8 - firstStack.distribution = .fillEqually - - let secondStack = UIStackView(arrangedSubviews: [deFiCapView, deFiTvlView]) - secondStack.spacing = .margin8 - secondStack.distribution = .fillEqually - - let leadingView = UIView() - let trailingView = UIView() - - let stackView = UIStackView(arrangedSubviews: [ - leadingView, - firstStack, - secondStack, - trailingView, - ]) - - scrollView.addSubview(stackView) - stackView.snp.makeConstraints { make in - make.leading.trailing.equalTo(scrollView).inset(-CGFloat.margin4) - make.top.bottom.equalTo(scrollView) - make.height.equalTo(scrollView) - } - - stackView.spacing = .margin8 - - leadingView.snp.makeConstraints { make in - make.width.equalTo(0) - } - trailingView.snp.makeConstraints { make in - make.width.equalTo(0) - } - firstStack.snp.makeConstraints { make in - make.width.equalTo(scrollView).offset(-CGFloat.margin8) - } - secondStack.snp.makeConstraints { make in - make.width.equalTo(scrollView).offset(-CGFloat.margin8) - } - - totalMarketCapView.onTap = { [weak self] in self?.onTap(metricType: .totalMarketCap) } - volume24hView.onTap = { [weak self] in self?.onTap(metricType: .volume24h) } - deFiCapView.onTap = { [weak self] in self?.onTap(metricType: .defiCap) } - deFiTvlView.onTap = { [weak self] in self?.onTap(metricType: .tvlInDefi) } - - totalMarketCapView.title = "market.total_market_cap".localized - volume24hView.title = "market.24h_volume".localized - deFiCapView.title = "market.defi_cap".localized - deFiTvlView.title = "market.defi_tvl".localized - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func onTap(metricType: MarketGlobalModule.MetricsType) { - let viewController = MarketGlobalMetricModule.viewController(type: metricType) - presentDelegate?.present(viewController: viewController) - - stat(page: .marketOverview, event: .open(page: metricType.statPage)) - } -} - -extension MarketOverviewMetricsCell { - func set(viewItem: MarketOverviewGlobalViewModel.GlobalMarketViewItem) { - totalMarketCapView.set(viewItem: viewItem.totalMarketCap) - volume24hView.set(viewItem: viewItem.volume24h) - deFiCapView.set(viewItem: viewItem.defiCap) - deFiTvlView.set(viewItem: viewItem.defiTvl) - } -} - -extension MarketCardView { - func set(viewItem: MarketOverviewGlobalViewModel.ChartViewItem) { - value = viewItem.value - descriptionText = DiffLabel.formatted(value: viewItem.diff) - descriptionColor = DiffLabel.color(value: viewItem.diff) - set(chartData: viewItem.chartData, trend: viewItem.chartTrend) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/MarketOverviewModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/MarketOverviewModule.swift deleted file mode 100644 index e552c3d6f8..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/MarketOverviewModule.swift +++ /dev/null @@ -1,44 +0,0 @@ -enum MarketOverviewModule { - static func viewController(presentDelegate: IPresentDelegate) -> MarketOverviewViewController { - let service = MarketOverviewService(marketKit: App.shared.marketKit, currencyManager: App.shared.currencyManager, appManager: App.shared.appManager) - - let globalService = MarketOverviewGlobalService(baseService: service) - let globalViewModel = MarketOverviewGlobalViewModel(service: globalService) - let marketOverviewDataSource = MarketOverviewGlobalDataSource(viewModel: globalViewModel, presentDelegate: presentDelegate) - - let topGainersService = MarketOverviewTopCoinsService(listType: .topGainers, baseService: service) - let topGainersDecorator = MarketListMarketFieldDecorator(service: topGainersService, statPage: .marketOverview) - let topGainersViewModel = MarketOverviewTopCoinsViewModel(service: topGainersService, decorator: topGainersDecorator) - let topGainersDataSource = MarketOverviewTopCoinsDataSource(viewModel: topGainersViewModel, presentDelegate: presentDelegate) - - let topLosersService = MarketOverviewTopCoinsService(listType: .topLosers, baseService: service) - let topLosersDecorator = MarketListMarketFieldDecorator(service: topLosersService, statPage: .marketOverview) - let topLosersViewModel = MarketOverviewTopCoinsViewModel(service: topLosersService, decorator: topLosersDecorator) - let topLosersDataSource = MarketOverviewTopCoinsDataSource(viewModel: topLosersViewModel, presentDelegate: presentDelegate) - - let topPairsService = MarketOverviewTopPairsService(baseService: service) - let topPairsDecorator = MarketListMarketPairDecorator(service: topPairsService) - let topPairsViewModel = MarketOverviewTopPairsViewModel(service: topPairsService, decorator: topPairsDecorator) - let topPairsDataSource = MarketOverviewTopPairsDataSource(viewModel: topPairsViewModel, presentDelegate: presentDelegate) - - let topPlatformsService = MarketOverviewTopPlatformsService(baseService: service) - let topPlatformsDecorator = MarketListTopPlatformDecorator(service: topPlatformsService) - let topPlatformsViewModel = MarketOverviewTopPlatformsViewModel(service: topPlatformsService, decorator: topPlatformsDecorator) - let topPlatformsDataSource = MarketOverviewTopPlatformsDataSource(viewModel: topPlatformsViewModel, presentDelegate: presentDelegate) - - let categoryService = MarketOverviewCategoryService(listType: .topGainers, baseService: service) - let categoryViewModel = MarketOverviewCategoryViewModel(service: categoryService) - let categoryDataSource = MarketOverviewCategoryDataSource(viewModel: categoryViewModel, presentDelegate: presentDelegate) - - let viewModel = MarketOverviewViewModel(service: service) - - return MarketOverviewViewController(viewModel: viewModel, dataSources: [ - marketOverviewDataSource, - topGainersDataSource, - topLosersDataSource, - topPairsDataSource, - topPlatformsDataSource, - categoryDataSource, - ]) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/MarketOverviewService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/MarketOverviewService.swift deleted file mode 100644 index daaa410a5a..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/MarketOverviewService.swift +++ /dev/null @@ -1,74 +0,0 @@ -import Combine -import Foundation -import HsExtensions -import MarketKit -import RxRelay -import RxSwift - -class MarketOverviewService { - private let marketKit: MarketKit.Kit - private let currencyManager: CurrencyManager - private let appManager: IAppManager - private let disposeBag = DisposeBag() - private var cancellables = Set() - private var tasks = Set() - - @PostPublished private(set) var state: DataStatus = .loading - - init(marketKit: MarketKit.Kit, currencyManager: CurrencyManager, appManager: IAppManager) { - self.marketKit = marketKit - self.currencyManager = currencyManager - self.appManager = appManager - } - - private func syncState() { - tasks = Set() - - if case .failed = state { - state = .loading - } - - Task { [weak self, marketKit, currency] in - do { - let currencyCode = currency.code - - async let marketOverview = try marketKit.marketOverview(currencyCode: currencyCode) - async let topMovers = try marketKit.topMovers(currencyCode: currencyCode) - - let item = try await Item(marketOverview: marketOverview, topMovers: topMovers) - self?.state = .completed(item) - } catch { - self?.state = .failed(error) - } - }.store(in: &tasks) - } -} - -extension MarketOverviewService { - var currency: Currency { - currencyManager.baseCurrency - } - - func load() { - currencyManager.$baseCurrency - .sink { [weak self] _ in - self?.syncState() - } - .store(in: &cancellables) - - subscribe(disposeBag, appManager.willEnterForegroundObservable) { [weak self] in self?.syncState() } - - syncState() - } - - func refresh() { - syncState() - } -} - -extension MarketOverviewService { - struct Item { - let marketOverview: MarketOverview - let topMovers: TopMovers - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/MarketOverviewViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/MarketOverviewViewController.swift deleted file mode 100644 index fd47400682..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/MarketOverviewViewController.swift +++ /dev/null @@ -1,128 +0,0 @@ -import Chart -import ComponentKit -import HUD -import RxCocoa -import RxSwift -import SectionsTableView -import ThemeKit -import UIKit - -protocol IMarketOverviewDataSource { - var isReady: Bool { get } - var updateObservable: Observable { get } - func sections(tableView: SectionsTableView) -> [SectionProtocol] -} - -class MarketOverviewViewController: ThemeViewController { - private let disposeBag = DisposeBag() - - private let viewModel: MarketOverviewViewModel - private let dataSources: [IMarketOverviewDataSource] - - private let tableView = SectionsTableView(style: .grouped) - private let spinner = HUDActivityView.create(with: .medium24) - private let errorView = PlaceholderViewModule.reachabilityView() - private let refreshControl = UIRefreshControl() - - init(viewModel: MarketOverviewViewModel, dataSources: [IMarketOverviewDataSource]) { - self.viewModel = viewModel - self.dataSources = dataSources - - super.init() - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - refreshControl.tintColor = .themeLeah - refreshControl.alpha = 0.6 - refreshControl.addTarget(self, action: #selector(onRefresh), for: .valueChanged) - - view.addSubview(tableView) - tableView.snp.makeConstraints { maker in - maker.edges.equalToSuperview() - } - - tableView.separatorStyle = .none - tableView.backgroundColor = .clear - tableView.showsVerticalScrollIndicator = false - - tableView.sectionDataSource = self - tableView.registerCell(forClass: MarketOverviewHeaderCell.self) - - view.addSubview(spinner) - spinner.snp.makeConstraints { maker in - maker.center.equalToSuperview() - } - - spinner.startAnimating() - - view.addSubview(errorView) - errorView.snp.makeConstraints { maker in - maker.edges.equalTo(view.safeAreaLayoutGuide) - } - - errorView.configureSyncError(action: { [weak self] in self?.onRetry() }) - - subscribe(disposeBag, viewModel.successDriver) { [weak self] in self?.sync(success: $0) } - subscribe(disposeBag, viewModel.loadingDriver) { [weak self] loading in - self?.spinner.isHidden = !loading - } - subscribe(disposeBag, viewModel.syncErrorDriver) { [weak self] visible in - self?.errorView.isHidden = !visible - } - - for dataSource in dataSources { - subscribe(MainScheduler.instance, disposeBag, dataSource.updateObservable) { [weak self] in self?.handleDataSourceUpdate() } - } - - viewModel.onLoad() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - tableView.refreshControl = refreshControl - } - - @objc private func onRetry() { - viewModel.refresh() - } - - @objc func onRefresh() { - viewModel.refresh() - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in - self?.refreshControl.endRefreshing() - } - - stat(page: .marketOverview, event: .refresh) - } - - private func sync(success: Bool) { - if success { - tableView.isHidden = false - } else { - tableView.isHidden = true - } - } - - func handleDataSourceUpdate() { - guard dataSources.allSatisfy(\.isReady) else { - return - } - - tableView.reload() - } -} - -extension MarketOverviewViewController: SectionsDataSource { - func buildSections() -> [SectionProtocol] { - dataSources.compactMap { $0.sections(tableView: tableView) }.flatMap { $0 } - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/MarketOverviewViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/MarketOverviewViewModel.swift deleted file mode 100644 index 641a19810c..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/MarketOverviewViewModel.swift +++ /dev/null @@ -1,62 +0,0 @@ -import Combine -import RxCocoa -import RxRelay -import RxSwift - -class MarketOverviewViewModel { - private let service: MarketOverviewService - private var cancellables = Set() - - private let successRelay = BehaviorRelay(value: false) - private let loadingRelay = BehaviorRelay(value: true) - private let syncErrorRelay = BehaviorRelay(value: false) - - init(service: MarketOverviewService) { - self.service = service - - service.$state - .sink { [weak self] in self?.sync(state: $0) } - .store(in: &cancellables) - - sync(state: service.state) - } - - private func sync(state: DataStatus) { - switch state { - case .loading: - loadingRelay.accept(true) - syncErrorRelay.accept(false) - successRelay.accept(false) - case .completed: - loadingRelay.accept(false) - syncErrorRelay.accept(false) - successRelay.accept(true) - case .failed: - loadingRelay.accept(false) - syncErrorRelay.accept(true) - successRelay.accept(false) - } - } -} - -extension MarketOverviewViewModel { - var successDriver: Driver { - successRelay.asDriver() - } - - var loadingDriver: Driver { - loadingRelay.asDriver() - } - - var syncErrorDriver: Driver { - syncErrorRelay.asDriver() - } - - func onLoad() { - service.load() - } - - func refresh() { - service.refresh() - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopCoins/MarketOverviewTopCoinsDataSource.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopCoins/MarketOverviewTopCoinsDataSource.swift deleted file mode 100644 index f16a7aa3c1..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopCoins/MarketOverviewTopCoinsDataSource.swift +++ /dev/null @@ -1,47 +0,0 @@ -import UIKit - -class MarketOverviewTopCoinsDataSource: BaseMarketOverviewTopListDataSource { - private let viewModel: MarketOverviewTopCoinsViewModel - - init(viewModel: MarketOverviewTopCoinsViewModel, presentDelegate: IPresentDelegate) { - self.viewModel = viewModel - - let imageName: String - let title: String - - switch viewModel.listType { - case .topGainers: - imageName = "circle_up_24" - title = "market.top.section.header.top_gainers".localized - case .topLosers: - imageName = "circle_down_24" - title = "market.top.section.header.top_losers".localized - } - - super.init( - topListViewModel: viewModel, - presentDelegate: presentDelegate, - rightSelectorMode: .selector, - imageName: imageName, - title: title - ) - } - - override func didTapSeeAll() { - let module = MarketTopModule.viewController( - marketTop: viewModel.marketTop, - sortingField: viewModel.listType.sortingField, - marketField: viewModel.listType.marketField - ) - presentDelegate?.present(viewController: module) - - stat(page: .marketOverview, section: viewModel.listType.statSection, event: .open(page: .topCoins)) - } - - override func onSelect(listViewItem: MarketModule.ListViewItem) { - if let coinUid = listViewItem.uid, let module = CoinPageModule.viewController(coinUid: coinUid) { - presentDelegate?.present(viewController: module) - stat(page: .marketOverview, section: viewModel.listType.statSection, event: .openCoin(coinUid: coinUid)) - } - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopCoins/MarketOverviewTopCoinsService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopCoins/MarketOverviewTopCoinsService.swift deleted file mode 100644 index e513a8bf98..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopCoins/MarketOverviewTopCoinsService.swift +++ /dev/null @@ -1,104 +0,0 @@ -import Combine -import Foundation -import MarketKit -import RxRelay -import RxSwift - -class MarketOverviewTopCoinsService { - private let baseService: MarketOverviewService - private var cancellables = Set() - - private(set) var marketTop: MarketModule.MarketTop = .top100 - let listType: ListType - - private let marketInfosRelay = PublishRelay<[MarketInfo]?>() - private(set) var marketInfos: [MarketInfo]? { - didSet { - marketInfosRelay.accept(marketInfos) - } - } - - init(listType: ListType, baseService: MarketOverviewService) { - self.listType = listType - self.baseService = baseService - - baseService.$state - .sink { [weak self] in self?.sync(state: $0) } - .store(in: &cancellables) - - sync(state: baseService.state) - } - - private func sync(state: DataStatus) { - marketInfos = state.data.map { item in - switch listType { - case .topGainers: - switch marketTop { - case .top100: return item.topMovers.gainers100 - case .top200: return item.topMovers.gainers200 - case .top300: return item.topMovers.gainers300 - } - case .topLosers: - switch marketTop { - case .top100: return item.topMovers.losers100 - case .top200: return item.topMovers.losers200 - case .top300: return item.topMovers.losers300 - } - } - } - } -} - -extension MarketOverviewTopCoinsService { - var marketInfosObservable: Observable<[MarketInfo]?> { - marketInfosRelay.asObservable() - } - - func set(marketTop: MarketModule.MarketTop) { - self.marketTop = marketTop - sync(state: baseService.state) - } -} - -extension MarketOverviewTopCoinsService: IMarketListDecoratorService { - var initialIndex: Int { - 0 - } - - var currency: Currency { - baseService.currency - } - - var priceChangeType: MarketModule.PriceChangeType { - .day - } - - func onUpdate(index _: Int) {} -} - -extension MarketOverviewTopCoinsService { - enum ListType: String, CaseIterable { - case topGainers - case topLosers - - var sortingField: MarketModule.SortingField { - switch self { - case .topGainers: return .topGainers - case .topLosers: return .topLosers - } - } - - var marketField: MarketModule.MarketField { - switch self { - case .topGainers, .topLosers: return .price - } - } - - var statSection: StatSection { - switch self { - case .topGainers: return .topGainers - case .topLosers: return .topLosers - } - } - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopCoins/MarketOverviewTopCoinsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopCoins/MarketOverviewTopCoinsViewModel.swift deleted file mode 100644 index 110ff412e4..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopCoins/MarketOverviewTopCoinsViewModel.swift +++ /dev/null @@ -1,56 +0,0 @@ -import MarketKit -import RxCocoa -import RxRelay -import RxSwift - -class MarketOverviewTopCoinsViewModel { - private let service: MarketOverviewTopCoinsService - private let decorator: MarketListMarketFieldDecorator - private let disposeBag = DisposeBag() - - private let listViewItemsRelay = BehaviorRelay<[MarketModule.ListViewItem]?>(value: nil) - - init(service: MarketOverviewTopCoinsService, decorator: MarketListMarketFieldDecorator) { - self.service = service - self.decorator = decorator - - subscribe(disposeBag, service.marketInfosObservable) { [weak self] in self?.sync(marketInfos: $0) } - - sync(marketInfos: service.marketInfos) - } - - private func sync(marketInfos: [MarketInfo]?) { - listViewItemsRelay.accept(marketInfos.map { $0.map { decorator.listViewItem(item: $0) } }) - } -} - -extension MarketOverviewTopCoinsViewModel { - var marketTop: MarketModule.MarketTop { - service.marketTop - } - - var listType: MarketOverviewTopCoinsService.ListType { - service.listType - } -} - -extension MarketOverviewTopCoinsViewModel: IBaseMarketOverviewTopListViewModel { - var listViewItemsDriver: Driver<[MarketModule.ListViewItem]?> { - listViewItemsRelay.asDriver() - } - - var selectorTitles: [String] { - MarketModule.MarketTop.allCases.map(\.title) - } - - var selectorIndex: Int { - MarketModule.MarketTop.allCases.firstIndex(of: service.marketTop) ?? 0 - } - - func onSelect(selectorIndex: Int) { - let marketTop = MarketModule.MarketTop.allCases[selectorIndex] - service.set(marketTop: marketTop) - - stat(page: .marketOverview, section: listType.statSection, event: .switchMarketTop(marketTop: marketTop.statMarketTop)) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopPairs/MarketOverviewTopPairsDataSource.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopPairs/MarketOverviewTopPairsDataSource.swift deleted file mode 100644 index 693ffd39a9..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopPairs/MarketOverviewTopPairsDataSource.swift +++ /dev/null @@ -1,34 +0,0 @@ -import UIKit - -class MarketOverviewTopPairsDataSource: BaseMarketOverviewTopListDataSource { - private let viewModel: MarketOverviewTopPairsViewModel - - init(viewModel: MarketOverviewTopPairsViewModel, presentDelegate: IPresentDelegate) { - self.viewModel = viewModel - - super.init( - topListViewModel: viewModel, - presentDelegate: presentDelegate, - rightSelectorMode: .none, - imageName: "pairs_24", - title: "market.top.top_market_pairs".localized - ) - } - - override func didTapSeeAll() { - let module = MarketTopPairsModule.viewController() - presentDelegate?.present(viewController: module) - - stat(page: .marketOverview, event: .open(page: .topMarketPairs)) - } - - override func onSelect(listViewItem: MarketModule.ListViewItem) { - guard let uid = listViewItem.uid, let marketPair = viewModel.marketPair(uid: uid), let tradeUrl = marketPair.tradeUrl else { - return - } - - UrlManager.open(url: tradeUrl) - - stat(page: .marketOverview, event: .open(page: .externalMarketPair)) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopPairs/MarketOverviewTopPairsService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopPairs/MarketOverviewTopPairsService.swift deleted file mode 100644 index 1385522789..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopPairs/MarketOverviewTopPairsService.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Combine -import Foundation -import HsExtensions -import MarketKit - -class MarketOverviewTopPairsService { - private let baseService: MarketOverviewService - private var cancellables = Set() - - @PostPublished private(set) var marketPairs: [MarketPair]? - - init(baseService: MarketOverviewService) { - self.baseService = baseService - - baseService.$state - .sink { [weak self] in self?.sync(state: $0) } - .store(in: &cancellables) - - sync(state: baseService.state) - } - - private func sync(state: DataStatus) { - marketPairs = state.data.map { item in - item.marketOverview.topPairs - } - } -} - -extension MarketOverviewTopPairsService: IMarketListMarketPairDecoratorService { - var currency: Currency { - baseService.currency - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopPairs/MarketOverviewTopPairsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopPairs/MarketOverviewTopPairsViewModel.swift deleted file mode 100644 index 7fbcec2f7c..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopPairs/MarketOverviewTopPairsViewModel.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Combine -import Foundation -import MarketKit -import RxCocoa -import RxRelay -import RxSwift - -class MarketOverviewTopPairsViewModel { - private let service: MarketOverviewTopPairsService - private let decorator: MarketListMarketPairDecorator - private var cancellables = Set() - - private let listViewItemsRelay = BehaviorRelay<[MarketModule.ListViewItem]?>(value: nil) - - init(service: MarketOverviewTopPairsService, decorator: MarketListMarketPairDecorator) { - self.service = service - self.decorator = decorator - - service.$marketPairs - .receive(on: DispatchQueue.main) - .sink { [weak self] in self?.sync(marketPairs: $0) } - .store(in: &cancellables) - - sync(marketPairs: service.marketPairs) - } - - private func sync(marketPairs: [MarketPair]?) { - listViewItemsRelay.accept(marketPairs.map { $0.map { decorator.listViewItem(item: $0) } }) - } -} - -extension MarketOverviewTopPairsViewModel { - func marketPair(uid: String) -> MarketPair? { - service.marketPairs?.first { $0.uid == uid } - } -} - -extension MarketOverviewTopPairsViewModel: IBaseMarketOverviewTopListViewModel { - var listViewItemsDriver: Driver<[MarketModule.ListViewItem]?> { - listViewItemsRelay.asDriver() - } - - var selectorTitles: [String] { - [] - } - - var selectorIndex: Int { - 0 - } - - func onSelect(selectorIndex _: Int) {} -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopPlatforms/MarketOverviewTopPlatformsDataSource.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopPlatforms/MarketOverviewTopPlatformsDataSource.swift deleted file mode 100644 index 24458fc006..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopPlatforms/MarketOverviewTopPlatformsDataSource.swift +++ /dev/null @@ -1,34 +0,0 @@ -import UIKit - -class MarketOverviewTopPlatformsDataSource: BaseMarketOverviewTopListDataSource { - private let viewModel: MarketOverviewTopPlatformsViewModel - - init(viewModel: MarketOverviewTopPlatformsViewModel, presentDelegate: IPresentDelegate) { - self.viewModel = viewModel - - super.init( - topListViewModel: viewModel, - presentDelegate: presentDelegate, - rightSelectorMode: .selector, - imageName: "blocks_24", - title: "market.top.top_platforms".localized - ) - } - - override func didTapSeeAll() { - let module = MarketTopPlatformsModule.viewController(timePeriod: viewModel.timePeriod) - presentDelegate?.present(viewController: module) - - stat(page: .marketOverview, event: .open(page: .topPlatforms)) - } - - override func onSelect(listViewItem: MarketModule.ListViewItem) { - guard let uid = listViewItem.uid, let topPlatform = viewModel.topPlatform(uid: uid) else { - return - } - - presentDelegate?.present(viewController: TopPlatformModule.viewController(topPlatform: topPlatform)) - - stat(page: .marketOverview, event: .openPlatform(chainUid: uid)) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopPlatforms/MarketOverviewTopPlatformsService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopPlatforms/MarketOverviewTopPlatformsService.swift deleted file mode 100644 index e39d36df96..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopPlatforms/MarketOverviewTopPlatformsService.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Combine -import Foundation -import MarketKit -import RxRelay -import RxSwift - -class MarketOverviewTopPlatformsService { - private let baseService: MarketOverviewService - private var cancellables = Set() - - var timePeriod: HsTimePeriod = .week1 { - didSet { - sync() - } - } - - private let topPlatformsRelay = PublishRelay<[TopPlatform]?>() - private(set) var topPlatforms: [TopPlatform]? { - didSet { - topPlatformsRelay.accept(topPlatforms) - } - } - - init(baseService: MarketOverviewService) { - self.baseService = baseService - - baseService.$state - .sink { [weak self] in self?.sync(state: $0) } - .store(in: &cancellables) - - sync() - } - - private func sync(state: DataStatus? = nil) { - let state = state ?? baseService.state - - topPlatforms = state.data.map { item in - item.marketOverview.topPlatforms - } - } -} - -extension MarketOverviewTopPlatformsService { - var topPlatformsObservable: Observable<[TopPlatform]?> { - topPlatformsRelay.asObservable() - } -} - -extension MarketOverviewTopPlatformsService: IMarketListTopPlatformDecoratorService { - var currency: Currency { - baseService.currency - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopPlatforms/MarketOverviewTopPlatformsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopPlatforms/MarketOverviewTopPlatformsViewModel.swift deleted file mode 100644 index fb83d52d86..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketOverview/TopPlatforms/MarketOverviewTopPlatformsViewModel.swift +++ /dev/null @@ -1,56 +0,0 @@ -import MarketKit -import RxCocoa -import RxRelay -import RxSwift - -class MarketOverviewTopPlatformsViewModel { - private let service: MarketOverviewTopPlatformsService - private let decorator: MarketListTopPlatformDecorator - private let disposeBag = DisposeBag() - - private let listViewItemsRelay = BehaviorRelay<[MarketModule.ListViewItem]?>(value: nil) - - init(service: MarketOverviewTopPlatformsService, decorator: MarketListTopPlatformDecorator) { - self.service = service - self.decorator = decorator - - subscribe(disposeBag, service.topPlatformsObservable) { [weak self] in self?.sync(topPlatforms: $0) } - - sync(topPlatforms: service.topPlatforms) - } - - private func sync(topPlatforms: [TopPlatform]?) { - listViewItemsRelay.accept(topPlatforms.map { $0.map { decorator.listViewItem(item: $0) } }) - } -} - -extension MarketOverviewTopPlatformsViewModel { - var timePeriod: HsTimePeriod { - service.timePeriod - } - - func topPlatform(uid: String) -> TopPlatform? { - service.topPlatforms?.first { $0.blockchain.uid == uid } - } -} - -extension MarketOverviewTopPlatformsViewModel: IBaseMarketOverviewTopListViewModel { - var listViewItemsDriver: Driver<[MarketModule.ListViewItem]?> { - listViewItemsRelay.asDriver() - } - - var selectorTitles: [String] { - MarketTopPlatformsModule.selectorValues.map(\.title) - } - - var selectorIndex: Int { - MarketTopPlatformsModule.selectorValues.firstIndex(of: service.timePeriod) ?? 0 - } - - func onSelect(selectorIndex: Int) { - let timePeriod = MarketTopPlatformsModule.selectorValues[selectorIndex] - service.timePeriod = timePeriod - - stat(page: .marketOverview, section: .topPlatforms, event: .switchPeriod(period: timePeriod.statPeriod)) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketPosts/MarketPostModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketPosts/MarketPostModule.swift deleted file mode 100644 index f13db300bf..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketPosts/MarketPostModule.swift +++ /dev/null @@ -1,7 +0,0 @@ -enum MarketPostModule { - static func viewController() -> MarketPostViewController { - let service = MarketPostService(marketKit: App.shared.marketKit) - let viewModel = MarketPostViewModel(service: service) - return MarketPostViewController(viewModel: viewModel, urlManager: UrlManager(inApp: true)) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketPosts/MarketPostService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketPosts/MarketPostService.swift deleted file mode 100644 index c68b62618f..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketPosts/MarketPostService.swift +++ /dev/null @@ -1,59 +0,0 @@ -import HsExtensions -import MarketKit -import RxRelay -import RxSwift - -class MarketPostService { - private let marketKit: Kit - private var tasks = Set() - - private let stateRelay = PublishRelay() - private(set) var state: State = .loading { - didSet { - stateRelay.accept(state) - } - } - - init(marketKit: Kit) { - self.marketKit = marketKit - } - - private func fetch() { - tasks = Set() - - if case .failed = state { - state = .loading - } - - Task { [weak self, marketKit] in - do { - let posts = try await marketKit.posts() - self?.state = .loaded(posts: posts) - } catch { - self?.state = .failed(error: error) - } - }.store(in: &tasks) - } -} - -extension MarketPostService { - var stateObservable: Observable { - stateRelay.asObservable() - } - - func load() { - fetch() - } - - func refresh() { - fetch() - } -} - -extension MarketPostService { - enum State { - case loading - case loaded(posts: [Post]) - case failed(error: Error) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketPosts/MarketPostViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketPosts/MarketPostViewController.swift deleted file mode 100644 index 2cb8acadc1..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketPosts/MarketPostViewController.swift +++ /dev/null @@ -1,159 +0,0 @@ -import ComponentKit -import HUD -import RxSwift -import SectionsTableView -import ThemeKit -import UIKit - -class MarketPostViewController: ThemeViewController { - private let viewModel: MarketPostViewModel - private let urlManager: UrlManager - private let disposeBag = DisposeBag() - - private let tableView = SectionsTableView(style: .grouped) - private let spinner = HUDActivityView.create(with: .medium24) - private let errorView = PlaceholderViewModule.reachabilityView() - private let refreshControl = UIRefreshControl() - - weak var parentNavigationController: UINavigationController? - - private var viewItems: [MarketPostViewModel.ViewItem]? - - init(viewModel: MarketPostViewModel, urlManager: UrlManager) { - self.viewModel = viewModel - self.urlManager = urlManager - - super.init() - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - refreshControl.tintColor = .themeLeah - refreshControl.alpha = 0.6 - refreshControl.addTarget(self, action: #selector(onRefresh), for: .valueChanged) - - view.addSubview(tableView) - tableView.snp.makeConstraints { maker in - maker.edges.equalToSuperview() - } - - tableView.separatorStyle = .none - tableView.backgroundColor = .clear - - tableView.sectionDataSource = self - tableView.registerCell(forClass: PostCell.self) - - view.addSubview(spinner) - spinner.snp.makeConstraints { maker in - maker.center.equalToSuperview() - } - - spinner.startAnimating() - - view.addSubview(errorView) - errorView.snp.makeConstraints { maker in - maker.edges.equalTo(view.safeAreaLayoutGuide) - } - - errorView.configureSyncError(action: { [weak self] in self?.onRetry() }) - - subscribe(disposeBag, viewModel.viewItemsDriver) { [weak self] in self?.sync(viewItems: $0) } - subscribe(disposeBag, viewModel.loadingDriver) { [weak self] loading in - self?.spinner.isHidden = !loading - } - subscribe(disposeBag, viewModel.syncErrorDriver) { [weak self] visible in - self?.errorView.isHidden = !visible - } - - viewModel.onLoad() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - tableView.refreshControl = refreshControl - } - - @objc private func onRetry() { - refresh() - } - - @objc private func onRefresh() { - refresh() - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in - self?.refreshControl.endRefreshing() - } - - stat(page: .news, event: .refresh) - } - - private func refresh() { - viewModel.refresh() - } - - private func sync(viewItems: [MarketPostViewModel.ViewItem]?) { - self.viewItems = viewItems - - if viewItems != nil { - tableView.bounces = true - } else { - tableView.bounces = false - } - - tableView.reload() - } - - private func open(url: String) { - urlManager.open(url: url, from: parentNavigationController) - - stat(page: .news, event: .open(page: .externalNews)) - } -} - -extension MarketPostViewController: SectionsDataSource { - private func row(viewItem: MarketPostViewModel.ViewItem) -> RowProtocol { - Row( - id: viewItem.title, - height: PostCell.height, - autoDeselect: true, - bind: { cell, _ in - cell.set(backgroundStyle: .lawrence, isFirst: true, isLast: true) - cell.bind( - header: viewItem.source, - title: viewItem.title, - body: viewItem.body, - time: viewItem.timeAgo - ) - }, - action: { [weak self] _ in - self?.open(url: viewItem.url) - } - ) - } - - func buildSections() -> [SectionProtocol] { - var sections = [SectionProtocol]() - - if let viewItems { - for (index, viewItem) in viewItems.enumerated() { - let section = Section( - id: "post_\(index)", - headerState: .margin(height: .margin12), - footerState: .margin(height: index == viewItems.count - 1 ? .margin32 : 0), - rows: [row(viewItem: viewItem)] - ) - - sections.append(section) - } - } - - return sections - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketPosts/MarketPostViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketPosts/MarketPostViewModel.swift deleted file mode 100644 index d93cf1a948..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketPosts/MarketPostViewModel.swift +++ /dev/null @@ -1,100 +0,0 @@ -import Foundation -import MarketKit -import RxCocoa -import RxRelay -import RxSwift - -class MarketPostViewModel { - private let service: MarketPostService - private let disposeBag = DisposeBag() - - private let viewItemsRelay = BehaviorRelay<[ViewItem]?>(value: nil) - private let loadingRelay = BehaviorRelay(value: false) - private let syncErrorRelay = BehaviorRelay(value: false) - - init(service: MarketPostService) { - self.service = service - - subscribe(disposeBag, service.stateObservable) { [weak self] in self?.sync(state: $0) } - - sync(state: service.state) - } - - private func sync(state: MarketPostService.State) { - switch state { - case .loading: - viewItemsRelay.accept(nil) - loadingRelay.accept(true) - syncErrorRelay.accept(false) - case let .loaded(posts): - viewItemsRelay.accept(posts.map { viewItem(post: $0) }) - loadingRelay.accept(false) - syncErrorRelay.accept(false) - case .failed: - viewItemsRelay.accept(nil) - loadingRelay.accept(false) - syncErrorRelay.accept(true) - } - } - - private func viewItem(post: Post) -> ViewItem { - ViewItem( - source: post.source, - title: post.title, - body: post.body, - timeAgo: timeAgo(interval: Date().timeIntervalSince1970 - post.timestamp), - url: post.url - ) - } - - private func timeAgo(interval: TimeInterval) -> String { - var interval = Int(interval) / 60 - - // interval from post in minutes - if interval < 60 { - return "timestamp.min_ago".localized(max(1, interval)) - } - - // interval in hours - interval /= 60 - if interval < 24 { - return "timestamp.hours_ago".localized(interval) - } - - // interval in days - interval /= 24 - return "timestamp.days_ago".localized(interval) - } -} - -extension MarketPostViewModel { - var viewItemsDriver: Driver<[ViewItem]?> { - viewItemsRelay.asDriver() - } - - var loadingDriver: Driver { - loadingRelay.asDriver() - } - - var syncErrorDriver: Driver { - syncErrorRelay.asDriver() - } - - func onLoad() { - service.load() - } - - func refresh() { - service.refresh() - } -} - -extension MarketPostViewModel { - struct ViewItem { - let source: String - let title: String - let body: String - let timeAgo: String - let url: String - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTop/MarketTopModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTop/MarketTopModule.swift deleted file mode 100644 index b00d8510b7..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTop/MarketTopModule.swift +++ /dev/null @@ -1,27 +0,0 @@ -import ThemeKit -import UIKit - -enum MarketTopModule { - static func viewController(marketTop: MarketModule.MarketTop = .top100, sortingField: MarketModule.SortingField = .highestCap, marketField: MarketModule.MarketField = .price) -> UIViewController { - let service = MarketTopService( - marketKit: App.shared.marketKit, - currencyManager: App.shared.currencyManager, - marketTop: marketTop, - sortingField: sortingField, - marketField: marketField - ) - let watchlistToggleService = MarketWatchlistToggleService( - coinUidService: service, - favoritesManager: App.shared.favoritesManager, - statPage: .topCoins - ) - - let decorator = MarketListMarketFieldDecorator(service: service, statPage: .topCoins) - let listViewModel = MarketListWatchViewModel(service: service, watchlistToggleService: watchlistToggleService, decorator: decorator) - let headerViewModel = MarketMultiSortHeaderViewModel(service: service, decorator: decorator) - - let viewController = MarketTopViewController(listViewModel: listViewModel, headerViewModel: headerViewModel) - - return ThemeNavigationController(rootViewController: viewController) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTop/MarketTopService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTop/MarketTopService.swift deleted file mode 100644 index 86ab164a84..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTop/MarketTopService.swift +++ /dev/null @@ -1,131 +0,0 @@ -import Combine -import HsExtensions -import MarketKit -import RxRelay -import RxSwift - -class MarketTopService: IMarketMultiSortHeaderService { - typealias Item = MarketInfo - - private let marketKit: MarketKit.Kit - private let currencyManager: CurrencyManager - private let disposeBag = DisposeBag() - private var tasks = Set() - - private var internalState: State = .loading { - didSet { - syncState() - } - } - - @PostPublished private(set) var state: MarketListServiceState = .loading - - var marketTop: MarketModule.MarketTop { - didSet { - syncIfPossible() - - stat(page: .topCoins, event: .switchMarketTop(marketTop: marketTop.statMarketTop)) - } - } - - var sortingField: MarketModule.SortingField { - didSet { - syncIfPossible() - - stat(page: .topCoins, event: .switchSortType(sortType: sortingField.statSortType)) - } - } - - let initialIndex: Int - - init(marketKit: MarketKit.Kit, currencyManager: CurrencyManager, marketTop: MarketModule.MarketTop, sortingField: MarketModule.SortingField, marketField: MarketModule.MarketField) { - self.marketKit = marketKit - self.currencyManager = currencyManager - self.marketTop = marketTop - self.sortingField = sortingField - initialIndex = marketField.rawValue - - syncMarketInfos() - } - - private func syncMarketInfos() { - tasks = Set() - - if case .failed = state { - internalState = .loading - } - - Task { [weak self, marketKit, currency] in - do { - let marketInfos = try await marketKit.marketInfos(top: 1000, currencyCode: currency.code) - self?.internalState = .loaded(marketInfos: marketInfos) - } catch { - self?.internalState = .failed(error: error) - } - }.store(in: &tasks) - } - - private func syncState(reorder: Bool = false) { - switch internalState { - case .loading: - state = .loading - case let .loaded(marketInfos): - let marketInfos: [MarketInfo] = Array(marketInfos.prefix(marketTop.rawValue)) - state = .loaded(items: marketInfos.sorted(sortingField: sortingField, priceChangeType: priceChangeType), softUpdate: false, reorder: reorder) - case let .failed(error): - state = .failed(error: error) - } - } - - private func syncIfPossible() { - guard case .loaded = internalState else { - return - } - - syncState(reorder: true) - } -} - -extension MarketTopService: IMarketListService { - var statePublisher: AnyPublisher, Never> { - $state - } - - func refresh() { - syncMarketInfos() - } -} - -extension MarketTopService: IMarketListCoinUidService { - func coinUid(index: Int) -> String? { - guard case let .loaded(marketInfos, _, _) = state, index < marketInfos.count else { - return nil - } - - return marketInfos[index].fullCoin.coin.uid - } -} - -extension MarketTopService: IMarketListDecoratorService { - var currency: Currency { - currencyManager.baseCurrency - } - - var priceChangeType: MarketModule.PriceChangeType { - .day - } - - func onUpdate(index _: Int) { - if case let .loaded(marketInfos, _, _) = state { - state = .loaded(items: marketInfos, softUpdate: false, reorder: false) - } - } -} - -extension MarketTopService { - private enum State { - case loading - case loaded(marketInfos: [MarketInfo]) - case failed(error: Error) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTop/MarketTopViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTop/MarketTopViewController.swift deleted file mode 100644 index 1d11e99751..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTop/MarketTopViewController.swift +++ /dev/null @@ -1,59 +0,0 @@ -import SectionsTableView -import SnapKit -import ThemeKit -import UIKit - -class MarketTopViewController: MarketListViewController { - private let multiSortHeaderView: MarketMultiSortHeaderView - - override var viewController: UIViewController? { self } - override var headerView: UITableViewHeaderFooterView? { multiSortHeaderView } - override var refreshEnabled: Bool { false } - - init(listViewModel: IMarketListViewModel, headerViewModel: MarketMultiSortHeaderViewModel) { - multiSortHeaderView = MarketMultiSortHeaderView(viewModel: headerViewModel, hasLeftSelector: true) - - super.init(listViewModel: listViewModel, statPage: .topCoins) - - multiSortHeaderView.viewController = self - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - navigationItem.largeTitleDisplayMode = .never - navigationItem.rightBarButtonItem = UIBarButtonItem(title: "button.close".localized, style: .plain, target: self, action: #selector(onTapClose)) - - tableView.registerCell(forClass: MarketHeaderCell.self) - } - - @objc private func onTapClose() { - dismiss(animated: true) - } - - override func topSections(loaded _: Bool) -> [SectionProtocol] { - [ - Section( - id: "header", - rows: [ - Row( - id: "header", - height: MarketHeaderCell.height, - bind: { cell, _ in - cell.set( - title: "market.top.title".localized, - description: "market.top.description".localized, - imageMode: .remote(imageUrl: "top_coins".headerImageUrl) - ) - } - ), - ] - ), - ] - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPairs/MarketListMarketPairDecorator.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPairs/MarketListMarketPairDecorator.swift deleted file mode 100644 index 9f009695cc..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPairs/MarketListMarketPairDecorator.swift +++ /dev/null @@ -1,38 +0,0 @@ -import Foundation -import MarketKit - -protocol IMarketListMarketPairDecoratorService { - var currency: Currency { get } -} - -class MarketListMarketPairDecorator { - typealias Item = MarketPair - - private let service: IMarketListMarketPairDecoratorService - - init(service: IMarketListMarketPairDecoratorService) { - self.service = service - } -} - -extension MarketListMarketPairDecorator: IMarketListDecorator { - func listViewItem(item: MarketPair) -> MarketModule.ListViewItem { - let currency = service.currency - - let volume = item.volume.flatMap { ValueFormatter.instance.formatShort(currency: currency, value: $0) } ?? "n/a".localized - let price = item.price.flatMap { ValueFormatter.instance.formatShort(value: $0, decimalCount: 8, symbol: item.target) } ?? "n/a".localized - - return MarketModule.ListViewItem( - uid: item.uid, - iconUrl: item.marketImageUrl, - iconShape: .square, - iconPlaceholderName: "placeholder_rectangle_32", - leftPrimaryValue: "\(item.base)/\(item.target)", - leftSecondaryValue: item.marketName, - badge: "\(item.rank)", - badgeSecondaryValue: nil, - rightPrimaryValue: volume, - rightSecondaryValue: .volume(price) - ) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPairs/MarketTopPairsModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPairs/MarketTopPairsModule.swift deleted file mode 100644 index 3e1f4f2f97..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPairs/MarketTopPairsModule.swift +++ /dev/null @@ -1,13 +0,0 @@ -import ThemeKit -import UIKit - -enum MarketTopPairsModule { - static func viewController() -> UIViewController { - let viewModel = MarketTopPairsViewModel(marketKit: App.shared.marketKit, currencyManager: App.shared.currencyManager, appManager: App.shared.appManager) - let decorator = MarketListMarketPairDecorator(service: viewModel) - let listViewModel = MarketListViewModel(service: viewModel, decorator: decorator) - let viewController = MarketTopPairsViewController(viewModel: viewModel, listViewModel: listViewModel) - - return ThemeNavigationController(rootViewController: viewController) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPairs/MarketTopPairsViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPairs/MarketTopPairsViewController.swift deleted file mode 100644 index 623bdec915..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPairs/MarketTopPairsViewController.swift +++ /dev/null @@ -1,66 +0,0 @@ -import SectionsTableView -import SnapKit -import ThemeKit -import UIKit - -class MarketTopPairsViewController: MarketListViewController { - private let viewModel: MarketTopPairsViewModel - - override var viewController: UIViewController? { self } - override var refreshEnabled: Bool { false } - - init(viewModel: MarketTopPairsViewModel, listViewModel: IMarketListViewModel) { - self.viewModel = viewModel - - super.init(listViewModel: listViewModel, statPage: .topMarketPairs) - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - navigationItem.largeTitleDisplayMode = .never - navigationItem.rightBarButtonItem = UIBarButtonItem(title: "button.close".localized, style: .plain, target: self, action: #selector(onTapClose)) - - tableView.registerCell(forClass: MarketHeaderCell.self) - } - - @objc private func onTapClose() { - dismiss(animated: true) - } - - override func topSections(loaded _: Bool) -> [SectionProtocol] { - [ - Section( - id: "header", - rows: [ - Row( - id: "header", - height: MarketHeaderCell.height, - bind: { cell, _ in - cell.set( - title: "top_pairs.title".localized, - description: "top_pairs.description".localized, - imageMode: .remote(imageUrl: "token_pairs".headerImageUrl) - ) - } - ), - ] - ), - ] - } - - override func onSelect(viewItem: MarketModule.ListViewItem) { - guard let uid = viewItem.uid, let marketPair = viewModel.marketPair(uid: uid), let tradeUrl = marketPair.tradeUrl else { - return - } - - UrlManager.open(url: tradeUrl) - - stat(page: .topMarketPairs, event: .open(page: .externalMarketPair)) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPairs/MarketTopPairsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPairs/MarketTopPairsViewModel.swift deleted file mode 100644 index 4dc89c76ca..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPairs/MarketTopPairsViewModel.swift +++ /dev/null @@ -1,72 +0,0 @@ -import Combine -import HsExtensions -import MarketKit -import RxSwift - -class MarketTopPairsViewModel { - typealias Item = MarketPair - - private let marketKit: MarketKit.Kit - private let currencyManager: CurrencyManager - private var disposeBag = DisposeBag() - private var cancellables = Set() - private var tasks = Set() - - @PostPublished private(set) var state: MarketListServiceState = .loading - - init(marketKit: MarketKit.Kit, currencyManager: CurrencyManager, appManager: IAppManager) { - self.marketKit = marketKit - self.currencyManager = currencyManager - - currencyManager.$baseCurrency - .sink { [weak self] _ in - self?.sync() - } - .store(in: &cancellables) - - subscribe(disposeBag, appManager.willEnterForegroundObservable) { [weak self] in self?.sync() } - - sync() - } - - private func sync() { - tasks = Set() - - if case .failed = state { - state = .loading - } - - Task { [weak self, marketKit, currency] in - do { - let topPairs = try await marketKit.topPairs(currencyCode: currency.code) - self?.state = .loaded(items: topPairs, softUpdate: false, reorder: false) - } catch { - self?.state = .failed(error: error) - } - }.store(in: &tasks) - } - - func marketPair(uid: String) -> MarketPair? { - guard case let .loaded(data, _, _) = state else { - return nil - } - - return data.first { $0.uid == uid } - } -} - -extension MarketTopPairsViewModel: IMarketListService { - var statePublisher: AnyPublisher, Never> { - $state - } - - func refresh() { - sync() - } -} - -extension MarketTopPairsViewModel: IMarketListMarketPairDecoratorService { - var currency: Currency { - currencyManager.baseCurrency - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPlatforms/MarketListTopPlatformDecorator.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPlatforms/MarketListTopPlatformDecorator.swift deleted file mode 100644 index 95cc9cc9a7..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPlatforms/MarketListTopPlatformDecorator.swift +++ /dev/null @@ -1,53 +0,0 @@ -import ComponentKit -import Foundation -import MarketKit - -protocol IMarketListTopPlatformDecoratorService { - var currency: Currency { get } - var timePeriod: HsTimePeriod { get } -} - -class MarketListTopPlatformDecorator { - typealias Item = MarketKit.TopPlatform - - private let service: IMarketListTopPlatformDecoratorService - - init(service: IMarketListTopPlatformDecoratorService) { - self.service = service - } -} - -extension MarketListTopPlatformDecorator: IMarketListDecorator { - func listViewItem(item: MarketKit.TopPlatform) -> MarketModule.ListViewItem { - let currency = service.currency - - let protocols = item.protocolsCount.map { "market.top.protocols".localized(String($0)) } ?? "" - - let marketCap = item.marketCap.flatMap { ValueFormatter.instance.formatShort(currency: currency, value: $0) } ?? "n/a".localized - - let rank = item.rank - let rankDiff: Int? = rank.flatMap { rank in - item.ranks[service.timePeriod].flatMap { pastRank in - let diff = pastRank - rank - return diff == 0 ? nil : diff - } - } - let rankChange: BadgeView.Change? = rankDiff.map { $0 < 0 ? .down("\(abs($0))") : .up("\($0)") } - - let diff = item.changes[service.timePeriod] - let dataValue: MarketModule.MarketDataValue = .diff(diff) - - return MarketModule.ListViewItem( - uid: item.blockchain.uid, - iconUrl: item.blockchain.type.imageUrl, - iconShape: .square, - iconPlaceholderName: "placeholder_rectangle_32", - leftPrimaryValue: item.blockchain.name, - leftSecondaryValue: protocols, - badge: rank.map { "\($0)" }, - badgeSecondaryValue: rankChange, - rightPrimaryValue: marketCap, - rightSecondaryValue: dataValue - ) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPlatforms/MarketTopPlatformsModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPlatforms/MarketTopPlatformsModule.swift deleted file mode 100644 index 0b2988b41d..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPlatforms/MarketTopPlatformsModule.swift +++ /dev/null @@ -1,75 +0,0 @@ -import MarketKit -import ThemeKit -import UIKit - -enum MarketTopPlatformsModule { - static func viewController(timePeriod: HsTimePeriod) -> UIViewController { - let service = MarketTopPlatformsService(marketKit: App.shared.marketKit, currencyManager: App.shared.currencyManager, appManager: App.shared.appManager, timePeriod: timePeriod) - - let decorator = MarketListTopPlatformDecorator(service: service) - let viewModel = MarketTopPlatformsViewModel(service: service) - let listViewModel = MarketListViewModel(service: service, decorator: decorator) - let headerViewModel = TopPlatformsMultiSortHeaderViewModel(service: service, decorator: decorator) - - let viewController = MarketTopPlatformsViewController(viewModel: viewModel, listViewModel: listViewModel, headerViewModel: headerViewModel) - - return ThemeNavigationController(rootViewController: viewController) - } - - enum SortType: Int, CaseIterable { - case highestCap - case lowestCap - case topGainers - case topLosers - - var title: String { - switch self { - case .highestCap: return "market.top.highest_cap".localized - case .lowestCap: return "market.top.lowest_cap".localized - case .topGainers: return "market.top.top_gainers".localized - case .topLosers: return "market.top.top_losers".localized - } - } - } - - static var selectorValues: [HsTimePeriod] { - [ - HsTimePeriod.week1, - HsTimePeriod.month1, - HsTimePeriod.month3, - ] - } -} - -extension [MarketKit.TopPlatform] { - func sorted(sortType: MarketTopPlatformsModule.SortType, timePeriod: HsTimePeriod) -> [TopPlatform] { - sorted { lhsPlatform, rhsPlatform in - let lhsCap = lhsPlatform.marketCap - let rhsCap = rhsPlatform.marketCap - - let lhsChange = lhsPlatform.changes[timePeriod] - let rhsChange = rhsPlatform.changes[timePeriod] - - switch sortType { - case .highestCap, .lowestCap: - guard let lhsCap else { - return true - } - guard let rhsCap else { - return false - } - - return sortType == .highestCap ? lhsCap > rhsCap : lhsCap < rhsCap - case .topGainers, .topLosers: - guard let lhsChange else { - return true - } - guard let rhsChange else { - return false - } - - return sortType == .topGainers ? lhsChange > rhsChange : lhsChange < rhsChange - } - } - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPlatforms/MarketTopPlatformsService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPlatforms/MarketTopPlatformsService.swift deleted file mode 100644 index 0412f9d13b..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPlatforms/MarketTopPlatformsService.swift +++ /dev/null @@ -1,111 +0,0 @@ -import Combine -import Foundation -import HsExtensions -import MarketKit -import RxRelay -import RxSwift - -class MarketTopPlatformsService { - typealias Item = TopPlatform - - private let marketKit: MarketKit.Kit - private let currencyManager: CurrencyManager - private var disposeBag = DisposeBag() - private var cancellables = Set() - private var tasks = Set() - - var sortType: MarketTopPlatformsModule.SortType = .highestCap { - didSet { - syncIfPossible() - - stat(page: .topPlatforms, event: .switchSortType(sortType: sortType.statSortType)) - } - } - - var timePeriod: MarketKit.HsTimePeriod { - didSet { - syncIfPossible() - - stat(page: .topPlatforms, event: .switchPeriod(period: timePeriod.statPeriod)) - } - } - - private var internalState: MarketListServiceState = .loading - - @PostPublished private(set) var state: MarketListServiceState = .loading - - init(marketKit: MarketKit.Kit, currencyManager: CurrencyManager, appManager: IAppManager, timePeriod: HsTimePeriod) { - self.marketKit = marketKit - self.currencyManager = currencyManager - self.timePeriod = timePeriod - - currencyManager.$baseCurrency - .sink { [weak self] _ in - self?.sync() - } - .store(in: &cancellables) - - subscribe(disposeBag, appManager.willEnterForegroundObservable) { [weak self] in self?.sync() } - - sync() - } - - private func sync() { - tasks = Set() - - if case .failed = state { - internalState = .loading - } - - Task { [weak self, marketKit, currency] in - do { - let topPlatforms = try await marketKit.topPlatforms(currencyCode: currency.code) - self?.internalState = .loaded(items: topPlatforms, softUpdate: false, reorder: false) - self?.sync(topPlatforms: topPlatforms) - } catch { - self?.internalState = .failed(error: error) - } - }.store(in: &tasks) - } - - private func sync(topPlatforms: [TopPlatform], reorder: Bool = false) { - let sortType = sortType - let timePeriod = timePeriod - - state = .loaded(items: topPlatforms.sorted(sortType: sortType, timePeriod: timePeriod), softUpdate: false, reorder: reorder) - } - - private func syncIfPossible() { - guard case let .loaded(platforms, _, _) = internalState else { - return - } - - sync(topPlatforms: platforms, reorder: true) - } -} - -extension MarketTopPlatformsService { - var topPlatforms: [TopPlatform]? { - if case let .loaded(data, _, _) = state { - return data - } - - return nil - } -} - -extension MarketTopPlatformsService: IMarketListService { - var statePublisher: AnyPublisher, Never> { - $state - } - - func refresh() { - sync() - } -} - -extension MarketTopPlatformsService: IMarketListTopPlatformDecoratorService { - var currency: Currency { - currencyManager.baseCurrency - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPlatforms/MarketTopPlatformsViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPlatforms/MarketTopPlatformsViewController.swift deleted file mode 100644 index 70bd616cf4..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPlatforms/MarketTopPlatformsViewController.swift +++ /dev/null @@ -1,71 +0,0 @@ -import SectionsTableView -import SnapKit -import ThemeKit -import UIKit - -class MarketTopPlatformsViewController: MarketListViewController { - private let viewModel: MarketTopPlatformsViewModel - private let multiSortHeaderView: MarketMultiSortHeaderView - - override var viewController: UIViewController? { self } - override var headerView: UITableViewHeaderFooterView? { multiSortHeaderView } - override var refreshEnabled: Bool { false } - - init(viewModel: MarketTopPlatformsViewModel, listViewModel: IMarketListViewModel, headerViewModel: TopPlatformsMultiSortHeaderViewModel) { - self.viewModel = viewModel - multiSortHeaderView = MarketMultiSortHeaderView(viewModel: headerViewModel) - - super.init(listViewModel: listViewModel, statPage: .topPlatforms) - - multiSortHeaderView.viewController = self - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - navigationItem.largeTitleDisplayMode = .never - navigationItem.rightBarButtonItem = UIBarButtonItem(title: "button.close".localized, style: .plain, target: self, action: #selector(onTapClose)) - - tableView.registerCell(forClass: MarketHeaderCell.self) - } - - @objc private func onTapClose() { - dismiss(animated: true) - } - - override func topSections(loaded _: Bool) -> [SectionProtocol] { - [ - Section( - id: "header", - rows: [ - Row( - id: "header", - height: MarketHeaderCell.height, - bind: { cell, _ in - cell.set( - title: "top_platforms.title".localized, - description: "top_platforms.description".localized, - imageMode: .remote(imageUrl: "top_platforms".headerImageUrl) - ) - } - ), - ] - ), - ] - } - - override func onSelect(viewItem: MarketModule.ListViewItem) { - guard let uid = viewItem.uid, let topPlatform = viewModel.topPlatform(uid: uid) else { - return - } - - present(TopPlatformModule.viewController(topPlatform: topPlatform), animated: true) - - stat(page: .topPlatforms, event: .openPlatform(chainUid: uid)) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPlatforms/MarketTopPlatformsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPlatforms/MarketTopPlatformsViewModel.swift deleted file mode 100644 index a162b9f5ff..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPlatforms/MarketTopPlatformsViewModel.swift +++ /dev/null @@ -1,13 +0,0 @@ -import MarketKit - -class MarketTopPlatformsViewModel { - let service: MarketTopPlatformsService - - init(service: MarketTopPlatformsService) { - self.service = service - } - - func topPlatform(uid: String) -> TopPlatform? { - service.topPlatforms?.first { $0.blockchain.uid == uid } - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPlatforms/TopPlatformsMultiSortHeaderViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPlatforms/TopPlatformsMultiSortHeaderViewModel.swift deleted file mode 100644 index f6b9758877..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketTopPlatforms/TopPlatformsMultiSortHeaderViewModel.swift +++ /dev/null @@ -1,47 +0,0 @@ -import MarketKit - -class TopPlatformsMultiSortHeaderViewModel { - private let service: MarketTopPlatformsService - private let decorator: MarketListTopPlatformDecorator - - init(service: MarketTopPlatformsService, decorator: MarketListTopPlatformDecorator) { - self.service = service - self.decorator = decorator - } -} - -extension TopPlatformsMultiSortHeaderViewModel: IMarketMultiSortHeaderViewModel { - var sortItems: [String] { - MarketTopPlatformsModule.SortType.allCases.map(\.title) - } - - var sortIndex: Int { - MarketTopPlatformsModule.SortType.allCases.firstIndex(of: service.sortType) ?? 0 - } - - var leftSelectorItems: [String] { - [] - } - - var leftSelectorIndex: Int { - 0 - } - - var rightSelectorItems: [String] { - MarketTopPlatformsModule.selectorValues.map(\.title) - } - - var rightSelectorIndex: Int { - MarketTopPlatformsModule.selectorValues.firstIndex(of: service.timePeriod) ?? 0 - } - - func onSelectSort(index: Int) { - service.sortType = MarketTopPlatformsModule.SortType.allCases[index] - } - - func onSelectLeft(index _: Int) {} - - func onSelectRight(index: Int) { - service.timePeriod = MarketTopPlatformsModule.selectorValues[index] - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketView.swift new file mode 100644 index 0000000000..9638caf1d9 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketView.swift @@ -0,0 +1,56 @@ +import Kingfisher +import MarketKit +import SwiftUI +import ThemeKit + +struct MarketView: View { + @StateObject var searchViewModel: MarketSearchViewModel + @StateObject var globalViewModel: MarketGlobalViewModel + @StateObject var watchlistViewModel: WatchlistViewModel + + @FocusState var searchFocused: Bool + @State private var advancedSearchPresented = false + + init() { + _searchViewModel = StateObject(wrappedValue: MarketSearchViewModel()) + _globalViewModel = StateObject(wrappedValue: MarketGlobalViewModel()) + _watchlistViewModel = StateObject(wrappedValue: WatchlistViewModel(page: .markets, section: .coins)) + } + + var body: some View { + ThemeView { + VStack(spacing: 0) { + SearchBarWithCancel(text: $searchViewModel.searchText, prompt: "placeholder.search".localized, focused: $searchFocused) + + ZStack { + VStack(spacing: 0) { + MarketGlobalView(viewModel: globalViewModel) + MarketTabView(watchlistViewModel: watchlistViewModel) + } + + if searchFocused { + MarketSearchView(viewModel: searchViewModel, watchlistViewModel: watchlistViewModel) + .onFirstAppear { stat(page: .markets, event: .open(page: .marketSearch)) } + } + } + } + } + .navigationTitle("market.title".localized) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button(action: { + stat(page: .markets, event: .open(page: .advancedSearch)) + advancedSearchPresented = true + }) { + Image("manage_2_24") + .renderingMode(.template) + .foregroundColor(.themeJacob) + } + } + } + .sheet(isPresented: $advancedSearchPresented) { + MarketAdvancedSearchView(isPresented: $advancedSearchPresented) + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketViewController.swift deleted file mode 100644 index 7ac6d3139a..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketViewController.swift +++ /dev/null @@ -1,331 +0,0 @@ -import Combine -import ComponentKit -import MarketKit -import SectionsTableView -import SnapKit -import ThemeKit -import UIKit - -class MarketViewController: ThemeSearchViewController { - private let viewModel = MarketViewModel() - private var cancellables = Set() - - private let tabsView = FilterView(buttonStyle: .tab) - private let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal) - - private var marketOverviewViewController: MarketOverviewViewController? - private let postViewController: MarketPostViewController - private let watchlistViewController: MarketWatchlistViewController - - private let tableView = SectionsTableView(style: .plain) - private let notFoundPlaceholder = PlaceholderView(layoutType: .keyboard) - - private var state: MarketViewModel.State = .idle - - init() { - postViewController = MarketPostModule.viewController() - watchlistViewController = MarketWatchlistModule.viewController() - - super.init(scrollViews: [tableView], automaticallyShowsCancelButton: true) - - marketOverviewViewController = MarketOverviewModule.viewController(presentDelegate: self) - - tabBarItem = UITabBarItem(title: "market.tab_bar_item".localized, image: UIImage(named: "market_2_24"), tag: 0) - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - title = "market.title".localized - navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(named: "manage_2_24"), style: .plain, target: self, action: #selector(onTapFilter)) - - view.addSubview(tabsView) - tabsView.snp.makeConstraints { maker in - maker.top.equalTo(view.safeAreaLayoutGuide) - maker.leading.trailing.equalToSuperview() - maker.height.equalTo(FilterView.height) - } - - view.addSubview(pageViewController.view) - pageViewController.view.snp.makeConstraints { maker in - maker.top.equalTo(tabsView.snp.bottom) - maker.leading.trailing.equalToSuperview() - maker.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom) - } - - tabsView.reload(filters: MarketModule.Tab.allCases.map { - FilterView.ViewItem.item(title: $0.title) - }) - - tabsView.onSelect = { [weak self] index in - self?.onSelectTab(index: index) - } - - postViewController.parentNavigationController = navigationController - watchlistViewController.parentNavigationController = navigationController - - view.addSubview(tableView) - tableView.snp.makeConstraints { maker in - maker.leading.top.trailing.equalToSuperview() - maker.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom) - } - - tableView.sectionDataSource = self - tableView.registerHeaderFooter(forClass: TransactionDateHeaderView.self) - - tableView.sectionHeaderTopPadding = 0 - tableView.backgroundColor = .themeTyler - tableView.separatorStyle = .none - - view.addSubview(notFoundPlaceholder) - notFoundPlaceholder.snp.makeConstraints { maker in - maker.edges.equalTo(view.safeAreaLayoutGuide) - } - - notFoundPlaceholder.image = UIImage(named: "not_found_48") - notFoundPlaceholder.text = "market_discovery.not_found".localized - - viewModel.$currentTab - .sink { [weak self] currentTab in - self?.tabsView.select(index: MarketModule.Tab.allCases.firstIndex(of: currentTab) ?? 0) - self?.setViewPager(tab: currentTab) - } - .store(in: &cancellables) - - viewModel.$state - .sink { [weak self] in self?.sync(state: $0) } - .store(in: &cancellables) - - viewModel.favoritedPublisher - .sink { HudHelper.instance.show(banner: .addedToWatchlist) } - .store(in: &cancellables) - - viewModel.unfavoritedPublisher - .sink { HudHelper.instance.show(banner: .removedFromWatchlist) } - .store(in: &cancellables) - - $filter - .receive(on: DispatchQueue.main) - .sink { [weak self] in self?.sync(filter: $0) } - .store(in: &cancellables) - - sync(state: viewModel.state) - } - - override func didPresentSearch() { - super.didPresentSearch() - - stat(page: .markets, event: .open(page: .marketSearch)) - } - - private func sync(state: MarketViewModel.State) { - self.state = state - - switch state { - case .idle: - tableView.isHidden = true - notFoundPlaceholder.isHidden = true - case .placeholder: - tableView.reload() - tableView.setContentOffset(CGPoint(x: 0, y: -tableView.adjustedContentInset.top), animated: false) - tableView.isHidden = false - notFoundPlaceholder.isHidden = true - case let .searchResults(fullCoins): - tableView.reload() - tableView.isHidden = false - notFoundPlaceholder.isHidden = !fullCoins.isEmpty - } - } - - private func sync(filter: String?) { - viewModel.onUpdate(searchActive: searchController.isActive, filter: filter ?? "") - } - - private func onSelectTab(index: Int) { - guard index < MarketModule.Tab.allCases.count else { - return - } - - let tab = MarketModule.Tab.allCases[index] - - viewModel.currentTab = tab - - stat(page: .markets, event: .switchTab(tab: tab.statTab)) - } - - private func setViewPager(tab: MarketModule.Tab) { - pageViewController.setViewControllers([viewController(tab: tab)], direction: .forward, animated: false) - } - - private func viewController(tab: MarketModule.Tab) -> UIViewController { - switch tab { - case .overview: return marketOverviewViewController ?? UIViewController() - case .posts: return postViewController - case .watchlist: return watchlistViewController - } - } - - @objc private func onTapFilter() { - let viewController = MarketAdvancedSearchModule.viewController() - present(ThemeNavigationController(rootViewController: viewController), animated: true) - - stat(page: .markets, event: .open(page: .advancedSearch)) - } - - func willPresentSearchController(_: UISearchController) { - viewModel.onUpdate(searchActive: true, filter: filter ?? "") - } - - func willDismissSearchController(_: UISearchController) { - viewModel.onUpdate(searchActive: false, filter: filter ?? "") - } -} - -extension MarketViewController: SectionsDataSource { - private func onSelect(fullCoin: FullCoin, statSection: StatSection) { - let coinUid = fullCoin.coin.uid - - guard let module = CoinPageModule.viewController(coinUid: coinUid) else { - return - } - - DispatchQueue.global().async { [weak self] in - self?.viewModel.handleOpen(coinUid: coinUid) - } - - present(module, animated: true) - - stat(page: .marketSearch, section: statSection, event: .openCoin(coinUid: coinUid)) - } - - private func rowActions(coinUid: String) -> [RowAction] { - let type: RowActionType - let iconName: String - let action: (UITableViewCell?) -> Void - - if viewModel.isFavorite(coinUid: coinUid) { - type = .destructive - iconName = "star_off_24" - action = { [weak self] _ in - self?.viewModel.unfavorite(coinUid: coinUid) - } - } else { - type = .additive - iconName = "star_24" - action = { [weak self] _ in - self?.viewModel.favorite(coinUid: coinUid) - } - } - - return [ - RowAction( - pattern: .icon(image: UIImage(named: iconName)?.withTintColor(type.iconColor), background: type.backgroundColor), - action: action - ), - ] - } - - private func rows(fullCoins: [FullCoin], statSection: StatSection) -> [RowProtocol] { - fullCoins.enumerated().map { index, fullCoin in - let coin = fullCoin.coin - let isLast = index == fullCoins.count - 1 - - return CellBuilderNew.row( - rootElement: .hStack([ - .image32 { component in - component.setImage(urlString: coin.imageUrl, placeholder: UIImage(named: "placeholder_circle_32")) - }, - .vStackCentered([ - .text { component in - component.font = .body - component.textColor = .themeLeah - component.text = coin.code - }, - .margin(3), - .text { component in - component.font = .subhead2 - component.textColor = .themeGray - component.text = coin.name - }, - ]), - ]), - tableView: tableView, - id: "coin_\(coin.uid)", - height: .heightDoubleLineCell, - autoDeselect: true, - rowActionProvider: { [weak self] in self?.rowActions(coinUid: coin.uid) ?? [] }, - bind: { cell in - cell.set(backgroundStyle: .transparent, isLast: isLast) - }, - action: { [weak self] in - self?.onSelect(fullCoin: fullCoin, statSection: statSection) - } - ) - } - } - - func buildSections() -> [SectionProtocol] { - switch state { - case .idle: - return [] - case let .placeholder(recentFullCoins, popularFullCoins): - var sections = [SectionProtocol]() - - if !recentFullCoins.isEmpty { - sections.append( - Section( - id: "recent", - headerState: .cellType( - hash: "recent", - binder: { (view: TransactionDateHeaderView) in - view.text = "market.search.recent".localized - }, - dynamicHeight: { _ in .heightSingleLineCell } - ), - rows: rows(fullCoins: recentFullCoins, statSection: .recent) - ) - ) - } - - if !popularFullCoins.isEmpty { - sections.append( - Section( - id: "popular", - headerState: .cellType( - hash: "popular", - binder: { (view: TransactionDateHeaderView) in - view.text = "market.search.popular".localized - }, - dynamicHeight: { _ in .heightSingleLineCell } - ), - rows: rows(fullCoins: popularFullCoins, statSection: .popular) - ) - ) - } - - return sections - case let .searchResults(fullCoins): - return [ - Section( - id: "coins", - rows: rows(fullCoins: fullCoins, statSection: .searchResults) - ), - ] - } - } -} - -extension MarketViewController: IPresentDelegate { - func present(viewController: UIViewController) { - navigationController?.present(viewController, animated: true) - } - - func push(viewController: UIViewController) { - navigationController?.pushViewController(viewController, animated: true) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketViewModel.swift deleted file mode 100644 index 2576223a78..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketViewModel.swift +++ /dev/null @@ -1,115 +0,0 @@ -import Combine -import MarketKit - -class MarketViewModel { - private let keyTab = "market-tab" - private let keyRecentCoinUids = "market-recent-coin-uids" - - private let userDefaultsStorage = App.shared.userDefaultsStorage - private let launchScreenManager = App.shared.launchScreenManager - private let marketKit = App.shared.marketKit - private let favoritesManager = App.shared.favoritesManager - - @Published var currentTab: MarketModule.Tab { - didSet { - userDefaultsStorage.set(value: currentTab.rawValue, for: keyTab) - } - } - - @Published private(set) var state: State = .idle - - private var recentCoinUids: [String] { - didSet { - userDefaultsStorage.set(value: recentCoinUids.joined(separator: ","), for: keyRecentCoinUids) - } - } - - private let favoritedSubject = PassthroughSubject() - private let unfavoritedSubject = PassthroughSubject() - - init() { - let currentTab: MarketModule.Tab - - switch launchScreenManager.launchScreen { - case .auto: - if let storedValue: String = userDefaultsStorage.value(for: keyTab), let storedTab = MarketModule.Tab(rawValue: storedValue) { - currentTab = storedTab - } else { - currentTab = .overview - } - case .balance, .marketOverview: - currentTab = .overview - case .watchlist: - currentTab = .watchlist - } - - self.currentTab = currentTab - - let recentCoinsUidsRaw: String = userDefaultsStorage.value(for: keyRecentCoinUids) ?? "" - recentCoinUids = recentCoinsUidsRaw.components(separatedBy: ",") - } -} - -extension MarketViewModel { - var favoritedPublisher: AnyPublisher { - favoritedSubject.eraseToAnyPublisher() - } - - var unfavoritedPublisher: AnyPublisher { - unfavoritedSubject.eraseToAnyPublisher() - } - - func onUpdate(searchActive: Bool, filter: String) { - if searchActive { - if filter.isEmpty { - let recentMarketFullCoins = (try? marketKit.fullCoins(coinUids: recentCoinUids)) ?? [] - let recentFullCoins = recentCoinUids.compactMap { coinUid in recentMarketFullCoins.first { $0.coin.uid == coinUid } } - - let popularFullCoins = (try? marketKit.topFullCoins()) ?? [] - - state = .placeholder(recentFullCoins: recentFullCoins, popularFullCoins: popularFullCoins) - } else { - state = .searchResults(fullCoins: (try? marketKit.fullCoins(filter: filter)) ?? []) - } - } else { - state = .idle - } - } - - func isFavorite(coinUid: String) -> Bool { - favoritesManager.isFavorite(coinUid: coinUid) - } - - func favorite(coinUid: String) { - favoritesManager.add(coinUid: coinUid) - favoritedSubject.send() - - stat(page: .marketSearch, event: .addToWatchlist(coinUid: coinUid)) - } - - func unfavorite(coinUid: String) { - favoritesManager.remove(coinUid: coinUid) - unfavoritedSubject.send() - - stat(page: .marketSearch, event: .removeFromWatchlist(coinUid: coinUid)) - } - - func handleOpen(coinUid: String) { - var recentCoinUids = recentCoinUids - - if let index = recentCoinUids.firstIndex(of: coinUid) { - recentCoinUids.remove(at: index) - } - - recentCoinUids.insert(coinUid, at: 0) - self.recentCoinUids = Array(recentCoinUids.prefix(5)) - } -} - -extension MarketViewModel { - enum State { - case idle - case placeholder(recentFullCoins: [FullCoin], popularFullCoins: [FullCoin]) - case searchResults(fullCoins: [FullCoin]) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketWatchlist/MarketWatchlistDecorator.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketWatchlist/MarketWatchlistDecorator.swift deleted file mode 100644 index 9de69327d3..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketWatchlist/MarketWatchlistDecorator.swift +++ /dev/null @@ -1,57 +0,0 @@ -import MarketKit - -class MarketWatchlistDecorator { - typealias Item = MarketInfo - - private let service: IMarketListDecoratorService - - var priceChangeType: MarketModule.PriceChangeType - - init(service: IMarketListDecoratorService) { - self.service = service - - priceChangeType = MarketModule.PriceChangeType.sortingTypes.at(index: service.initialIndex) ?? .day - } -} - -extension MarketWatchlistDecorator: IMarketSingleSortHeaderDecorator { - var allFields: [String] { - MarketModule.PriceChangeType.sortingTypes.map(\.shortTitle) - } - - var currentFieldIndex: Int { - MarketModule.PriceChangeType.sortingTypes.firstIndex(of: priceChangeType) ?? 0 - } - - func setCurrentField(index: Int) { - priceChangeType = MarketModule.PriceChangeType.sortingTypes.at(index: index) ?? .day - service.onUpdate(index: index) - - stat(page: .watchlist, event: .switchPeriod(period: priceChangeType.statPeriod)) - } -} - -extension MarketWatchlistDecorator: IMarketListDecorator { - func listViewItem(item marketInfo: MarketInfo) -> MarketModule.ListViewItem { - let currency = service.currency - - let price = marketInfo.price.flatMap { ValueFormatter.instance.formatFull(currency: currency, value: $0) } ?? "n/a".localized - - let dataValue: MarketModule.MarketDataValue - - dataValue = .diff(marketInfo.priceChangeValue(type: priceChangeType)) - - return MarketModule.ListViewItem( - uid: marketInfo.fullCoin.coin.uid, - iconUrl: marketInfo.fullCoin.coin.imageUrl, - iconShape: .full, - iconPlaceholderName: "placeholder_circle_32", - leftPrimaryValue: marketInfo.fullCoin.coin.code, - leftSecondaryValue: marketInfo.fullCoin.coin.name, - badge: marketInfo.marketCapRank.map { "\($0)" }, - badgeSecondaryValue: nil, - rightPrimaryValue: price, - rightSecondaryValue: dataValue - ) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketWatchlist/MarketWatchlistModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketWatchlist/MarketWatchlistModule.swift deleted file mode 100644 index 20227bfaec..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketWatchlist/MarketWatchlistModule.swift +++ /dev/null @@ -1,25 +0,0 @@ -import UIKit - -enum MarketWatchlistModule { - static func viewController() -> MarketWatchlistViewController { - let service = MarketWatchlistService( - marketKit: App.shared.marketKit, - currencyManager: App.shared.currencyManager, - favoritesManager: App.shared.favoritesManager, - appManager: App.shared.appManager, - userDefaultsStorage: App.shared.userDefaultsStorage - ) - let watchlistToggleService = MarketWatchlistToggleService( - coinUidService: service, - favoritesManager: App.shared.favoritesManager, - statPage: .watchlist - ) - - let decorator = MarketWatchlistDecorator(service: service) - let viewModel = MarketWatchlistViewModel(service: service) - let headerViewModel = MarketSingleSortHeaderViewModel(service: service, decorator: decorator) - let listViewModel = MarketListWatchViewModel(service: service, watchlistToggleService: watchlistToggleService, decorator: decorator) - - return MarketWatchlistViewController(viewModel: viewModel, listViewModel: listViewModel, headerViewModel: headerViewModel) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketWatchlist/MarketWatchlistService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketWatchlist/MarketWatchlistService.swift deleted file mode 100644 index a05b1f1580..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketWatchlist/MarketWatchlistService.swift +++ /dev/null @@ -1,150 +0,0 @@ -import Combine -import HsExtensions -import MarketKit -import RxRelay -import RxSwift - -class MarketWatchlistService: IMarketSingleSortHeaderService { - typealias Item = MarketInfo - - private let keySortDirectionField = "market-watchlist-sort-direction-field" - private let keyPriceChangeField = "market-watchlist-price-change-field" - - private let marketKit: MarketKit.Kit - private let currencyManager: CurrencyManager - private let favoritesManager: FavoritesManager - private let appManager: IAppManager - private let userDefaultsStorage: UserDefaultsStorage - private let disposeBag = DisposeBag() - private var cancellables = Set() - private var tasks = Set() - - @PostPublished private(set) var state: MarketListServiceState = .loading - - private var coinUids = [String]() - - var sortDirectionAscending: Bool { - didSet { - userDefaultsStorage.set(value: sortDirectionAscending, for: keySortDirectionField) - syncIfPossible() - - stat(page: .watchlist, event: .toggleSortDirection) - } - } - - init(marketKit: MarketKit.Kit, currencyManager: CurrencyManager, favoritesManager: FavoritesManager, appManager: IAppManager, userDefaultsStorage: UserDefaultsStorage) { - self.marketKit = marketKit - self.currencyManager = currencyManager - self.favoritesManager = favoritesManager - self.appManager = appManager - self.userDefaultsStorage = userDefaultsStorage - - sortDirectionAscending = userDefaultsStorage.value(for: keySortDirectionField) ?? false - } - - private func syncCoinUids() { - coinUids = favoritesManager.allCoinUids - - if case let .loaded(marketInfos, _, _) = state { - let newMarketInfos = marketInfos.filter { marketInfo in - coinUids.contains(marketInfo.fullCoin.coin.uid) - } - - if newMarketInfos.count == coinUids.count { - state = .loaded(items: newMarketInfos, softUpdate: true, reorder: false) - return - } - } - - syncMarketInfos() - } - - private func syncMarketInfos() { - tasks = Set() - - if coinUids.isEmpty { - state = .loaded(items: [], softUpdate: false, reorder: false) - return - } - - if case .failed = state { - state = .loading - } - - Task { [weak self, marketKit, coinUids, currency] in - do { - let marketInfos = try await marketKit.marketInfos(coinUids: coinUids, currencyCode: currency.code) - self?.sync(marketInfos: marketInfos) - } catch { - self?.state = .failed(error: error) - } - }.store(in: &tasks) - } - - private func sync(marketInfos: [MarketInfo], reorder: Bool = false) { - let sortingField: MarketModule.SortingField = sortDirectionAscending ? .topLosers : .topGainers - state = .loaded(items: marketInfos.sorted(sortingField: sortingField, priceChangeType: priceChangeType), softUpdate: false, reorder: reorder) - } - - private func syncIfPossible() { - guard case let .loaded(marketInfos, _, _) = state else { - return - } - - sync(marketInfos: marketInfos, reorder: true) - } -} - -extension MarketWatchlistService: IMarketListService { - var statePublisher: AnyPublisher, Never> { - $state - } - - func load() { - currencyManager.$baseCurrency - .sink { [weak self] _ in - self?.syncMarketInfos() - } - .store(in: &cancellables) - - subscribe(disposeBag, favoritesManager.coinUidsUpdatedObservable) { [weak self] in self?.syncCoinUids() } - subscribe(disposeBag, appManager.willEnterForegroundObservable) { [weak self] in self?.syncMarketInfos() } - - syncCoinUids() - } - - func refresh() { - syncMarketInfos() - stat(page: .watchlist, event: .refresh) - } -} - -extension MarketWatchlistService: IMarketListCoinUidService { - func coinUid(index: Int) -> String? { - guard case let .loaded(marketInfos, _, _) = state, index < marketInfos.count else { - return nil - } - - return marketInfos[index].fullCoin.coin.uid - } -} - -extension MarketWatchlistService: IMarketListDecoratorService { - var initialIndex: Int { - userDefaultsStorage.value(for: keyPriceChangeField) ?? 0 - } - - var currency: Currency { - currencyManager.baseCurrency - } - - var priceChangeType: MarketModule.PriceChangeType { - MarketModule.PriceChangeType.sortingTypes.at(index: initialIndex) ?? .day - } - - func onUpdate(index: Int) { - userDefaultsStorage.set(value: index, for: keyPriceChangeField) - - syncIfPossible() - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketWatchlist/MarketWatchlistViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketWatchlist/MarketWatchlistViewController.swift deleted file mode 100644 index cf646b41c0..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketWatchlist/MarketWatchlistViewController.swift +++ /dev/null @@ -1,42 +0,0 @@ -import SectionsTableView -import SnapKit -import ThemeKit -import UIKit - -class MarketWatchlistViewController: MarketListViewController { - weak var parentNavigationController: UINavigationController? - - private let viewModel: MarketWatchlistViewModel - - private let singleSortHeaderView: MarketSingleSortHeaderView - private let placeholderView = PlaceholderView() - - override var viewController: UIViewController? { parentNavigationController } - override var headerView: UITableViewHeaderFooterView? { singleSortHeaderView } - override var emptyView: UIView? { placeholderView } - - init(viewModel: MarketWatchlistViewModel, listViewModel: IMarketListViewModel, headerViewModel: MarketSingleSortHeaderViewModel) { - self.viewModel = viewModel - singleSortHeaderView = MarketSingleSortHeaderView(viewModel: headerViewModel, hasTopSeparator: false) - - super.init(listViewModel: listViewModel, statPage: .watchlist) - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - placeholderView.image = UIImage(named: "rate_48") - placeholderView.text = "market_watchlist.empty.caption".localized - - viewModel.onLoad() - } - - override func showAddedToWatchlist() {} - - override func showRemovedFromWatchlist() {} -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketWatchlist/MarketWatchlistViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketWatchlist/MarketWatchlistViewModel.swift deleted file mode 100644 index f609521b6b..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketWatchlist/MarketWatchlistViewModel.swift +++ /dev/null @@ -1,13 +0,0 @@ -class MarketWatchlistViewModel { - private let service: MarketWatchlistService - - init(service: MarketWatchlistService) { - self.service = service - } -} - -extension MarketWatchlistViewModel { - func onLoad() { - service.load() - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/News/MarketNewsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/News/MarketNewsView.swift new file mode 100644 index 0000000000..1a62817382 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/News/MarketNewsView.swift @@ -0,0 +1,115 @@ +import MarketKit +import SwiftUI + +struct MarketNewsView: View { + @ObservedObject var viewModel: MarketNewsViewModel + + var body: some View { + ThemeView { + switch viewModel.state { + case .loading: + loadingList() + case let .loaded(posts): + list(posts: posts) + case .failed: + SyncErrorView { + Task { + await viewModel.refresh() + } + } + } + } + } + + @ViewBuilder private func list(posts: [Post]) -> some View { + ScrollView { + LazyVStack(spacing: .margin12) { + ForEach(posts.indices, id: \.self) { index in + let post = posts[index] + + ListSection { + ClickableRow( + padding: EdgeInsets(top: .margin16, leading: .margin16, bottom: .margin16, trailing: .margin16), + action: { + UrlManager.open(url: post.url) + stat(page: .markets, section: .news, event: .open(page: .externalNews)) + } + ) { + itemContent( + source: post.source, + title: post.title, + body: post.body, + ago: timeAgo(interval: Date().timeIntervalSince1970 - post.timestamp) + ) + } + } + } + } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) + } + .refreshable { + await viewModel.refresh() + } + } + + @ViewBuilder private func loadingList() -> some View { + ScrollView { + LazyVStack(spacing: .margin12) { + ForEach(0 ... 5, id: \.self) { _ in + ListSection { + ListRow(padding: EdgeInsets(top: .margin16, leading: .margin16, bottom: .margin16, trailing: .margin16)) { + itemContent( + source: "Post Source", + title: "Post title post title post title post title post title post title", + body: "Post body post body post body post body post body post body post body post body post body post body post body post body post body post body post body post body", + ago: "1h ago" + ) + .redacted() + } + } + } + } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) + } + .simultaneousGesture(DragGesture(minimumDistance: 0), including: .all) + } + + @ViewBuilder private func itemContent(source: String, title: String, body: String, ago: String) -> some View { + VStack(alignment: .leading, spacing: .margin12) { + VStack(alignment: .leading, spacing: .margin8) { + Text(source).themeCaptionSB() + + VStack(alignment: .leading, spacing: .margin6) { + Text(title) + .themeHeadline2() + .lineLimit(3) + + Text(body) + .themeSubhead2() + .lineLimit(2) + } + } + + Text(ago).themeMicro(color: .themeGray50) + } + } + + private func timeAgo(interval: TimeInterval) -> String { + var interval = Int(interval) / 60 + + // interval from post in minutes + if interval < 60 { + return "timestamp.min_ago".localized(max(1, interval)) + } + + // interval in hours + interval /= 60 + if interval < 24 { + return "timestamp.hours_ago".localized(interval) + } + + // interval in days + interval /= 24 + return "timestamp.days_ago".localized(interval) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/News/MarketNewsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/News/MarketNewsViewModel.swift new file mode 100644 index 0000000000..8fa6966f12 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/News/MarketNewsViewModel.swift @@ -0,0 +1,65 @@ +import Combine +import Foundation +import HsExtensions +import MarketKit + +class MarketNewsViewModel: ObservableObject { + private let marketKit = App.shared.marketKit + private let appManager = App.shared.appManager + private var cancellables = Set() + private var tasks = Set() + + @Published var state: State = .loading + + private func sync() { + tasks = Set() + + Task { [weak self] in + await self?._sync() + } + .store(in: &tasks) + } + + private func _sync() async { + if case .failed = state { + await MainActor.run { [weak self] in + self?.state = .loading + } + } + + do { + let posts = try await marketKit.posts() + + await MainActor.run { [weak self] in + self?.state = .loaded(posts: posts) + } + } catch { + await MainActor.run { [weak self] in + self?.state = .failed(error: error) + } + } + } +} + +extension MarketNewsViewModel { + func load() { + appManager.willEnterForegroundPublisher + .sink { [weak self] in self?.sync() } + .store(in: &cancellables) + + sync() + } + + func refresh() async { + await _sync() + stat(page: .markets, section: .news, event: .refresh) + } +} + +extension MarketNewsViewModel { + enum State { + case loading + case loaded(posts: [Post]) + case failed(error: Error) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Pairs/MarketPairsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Pairs/MarketPairsView.swift new file mode 100644 index 0000000000..577e041874 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Pairs/MarketPairsView.swift @@ -0,0 +1,149 @@ +import Kingfisher +import MarketKit +import SwiftUI + +struct MarketPairsView: View { + @ObservedObject var viewModel: MarketPairsViewModel + + var body: some View { + ThemeView { + switch viewModel.state { + case .loading: + VStack(spacing: 0) { + header(disabled: true) + loadingList() + } + case let .loaded(pairs): + VStack(spacing: 0) { + header() + list(pairs: pairs) + } + case .failed: + SyncErrorView { + Task { + await viewModel.refresh() + } + } + } + } + } + + @ViewBuilder private func header(disabled: Bool = false) -> some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + Button(action: { + viewModel.volumeSortOrder.toggle() + }) { + Text("market.pairs.volume".localized) + } + .buttonStyle(SecondaryButtonStyle(style: .default, rightAccessory: .custom(image: volumeSortIcon()))) + .disabled(disabled) + } + .padding(.horizontal, .margin16) + .padding(.vertical, .margin8) + } + } + + @ViewBuilder private func list(pairs: [MarketPair]) -> some View { + ScrollViewReader { proxy in + ThemeList(pairs, invisibleTopView: true) { pair in + ClickableRow(action: { + if let tradeUrl = pair.tradeUrl { + UrlManager.open(url: tradeUrl) + stat(page: .markets, section: .pairs, event: .open(page: .externalMarketPair)) + } + }) { + itemContent( + baseCoin: pair.baseCoin, + targetCoin: pair.targetCoin, + base: pair.base, + target: pair.target, + volume: pair.volume.flatMap { ValueFormatter.instance.formatShort(currency: viewModel.currency, value: $0) } ?? "n/a".localized, + marketName: pair.marketName, + rank: pair.rank, + price: pair.price.flatMap { ValueFormatter.instance.formatShort(value: $0, decimalCount: 8, symbol: pair.target) } ?? "n/a".localized + ) + } + } + .themeListStyle(.transparent) + .refreshable { + await viewModel.refresh() + } + .onChange(of: viewModel.volumeSortOrder) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + } + } + + @ViewBuilder private func loadingList() -> some View { + ThemeList(Array(0 ... 10)) { _ in + ListRow { + itemContent( + baseCoin: nil, + targetCoin: nil, + base: "CODE", + target: "CODE", + volume: "$123.4 B", + marketName: "Market Name", + rank: 12, + price: "123 CODE" + ) + .redacted() + } + } + .themeListStyle(.transparent) + .simultaneousGesture(DragGesture(minimumDistance: 0), including: .all) + } + + @ViewBuilder private func itemContent(baseCoin: Coin?, targetCoin: Coin?, base: String, target: String, volume: String, marketName: String, rank: Int, price: String) -> some View { + ZStack(alignment: .leading) { + HStack { + Spacer() + icon(coin: targetCoin, ticker: target) + } + + icon(coin: baseCoin, ticker: base) + } + .frame(width: 52) + + VStack(spacing: 1) { + HStack(spacing: .margin8) { + Text("\(base) / \(target)").textBody() + Spacer() + Text(volume).textBody() + } + + HStack(spacing: .margin8) { + HStack(spacing: .margin4) { + BadgeViewNew(text: "\(rank)") + Text(marketName).textSubhead2() + } + Spacer() + Text(price).textSubhead2() + } + } + } + + @ViewBuilder private func icon(coin: Coin?, ticker: String) -> some View { + ZStack { + Circle() + .fill(Color.themeTyler) + .frame(width: .iconSize32, height: .iconSize32) + + if let coin { + CoinIconView(coin: coin) + } else { + KFImage.url(URL(string: ticker.fiatImageUrl)) + .resizable() + .placeholder { Circle().fill(Color.themeSteel20) } + .clipShape(Circle()) + .frame(width: .iconSize32, height: .iconSize32) + } + } + } + + private func volumeSortIcon() -> Image { + switch viewModel.volumeSortOrder { + case .asc: return Image("arrow_medium_2_up_20") + case .desc: return Image("arrow_medium_2_down_20") + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Pairs/MarketPairsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Pairs/MarketPairsViewModel.swift new file mode 100644 index 0000000000..6f5f7845d1 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Pairs/MarketPairsViewModel.swift @@ -0,0 +1,113 @@ +import Combine +import Foundation +import HsExtensions +import MarketKit + +class MarketPairsViewModel: ObservableObject { + private let marketKit = App.shared.marketKit + private let currencyManager = App.shared.currencyManager + private let appManager = App.shared.appManager + private var cancellables = Set() + private var tasks = Set() + + private var internalState: State = .loading { + didSet { + syncState() + } + } + + @Published var state: State = .loading + + var volumeSortOrder: MarketModule.SortOrder = .desc { + didSet { + stat(page: .markets, section: .pairs, event: .switchSortType(sortType: volumeSortOrder.statVolumeSortType)) + syncState() + } + } + + private func sync() { + tasks = Set() + + Task { [weak self] in + await self?._sync() + } + .store(in: &tasks) + } + + private func _sync() async { + if case .failed = state { + await MainActor.run { [weak self] in + self?.internalState = .loading + } + } + + do { + let pairs = try await marketKit.topPairs(currencyCode: currency.code) + + await MainActor.run { [weak self] in + self?.internalState = .loaded(pairs: pairs) + } + } catch { + await MainActor.run { [weak self] in + self?.internalState = .failed(error: error) + } + } + } + + private func syncState() { + switch internalState { + case .loading: + state = .loading + case let .loaded(pairs): + state = .loaded(pairs: pairs.sorted(volumeSortOrder: volumeSortOrder)) + case let .failed(error): + state = .failed(error: error) + } + } +} + +extension MarketPairsViewModel { + var currency: Currency { + currencyManager.baseCurrency + } + + func load() { + currencyManager.$baseCurrency + .sink { [weak self] _ in + self?.sync() + } + .store(in: &cancellables) + + appManager.willEnterForegroundPublisher + .sink { [weak self] in self?.sync() } + .store(in: &cancellables) + + sync() + } + + func refresh() async { + await _sync() + } +} + +extension MarketPairsViewModel { + enum State { + case loading + case loaded(pairs: [MarketPair]) + case failed(error: Error) + } +} + +extension [MarketPair] { + func sorted(volumeSortOrder: MarketModule.SortOrder) -> [MarketPair] { + sorted { lhsPair, rhsPair in + let lhsVolume = lhsPair.volume ?? 0 + let rhsVolume = rhsPair.volume ?? 0 + + switch volumeSortOrder { + case .asc: return lhsVolume < rhsVolume + case .desc: return lhsVolume > rhsVolume + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Platform/MarketPlatformViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Platform/MarketPlatformViewModel.swift new file mode 100644 index 0000000000..b1c8ca8961 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Platform/MarketPlatformViewModel.swift @@ -0,0 +1,94 @@ +import Combine +import Foundation +import HsExtensions +import MarketKit + +class MarketPlatformViewModel: ObservableObject { + private let marketKit = App.shared.marketKit + private let currencyManager = App.shared.currencyManager + + let platform: TopPlatform + + private var cancellables = Set() + private var tasks = Set() + + private var internalState: State = .loading { + didSet { + syncState() + } + } + + @Published var state: State = .loading + + var sortBy: MarketModule.SortBy = .highestCap { + didSet { + stat(page: .topPlatform, event: .switchSortType(sortType: sortBy.statSortType)) + syncState() + } + } + + init(platform: TopPlatform) { + self.platform = platform + currencyManager.$baseCurrency + .sink { [weak self] _ in + self?.sync() + } + .store(in: &cancellables) + + sync() + } + + private func syncState() { + switch internalState { + case .loading: + state = .loading + case let .loaded(marketInfos): + state = .loaded(marketInfos: marketInfos.sorted(sortBy: sortBy, timePeriod: .day1)) + case let .failed(error): + state = .failed(error: error) + } + } +} + +extension MarketPlatformViewModel { + var currency: Currency { + currencyManager.baseCurrency + } + + var sortBys: [MarketModule.SortBy] { + [.highestCap, .lowestCap, .gainers, .losers] + } + + func sync() { + tasks = Set() + + if case .failed = internalState { + internalState = .loading + } + + let platform = platform + + Task { [weak self, marketKit, currency] in + do { + let marketInfos = try await marketKit.topPlatformMarketInfos(blockchain: platform.blockchain.uid, currencyCode: currency.code) + + await MainActor.run { [weak self] in + self?.internalState = .loaded(marketInfos: marketInfos) + } + } catch { + await MainActor.run { [weak self] in + self?.internalState = .failed(error: error) + } + } + } + .store(in: &tasks) + } +} + +extension MarketPlatformViewModel { + enum State { + case loading + case loaded(marketInfos: [MarketInfo]) + case failed(error: Error) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Platform/MarketPlatformViewNew.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Platform/MarketPlatformViewNew.swift new file mode 100644 index 0000000000..ff7bd08ea5 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Platform/MarketPlatformViewNew.swift @@ -0,0 +1,197 @@ +import Kingfisher +import MarketKit +import SwiftUI + +struct MarketPlatformViewNew: View { + @StateObject var viewModel: MarketPlatformViewModel + @StateObject var chartViewModel: MetricChartViewModel + @StateObject var watchlistViewModel: WatchlistViewModel + @Binding var isPresented: Bool + + @State private var sortBySelectorPresented = false + @State private var presentedFullCoin: FullCoin? + + init(isPresented: Binding, platform: TopPlatform) { + _viewModel = StateObject(wrappedValue: MarketPlatformViewModel(platform: platform)) + _chartViewModel = StateObject(wrappedValue: MetricChartViewModel.platformInstance(platform: platform)) + _watchlistViewModel = StateObject(wrappedValue: WatchlistViewModel(page: .topPlatform)) + _isPresented = isPresented + } + + var body: some View { + ThemeNavigationView { + ThemeView { + switch viewModel.state { + case .loading: + VStack(spacing: 0) { + header() + Spacer() + ProgressView() + Spacer() + } + case let .loaded(marketInfos): + ScrollViewReader { proxy in + ThemeList(bottomSpacing: .margin16, invisibleTopView: true) { + header() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + chart() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + + list(marketInfos: marketInfos) + } + .onChange(of: viewModel.sortBy) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + } + case .failed: + VStack(spacing: 0) { + header() + + SyncErrorView { + viewModel.sync() + } + } + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("button.close".localized) { + isPresented = false + } + } + } + .sheet(item: $presentedFullCoin) { fullCoin in + CoinPageViewNew(coinUid: fullCoin.coin.uid).ignoresSafeArea() + .onFirstAppear { stat(page: .globalMetricsTvlInDefi, event: .openCoin(coinUid: fullCoin.coin.uid)) } + } + } + } + + @ViewBuilder private func header() -> some View { + HStack(spacing: .margin32) { + VStack(spacing: .margin8) { + Text("top_platform.title".localized(viewModel.platform.blockchain.name)).themeHeadline1() + Text("top_platform.description".localized(viewModel.platform.blockchain.name)).themeSubhead2() + } + .padding(.vertical, .margin12) + + KFImage.url(URL(string: viewModel.platform.blockchain.type.imageUrl)) + .resizable() + .frame(width: .iconSize32, height: .iconSize32) + } + .padding(.leading, .margin16) + .padding(.trailing, .margin16) + } + + @ViewBuilder private func chart() -> some View { + ChartView(viewModel: chartViewModel, configuration: .marketCapChart) + .frame(maxWidth: .infinity) + .onFirstAppear { + chartViewModel.start() + } + } + + @ViewBuilder private func listHeader(disabled: Bool = false) -> some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + Button(action: { + sortBySelectorPresented = true + }) { + Text(viewModel.sortBy.title) + } + .buttonStyle(SecondaryButtonStyle(style: .default, rightAccessory: .dropDown)) + .disabled(disabled) + } + .padding(.horizontal, .margin16) + .padding(.vertical, .margin8) + } + .alert( + isPresented: $sortBySelectorPresented, + title: "market.sort_by.title".localized, + viewItems: viewModel.sortBys.map { .init(text: $0.title, selected: viewModel.sortBy == $0) }, + onTap: { index in + guard let index else { + return + } + + viewModel.sortBy = viewModel.sortBys[index] + } + ) + } + + @ViewBuilder private func list(marketInfos: [MarketInfo]) -> some View { + Section { + ListForEach(marketInfos) { marketInfo in + let coin = marketInfo.fullCoin.coin + + ClickableRow(action: { + presentedFullCoin = marketInfo.fullCoin + }) { + itemContent( + coin: coin, + marketCap: marketInfo.marketCap, + price: marketInfo.price.flatMap { ValueFormatter.instance.formatFull(currency: viewModel.currency, value: $0) } ?? "n/a".localized, + rank: marketInfo.marketCapRank, + diff: marketInfo.priceChange24h + ) + } + .watchlistSwipeActions(viewModel: watchlistViewModel, coinUid: coin.uid) + } + } header: { + listHeader() + .listRowInsets(EdgeInsets()) + .background(Color.themeTyler) + } + } + + @ViewBuilder private func loadingList() -> some View { + Section { + ListForEach(Array(0 ... 10)) { index in + ListRow { + itemContent( + coin: nil, + marketCap: 123_456, + price: "$123.45", + rank: 12, + diff: index % 2 == 0 ? 12.34 : -12.34 + ) + .redacted() + } + } + .simultaneousGesture(DragGesture(minimumDistance: 0), including: .all) + } header: { + listHeader(disabled: true) + .listRowInsets(EdgeInsets()) + .background(Color.themeTyler) + } + } + + @ViewBuilder private func itemContent(coin: Coin?, marketCap: Decimal?, price: String, rank: Int?, diff: Decimal?) -> some View { + CoinIconView(coin: coin) + + VStack(spacing: 1) { + HStack(spacing: .margin8) { + Text(coin?.code ?? "CODE").textBody() + Spacer() + Text(price).textBody() + } + + HStack(spacing: .margin8) { + HStack(spacing: .margin4) { + if let rank { + BadgeViewNew(text: "\(rank)") + } + + if let marketCap, let formatted = ValueFormatter.instance.formatShort(currency: viewModel.currency, value: marketCap) { + Text(formatted).textSubhead2() + } + } + Spacer() + DiffText(diff) + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Platforms/MarketPlatformsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Platforms/MarketPlatformsView.swift new file mode 100644 index 0000000000..56376d89c9 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Platforms/MarketPlatformsView.swift @@ -0,0 +1,169 @@ +import Kingfisher +import MarketKit +import SwiftUI + +struct MarketPlatformsView: View { + @ObservedObject var viewModel: MarketPlatformsViewModel + + @State private var sortBySelectorPresented = false + @State private var timePeriodSelectorPresented = false + + @State private var presentedPlatform: TopPlatform? + + var body: some View { + ThemeView { + switch viewModel.state { + case .loading: + VStack(spacing: 0) { + header(disabled: true) + loadingList() + } + case let .loaded(platforms): + VStack(spacing: 0) { + header() + list(platforms: platforms) + } + case .failed: + SyncErrorView { + Task { + await viewModel.refresh() + } + } + } + } + .sheet(item: $presentedPlatform) { platform in + let isPresented = Binding( + get: { presentedPlatform != nil }, + set: { newValue in if !newValue { presentedPlatform = nil }} + ) + + MarketPlatformViewNew(isPresented: isPresented, platform: platform).ignoresSafeArea() + .onFirstAppear { stat(page: .markets, section: .platforms, event: .openPlatform(chainUid: platform.blockchain.uid)) } + } + } + + @ViewBuilder private func header(disabled: Bool = false) -> some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + Button(action: { + sortBySelectorPresented = true + }) { + Text(viewModel.sortBy.title) + } + .buttonStyle(SecondaryButtonStyle(style: .default, rightAccessory: .dropDown)) + .disabled(disabled) + + Button(action: { + timePeriodSelectorPresented = true + }) { + Text(viewModel.timePeriod.shortTitle) + } + .buttonStyle(SecondaryButtonStyle(style: .default, rightAccessory: .dropDown)) + .disabled(disabled) + } + .padding(.horizontal, .margin16) + .padding(.vertical, .margin8) + } + .alert( + isPresented: $sortBySelectorPresented, + title: "market.sort_by.title".localized, + viewItems: viewModel.sortBys.map { .init(text: $0.title, selected: viewModel.sortBy == $0) }, + onTap: { index in + guard let index else { + return + } + + viewModel.sortBy = viewModel.sortBys[index] + } + ) + .alert( + isPresented: $timePeriodSelectorPresented, + title: "market.time_period.title".localized, + viewItems: viewModel.timePeriods.map { .init(text: $0.title, selected: viewModel.timePeriod == $0) }, + onTap: { index in + guard let index else { + return + } + + viewModel.timePeriod = viewModel.timePeriods[index] + } + ) + } + + @ViewBuilder private func list(platforms: [TopPlatform]) -> some View { + ScrollViewReader { proxy in + ThemeList(platforms, invisibleTopView: true) { platform in + ClickableRow(action: { + presentedPlatform = platform + }) { + let blockchain = platform.blockchain + + itemContent( + imageUrl: URL(string: blockchain.type.imageUrl), + name: blockchain.name, + marketCap: platform.marketCap.flatMap { ValueFormatter.instance.formatShort(currency: viewModel.currency, value: $0) } ?? "n/a".localized, + protocolsCount: platform.protocolsCount, + rank: platform.rank, + rankChange: platform.rank.flatMap { rank in platform.ranks[viewModel.timePeriod].map { $0 - rank } }, + diff: platform.changes[viewModel.timePeriod] + ) + } + } + .themeListStyle(.transparent) + .refreshable { + await viewModel.refresh() + } + .onChange(of: viewModel.sortBy) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + .onChange(of: viewModel.timePeriod) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + } + } + + @ViewBuilder private func loadingList() -> some View { + ThemeList(Array(0 ... 10)) { index in + ListRow { + itemContent( + imageUrl: nil, + name: "Blockchain", + marketCap: "$123.4 B", + protocolsCount: 123, + rank: 12, + rankChange: nil, + diff: index % 2 == 0 ? 12.34 : -12.34 + ) + .redacted() + } + } + .themeListStyle(.transparent) + .simultaneousGesture(DragGesture(minimumDistance: 0), including: .all) + } + + @ViewBuilder private func itemContent(imageUrl: URL?, name: String, marketCap: String, protocolsCount: Int?, rank: Int?, rankChange: Int?, diff: Decimal?) -> some View { + KFImage.url(imageUrl) + .resizable() + .placeholder { RoundedRectangle(cornerRadius: .cornerRadius8).fill(Color.themeSteel20) } + .clipShape(RoundedRectangle(cornerRadius: .cornerRadius8)) + .frame(width: .iconSize32, height: .iconSize32) + + VStack(spacing: 1) { + HStack(spacing: .margin8) { + Text(name).textBody() + Spacer() + Text(marketCap).textBody() + } + + HStack(spacing: .margin8) { + HStack(spacing: .margin4) { + if let rank { + BadgeViewNew(text: "\(rank)", change: rankChange) + } + + if let protocolsCount { + Text("market.top.protocols".localized(String(protocolsCount))).textSubhead2() + } + } + Spacer() + DiffText(diff) + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Platforms/MarketPlatformsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Platforms/MarketPlatformsViewModel.swift new file mode 100644 index 0000000000..a488033010 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Platforms/MarketPlatformsViewModel.swift @@ -0,0 +1,113 @@ +import Combine +import Foundation +import HsExtensions +import MarketKit + +class MarketPlatformsViewModel: ObservableObject { + private let marketKit = App.shared.marketKit + private let currencyManager = App.shared.currencyManager + private let appManager = App.shared.appManager + private var cancellables = Set() + private var tasks = Set() + + private var internalState: State = .loading { + didSet { + syncState() + } + } + + @Published var state: State = .loading + + var sortBy: MarketModule.SortBy = .gainers { + didSet { + stat(page: .markets, section: .platforms, event: .switchSortType(sortType: sortBy.statSortType)) + syncState() + } + } + + var timePeriod: HsTimePeriod = .week1 { + didSet { + stat(page: .markets, section: .platforms, event: .switchPeriod(period: timePeriod.statPeriod)) + syncState() + } + } + + private func syncMarketInfos() { + tasks = Set() + + Task { [weak self] in + await self?._syncMarketInfos() + }.store(in: &tasks) + } + + private func _syncMarketInfos() async { + if case .failed = state { + await MainActor.run { [weak self] in + self?.internalState = .loading + } + } + + do { + let platforms = try await marketKit.topPlatforms(currencyCode: currency.code) + + await MainActor.run { [weak self] in + self?.internalState = .loaded(platforms: platforms) + } + } catch { + await MainActor.run { [weak self] in + self?.internalState = .failed(error: error) + } + } + } + + private func syncState() { + switch internalState { + case .loading: + state = .loading + case let .loaded(platforms): + state = .loaded(platforms: platforms.sorted(sortBy: sortBy, timePeriod: timePeriod)) + case let .failed(error): + state = .failed(error: error) + } + } +} + +extension MarketPlatformsViewModel { + var currency: Currency { + currencyManager.baseCurrency + } + + var sortBys: [MarketModule.SortBy] { + [.highestCap, .lowestCap, .gainers, .losers] + } + + var timePeriods: [HsTimePeriod] { + [.week1, .month1, .month3] + } + + func load() { + currencyManager.$baseCurrency + .sink { [weak self] _ in + self?.syncMarketInfos() + } + .store(in: &cancellables) + + appManager.willEnterForegroundPublisher + .sink { [weak self] in self?.syncMarketInfos() } + .store(in: &cancellables) + + syncMarketInfos() + } + + func refresh() async { + await _syncMarketInfos() + } +} + +extension MarketPlatformsViewModel { + enum State { + case loading + case loaded(platforms: [TopPlatform]) + case failed(error: Error) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Search/MarketSearchView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Search/MarketSearchView.swift new file mode 100644 index 0000000000..6b8c83f8ef --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Search/MarketSearchView.swift @@ -0,0 +1,64 @@ +import Kingfisher +import MarketKit +import SwiftUI +import ThemeKit + +struct MarketSearchView: View { + @ObservedObject var viewModel: MarketSearchViewModel + @ObservedObject var watchlistViewModel: WatchlistViewModel + + @State private var presentedFullCoin: FullCoin? + + var body: some View { + ThemeView { + switch viewModel.state { + case let .placeholder(recentFullCoins, popularFullCoins): + ThemeList { + if !recentFullCoins.isEmpty { + Section { + ListForEach(recentFullCoins) { fullCoin in + itemContent(fullCoin: fullCoin) + } + } header: { + ThemeListSectionHeader(text: "market.search.recent".localized) + } + } + + Section { + ListForEach(popularFullCoins) { fullCoin in + itemContent(fullCoin: fullCoin) + } + } header: { + ThemeListSectionHeader(text: "market.search.popular".localized) + } + } + case let .searchResults(fullCoins): + ThemeList { + ListForEach(fullCoins) { fullCoin in + itemContent(fullCoin: fullCoin) + } + } + } + } + .sheet(item: $presentedFullCoin) { fullCoin in + CoinPageViewNew(coinUid: fullCoin.coin.uid).ignoresSafeArea() + } + } + + @ViewBuilder private func itemContent(fullCoin: FullCoin) -> some View { + let coin = fullCoin.coin + + ClickableRow(action: { + viewModel.handleOpen(coinUid: coin.uid) + presentedFullCoin = fullCoin + }) { + CoinIconView(coin: coin) + + VStack(spacing: 1) { + Text(coin.code).themeBody() + Text(coin.name).themeSubhead2() + } + } + .watchlistSwipeActions(viewModel: watchlistViewModel, coinUid: coin.uid) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Search/MarketSearchViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Search/MarketSearchViewModel.swift new file mode 100644 index 0000000000..9dc7feb0b5 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Search/MarketSearchViewModel.swift @@ -0,0 +1,63 @@ +import Combine +import Foundation +import MarketKit + +class MarketSearchViewModel: ObservableObject { + private let keyRecentCoinUids = "market-recent-coin-uids" + + private let marketKit = App.shared.marketKit + private let userDefaultsStorage = App.shared.userDefaultsStorage + + @Published private(set) var state: State = .placeholder(recentFullCoins: [], popularFullCoins: []) + @Published var searchText: String = "" { + didSet { + syncState() + } + } + + private var recentCoinUids: [String] { + didSet { + userDefaultsStorage.set(value: recentCoinUids.joined(separator: ","), for: keyRecentCoinUids) + } + } + + init() { + let recentCoinsUidsRaw: String = userDefaultsStorage.value(for: keyRecentCoinUids) ?? "" + recentCoinUids = recentCoinsUidsRaw.components(separatedBy: ",") + + syncState() + } + + private func syncState() { + if searchText.isEmpty { + let recentMarketFullCoins = (try? marketKit.fullCoins(coinUids: recentCoinUids)) ?? [] + let recentFullCoins = recentCoinUids.compactMap { coinUid in recentMarketFullCoins.first { $0.coin.uid == coinUid } } + + let popularFullCoins = (try? marketKit.topFullCoins()) ?? [] + + state = .placeholder(recentFullCoins: recentFullCoins, popularFullCoins: popularFullCoins) + } else { + state = .searchResults(fullCoins: (try? marketKit.fullCoins(filter: searchText)) ?? []) + } + } +} + +extension MarketSearchViewModel { + func handleOpen(coinUid: String) { + var recentCoinUids = recentCoinUids + + if let index = recentCoinUids.firstIndex(of: coinUid) { + recentCoinUids.remove(at: index) + } + + recentCoinUids.insert(coinUid, at: 0) + self.recentCoinUids = Array(recentCoinUids.prefix(5)) + } +} + +extension MarketSearchViewModel { + enum State { + case placeholder(recentFullCoins: [FullCoin], popularFullCoins: [FullCoin]) + case searchResults(fullCoins: [FullCoin]) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Tab/MarketTabView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Tab/MarketTabView.swift new file mode 100644 index 0000000000..f35e84b0cf --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Tab/MarketTabView.swift @@ -0,0 +1,76 @@ +import SwiftUI +import ThemeKit + +struct MarketTabView: View { + @StateObject var viewModel: MarketTabViewModel + @ObservedObject var watchlistViewModel: WatchlistViewModel + + @StateObject var coinsViewModel: MarketCoinsViewModel + @StateObject var marketWatchlistViewModel: MarketWatchlistViewModel + @StateObject var newsViewModel: MarketNewsViewModel + @StateObject var platformsViewModel: MarketPlatformsViewModel + @StateObject var pairsViewModel: MarketPairsViewModel + + @State private var loadedTabs = [MarketModule.Tab]() + + init(watchlistViewModel: WatchlistViewModel) { + _viewModel = StateObject(wrappedValue: MarketTabViewModel()) + self.watchlistViewModel = watchlistViewModel + + _coinsViewModel = StateObject(wrappedValue: MarketCoinsViewModel()) + _marketWatchlistViewModel = StateObject(wrappedValue: MarketWatchlistViewModel()) + _newsViewModel = StateObject(wrappedValue: MarketNewsViewModel()) + _platformsViewModel = StateObject(wrappedValue: MarketPlatformsViewModel()) + _pairsViewModel = StateObject(wrappedValue: MarketPairsViewModel()) + } + + var body: some View { + VStack(spacing: 0) { + ScrollableTabHeaderView( + tabs: MarketModule.Tab.allCases.map(\.title), + currentTabIndex: Binding( + get: { + MarketModule.Tab.allCases.firstIndex(of: viewModel.currentTab) ?? 0 + }, + set: { index in + viewModel.currentTab = MarketModule.Tab.allCases[index] + } + ) + ) + + VStack { + switch viewModel.currentTab { + case .coins: MarketCoinsView(viewModel: coinsViewModel, watchlistViewModel: watchlistViewModel) + case .watchlist: MarketWatchlistView(viewModel: marketWatchlistViewModel) + case .news: MarketNewsView(viewModel: newsViewModel) + case .platforms: MarketPlatformsView(viewModel: platformsViewModel) + case .pairs: MarketPairsView(viewModel: pairsViewModel) + } + } + .frame(maxHeight: .infinity) + .onChange(of: viewModel.currentTab) { tab in + stat(page: .markets, event: .switchTab(tab: tab.statTab)) + load(tab: tab) + } + .onFirstAppear { + load(tab: viewModel.currentTab) + } + } + } + + private func load(tab: MarketModule.Tab) { + guard !loadedTabs.contains(tab) else { + return + } + + loadedTabs.append(tab) + + switch tab { + case .coins: coinsViewModel.load() + case .watchlist: marketWatchlistViewModel.load() + case .news: newsViewModel.load() + case .platforms: platformsViewModel.load() + case .pairs: pairsViewModel.load() + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Tab/MarketTabViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Tab/MarketTabViewModel.swift new file mode 100644 index 0000000000..233b10a00a --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Tab/MarketTabViewModel.swift @@ -0,0 +1,29 @@ +import Combine + +class MarketTabViewModel: ObservableObject { + private let keyTab = "market-tab" + + private let userDefaultsStorage = App.shared.userDefaultsStorage + private let launchScreenManager = App.shared.launchScreenManager + + @Published var currentTab: MarketModule.Tab { + didSet { + userDefaultsStorage.set(value: currentTab.rawValue, for: keyTab) + } + } + + init() { + switch launchScreenManager.launchScreen { + case .auto: + if let storedValue: String = userDefaultsStorage.value(for: keyTab), let storedTab = MarketModule.Tab(rawValue: storedValue) { + currentTab = storedTab + } else { + currentTab = .coins + } + case .balance, .marketOverview: + currentTab = .coins + case .watchlist: + currentTab = .watchlist + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/TopPlatform/TopPlatformHeaderCell.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/TopPlatform/TopPlatformHeaderCell.swift deleted file mode 100644 index 7874c7426b..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/TopPlatform/TopPlatformHeaderCell.swift +++ /dev/null @@ -1,54 +0,0 @@ -import UIKit - -class TopPlatformHeaderCell: UITableViewCell { - static let height: CGFloat = 108 - - private let titleLabel = UILabel() - private let descriptionLabel = UILabel() - private let rightImageView = UIImageView() - - override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - backgroundColor = .clear - selectionStyle = .none - - contentView.addSubview(titleLabel) - titleLabel.snp.makeConstraints { make in - make.leading.equalToSuperview().inset(CGFloat.margin16) - make.top.equalToSuperview().inset(CGFloat.margin12) - } - - titleLabel.font = .headline1 - titleLabel.textColor = .themeLeah - - contentView.addSubview(descriptionLabel) - descriptionLabel.snp.makeConstraints { make in - make.leading.trailing.equalTo(titleLabel) - make.top.equalTo(titleLabel.snp.bottom).offset(CGFloat.margin8) - } - - descriptionLabel.numberOfLines = 0 - descriptionLabel.font = .subhead2 - descriptionLabel.textColor = .themeGray - - contentView.addSubview(rightImageView) - rightImageView.snp.makeConstraints { make in - make.leading.equalTo(titleLabel.snp.trailing).offset(CGFloat.margin24) - make.trailing.equalToSuperview().inset(CGFloat.margin24) - make.centerY.equalToSuperview() - make.size.equalTo(CGFloat.iconSize32) - } - } - - @available(*, unavailable) - public required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func set(title: String, description: String, imageUrl: String) { - titleLabel.text = title - descriptionLabel.text = description - rightImageView.setImage(withUrlString: imageUrl, placeholder: nil) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/TopPlatform/TopPlatformModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/TopPlatform/TopPlatformModule.swift deleted file mode 100644 index 47618b867d..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/TopPlatform/TopPlatformModule.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Chart -import MarketKit -import ThemeKit -import UIKit - -enum TopPlatformModule { - static func viewController(topPlatform: TopPlatform) -> UIViewController { - let service = TopPlatformService(topPlatform: topPlatform, marketKit: App.shared.marketKit) - let listService = MarketFilteredListService(currencyManager: App.shared.currencyManager, provider: service, statPage: .topPlatform) - let watchlistToggleService = MarketWatchlistToggleService(coinUidService: listService, favoritesManager: App.shared.favoritesManager, statPage: .topPlatform) - - let marketCapFetcher = TopPlatformMarketCapFetcher(marketKit: App.shared.marketKit, currencyManager: App.shared.currencyManager, topPlatform: topPlatform) - let chartService = MetricChartService(chartFetcher: marketCapFetcher, interval: .byPeriod(.week1), statPage: .topPlatform) - let factory = MetricChartFactory(currentLocale: LanguageManager.shared.currentLocale) - let chartViewModel = MetricChartViewModel(service: chartService, factory: factory) - - let decorator = MarketListMarketFieldDecorator(service: listService, statPage: .topPlatform) - let viewModel = TopPlatformViewModel(service: service) - let listViewModel = MarketListWatchViewModel(service: listService, watchlistToggleService: watchlistToggleService, decorator: decorator) - let headerViewModel = MarketMultiSortHeaderViewModel(service: listService, decorator: decorator) - - let viewController = TopPlatformViewController(viewModel: viewModel, chartViewModel: chartViewModel, listViewModel: listViewModel, headerViewModel: headerViewModel) - - return ThemeNavigationController(rootViewController: viewController) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/TopPlatform/TopPlatformService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/TopPlatform/TopPlatformService.swift deleted file mode 100644 index 8ad356c2b7..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/TopPlatform/TopPlatformService.swift +++ /dev/null @@ -1,17 +0,0 @@ -import MarketKit - -class TopPlatformService { - let topPlatform: TopPlatform - private let marketKit: MarketKit.Kit - - init(topPlatform: TopPlatform, marketKit: MarketKit.Kit) { - self.topPlatform = topPlatform - self.marketKit = marketKit - } -} - -extension TopPlatformService: IMarketFilteredListProvider { - func marketInfos(currencyCode: String) async throws -> [MarketInfo] { - try await marketKit.topPlatformMarketInfos(blockchain: topPlatform.blockchain.uid, currencyCode: currencyCode) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/TopPlatform/TopPlatformViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/TopPlatform/TopPlatformViewController.swift deleted file mode 100644 index 6b0fb725ad..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/TopPlatform/TopPlatformViewController.swift +++ /dev/null @@ -1,84 +0,0 @@ -import SectionsTableView -import SnapKit -import ThemeKit -import UIKit - -class TopPlatformViewController: MarketListViewController { - private let viewModel: TopPlatformViewModel - private let multiSortHeaderView: MarketMultiSortHeaderView - - override var viewController: UIViewController? { self } - override var headerView: UITableViewHeaderFooterView? { multiSortHeaderView } - override var refreshEnabled: Bool { false } - - private let chartViewModel: MetricChartViewModel - private let chartCell: ChartCell - private let chartRow: StaticRow - - init(viewModel: TopPlatformViewModel, chartViewModel: MetricChartViewModel, listViewModel: IMarketListViewModel, headerViewModel: MarketMultiSortHeaderViewModel) { - self.viewModel = viewModel - self.chartViewModel = chartViewModel - multiSortHeaderView = MarketMultiSortHeaderView(viewModel: headerViewModel) - - chartCell = ChartCell(viewModel: chartViewModel, configuration: .baseChart) - chartRow = StaticRow( - cell: chartCell, - id: "chartView", - height: chartCell.cellHeight - ) - - super.init(listViewModel: listViewModel, statPage: .topPlatform) - - multiSortHeaderView.viewController = self - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - navigationItem.largeTitleDisplayMode = .never - navigationItem.rightBarButtonItem = UIBarButtonItem(title: "button.close".localized, style: .plain, target: self, action: #selector(onTapClose)) - - tableView.registerCell(forClass: TopPlatformHeaderCell.self) - - chartRow.onReady = { [weak chartCell] in chartCell?.onLoad() } - chartViewModel.start() - } - - @objc private func onTapClose() { - dismiss(animated: true) - } - - override func topSections(loaded: Bool) -> [SectionProtocol] { - var sections = [Section( - id: "header", - rows: [ - Row( - id: "header", - height: TopPlatformHeaderCell.height, - bind: { [weak self] cell, _ in - self?.bind(cell: cell) - } - ), - ] - )] - - if loaded { - sections.append(Section(id: "chart", rows: [chartRow])) - } - - return sections - } - - private func bind(cell: TopPlatformHeaderCell) { - cell.set( - title: viewModel.title, - description: viewModel.description, - imageUrl: viewModel.imageUrl - ) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/TopPlatform/TopPlatformViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/TopPlatform/TopPlatformViewModel.swift deleted file mode 100644 index 4d5acfd65e..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/TopPlatform/TopPlatformViewModel.swift +++ /dev/null @@ -1,21 +0,0 @@ -class TopPlatformViewModel { - private let service: TopPlatformService - - init(service: TopPlatformService) { - self.service = service - } -} - -extension TopPlatformViewModel { - var title: String { - "top_platform.title".localized(service.topPlatform.blockchain.name) - } - - var description: String { - "top_platform.description".localized(service.topPlatform.blockchain.name) - } - - var imageUrl: String { - service.topPlatform.blockchain.type.imageUrl - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Tvl/MarketTvlView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Tvl/MarketTvlView.swift new file mode 100644 index 0000000000..0624a1a47b --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Tvl/MarketTvlView.swift @@ -0,0 +1,270 @@ +import Kingfisher +import MarketKit +import SwiftUI + +struct MarketTvlView: View { + @StateObject var viewModel: MarketTvlViewModel + @StateObject var chartViewModel: MetricChartViewModel + @StateObject var watchlistViewModel: WatchlistViewModel + + @Environment(\.presentationMode) private var presentationMode + + @State private var filterBySelectorPresented = false + @State private var presentedFullCoin: FullCoin? + + init() { + _viewModel = StateObject(wrappedValue: MarketTvlViewModel()) + _chartViewModel = StateObject(wrappedValue: MetricChartViewModel.instance(type: .tvlInDefi)) + _watchlistViewModel = StateObject(wrappedValue: WatchlistViewModel(page: .globalMetricsTvlInDefi)) + } + + var body: some View { + ThemeView { + switch viewModel.state { + case .loading: + VStack(spacing: 0) { + header() + Spacer() + ProgressView() + Spacer() + } + case let .loaded(defiCoins): + ScrollViewReader { proxy in + ThemeList(bottomSpacing: .margin16, invisibleTopView: true) { + header() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + chart() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + + list(defiCoins: defiCoins) + } + .onChange(of: viewModel.platforms) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + .onChange(of: viewModel.sortOrder) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + } + case .failed: + VStack(spacing: 0) { + header() + + SyncErrorView { + viewModel.sync() + } + } + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("button.close".localized) { + presentationMode.wrappedValue.dismiss() + } + } + } + .onReceive(chartViewModel.$periodType) { periodType in + viewModel.timePeriod = HsTimePeriod(periodType) ?? .day1 + } + .sheet(item: $presentedFullCoin) { fullCoin in + CoinPageViewNew(coinUid: fullCoin.coin.uid).ignoresSafeArea() + .onFirstAppear { stat(page: .globalMetricsTvlInDefi, event: .openCoin(coinUid: fullCoin.coin.uid)) } + } + } + + @ViewBuilder private func header() -> some View { + HStack(spacing: .margin32) { + VStack(spacing: .margin8) { + Text("market.tvl_in_defi.title".localized).themeHeadline1() + Text("market.tvl_in_defi.description".localized).themeSubhead2() + } + .padding(.vertical, .margin12) + + KFImage.url(URL(string: "tvl".headerImageUrl)) + .resizable() + .frame(width: 76, height: 108) + } + .padding(.leading, .margin16) + } + + @ViewBuilder private func chart() -> some View { + ChartView(viewModel: chartViewModel, configuration: .marketCapChart) + .frame(maxWidth: .infinity) + .onFirstAppear { + chartViewModel.start() + } + } + + @ViewBuilder private func listHeader(disabled: Bool = false) -> some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + Button(action: { + filterBySelectorPresented = true + }) { + Text(viewModel.platforms.title) + } + .buttonStyle(SecondaryButtonStyle(style: .default, rightAccessory: .dropDown)) + .disabled(disabled) + + Button(action: { + viewModel.sortOrder.toggle() + }) { + Text("market.tvl_in_defi.tvl".localized) + } + .buttonStyle(SecondaryButtonStyle(style: .default, rightAccessory: .custom(image: sortIcon()))) + + Button(action: { + viewModel.diffType.toggle() + }) { + diffIcon().themeIcon(color: .themeLeah) + } + .buttonStyle(SecondaryCircleButtonStyle(style: .default)) + } + .padding(.horizontal, .margin16) + .padding(.vertical, .margin8) + } + .alert( + isPresented: $filterBySelectorPresented, + title: "market.tvl_in_defi.filter_by_chain".localized, + viewItems: MarketTvlViewModel.Platforms.allCases.map { .init(text: $0.title, selected: viewModel.platforms == $0) }, + onTap: { index in + guard let index else { + return + } + + viewModel.platforms = MarketTvlViewModel.Platforms.allCases[index] + } + ) + } + + @ViewBuilder private func list(defiCoins: [DefiCoin]) -> some View { + Section { + ListForEach(defiCoins) { defiCoin in + let platform = defiCoin.chains.count == 1 ? defiCoin.chains[0] : "market.tvl_in_defi.multi_chain".localized + let values: (Decimal?, DiffText.Diff?) = viewModel.values(defiCoin: defiCoin) + + switch defiCoin.type { + case let .defiCoin(name, url): + ListRow { + itemContent( + imageUrl: URL(string: url), + code: name, + platform: platform, + rank: defiCoin.tvlRank, + tvl: values.0, + diff: values.1 + ) + } + case let .fullCoin(fullCoin): + let coin = fullCoin.coin + ClickableRow(action: { + presentedFullCoin = fullCoin + }) { + itemContent( + coin: coin, + platform: platform, + rank: defiCoin.tvlRank, + tvl: values.0, + diff: values.1 + ) + } + .watchlistSwipeActions(viewModel: watchlistViewModel, coinUid: coin.uid) + } + } + } header: { + listHeader() + .listRowInsets(EdgeInsets()) + .background(Color.themeTyler) + } + } + + @ViewBuilder private func loadingList() -> some View { + Section { + ListForEach(Array(0 ... 10)) { index in + ListRow { + itemContent( + imageUrl: nil, + code: "CODE", + platform: "market.global.tvl_in_defi.multi_chain".localized, + rank: 12, + tvl: 123_456, + diff: .percent(value: index % 2 == 0 ? 12.34 : -12.34) + ) + .redacted() + } + } + } header: { + listHeader(disabled: true) + .listRowInsets(EdgeInsets()) + .background(Color.themeTyler) + } + } + + @ViewBuilder private func itemContent(imageUrl: URL?, code: String, platform: String, rank: Int?, tvl: Decimal?, diff: DiffText.Diff?) -> some View { + KFImage.url(imageUrl) + .resizable() + .placeholder { Circle().fill(Color.themeSteel20) } + .clipShape(Circle()) + .frame(width: .iconSize32, height: .iconSize32) + + VStack(spacing: 1) { + HStack(spacing: .margin8) { + Text(code).textBody() + Spacer() + if let tvl, let formatted = ValueFormatter.instance.formatShort(currency: viewModel.currency, value: tvl) { + Text(formatted).textBody() + } + } + + HStack(spacing: .margin8) { + HStack(spacing: .margin4) { + if let rank { + BadgeViewNew(text: "\(rank)") + } + Text(platform).textSubhead2() + } + Spacer() + DiffText(diff) + } + } + } + + @ViewBuilder private func itemContent(coin: Coin?, platform: String, rank: Int?, tvl: Decimal?, diff: DiffText.Diff?) -> some View { + CoinIconView(coin: coin) + + VStack(spacing: 1) { + HStack(spacing: .margin8) { + Text(coin?.code ?? "CODE").textBody() + Spacer() + if let tvl, let formatted = ValueFormatter.instance.formatShort(currency: viewModel.currency, value: tvl) { + Text(formatted).textBody() + } + } + + HStack(spacing: .margin8) { + HStack(spacing: .margin4) { + if let rank { + BadgeViewNew(text: "\(rank)") + } + Text(platform).textSubhead2() + } + Spacer() + DiffText(diff) + } + } + } + + private func sortIcon() -> Image { + switch viewModel.sortOrder { + case .asc: return Image("arrow_medium_2_up_20") + case .desc: return Image("arrow_medium_2_down_20") + } + } + + private func diffIcon() -> Image { + switch viewModel.diffType { + case .percent: return Image("percent_20") + case .currencyValue: return Image("usd_20") + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Tvl/MarketTvlViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Tvl/MarketTvlViewModel.swift new file mode 100644 index 0000000000..7cbee06e56 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Tvl/MarketTvlViewModel.swift @@ -0,0 +1,186 @@ +import Combine +import Foundation +import HsExtensions +import MarketKit + +class MarketTvlViewModel: ObservableObject { + private let marketKit = App.shared.marketKit + private let currencyManager = App.shared.currencyManager + + private var cancellables = Set() + private var tasks = Set() + + private var internalState: State = .loading { + didSet { + syncState() + } + } + + @Published var state: State = .loading + + var platforms: MarketTvlViewModel.Platforms = .all { + didSet { + stat(page: .globalMetricsTvlInDefi, event: .switchTvlChain(chain: platforms.statPlatform)) + syncState() + } + } + + var sortOrder: MarketModule.SortOrder = .desc { + didSet { + stat(page: .globalMetricsTvlInDefi, event: .toggleSortDirection) + syncState() + } + } + + @Published var timePeriod: HsTimePeriod = .day1 + @Published var diffType: DiffType = .percent { + didSet { + stat(page: .globalMetricsTvlInDefi, event: .toggleTvlField(field: diffType.statField)) + } + } + + init() { + currencyManager.$baseCurrency + .sink { [weak self] _ in + self?.sync() + } + .store(in: &cancellables) + + sync() + } + + private func syncState() { + switch internalState { + case .loading: + state = .loading + case let .loaded(defiCoins): + let asc = sortOrder.isAsc + let defiCoins = defiCoins + .filter { defiCoin in + switch platforms { + case .all: return true + default: return defiCoin.chains.contains(platforms.chain) + } + } + .sorted { lhsDefiCoin, rhsDefiCoin in + let lhsTvl = tvl(defiCoin: lhsDefiCoin, platforms: platforms) ?? 0 + let rhsTvl = tvl(defiCoin: rhsDefiCoin, platforms: platforms) ?? 0 + return asc ? lhsTvl < rhsTvl : lhsTvl > rhsTvl + } + state = .loaded(defiCoins: defiCoins) + case let .failed(error): + state = .failed(error: error) + } + } + + private func tvl(defiCoin: DefiCoin, platforms: MarketTvlViewModel.Platforms) -> Decimal? { + switch platforms { + case .all: return defiCoin.tvl + default: return defiCoin.chainTvls[platforms.chain] + } + } +} + +extension MarketTvlViewModel { + var currency: Currency { + currencyManager.baseCurrency + } + + func sync() { + tasks = Set() + + if case .failed = internalState { + internalState = .loading + } + + Task { [weak self, marketKit, currency] in + do { + let defiCoins = try await marketKit.defiCoins(currencyCode: currency.code) + + await MainActor.run { [weak self] in + self?.internalState = .loaded(defiCoins: defiCoins) + } + } catch { + await MainActor.run { [weak self] in + self?.internalState = .failed(error: error) + } + } + } + .store(in: &tasks) + } + + func values(defiCoin: DefiCoin) -> (Decimal?, DiffText.Diff?) { + var tvl: Decimal? + let diff: DiffText.Diff? + + switch platforms { + case .all: + tvl = defiCoin.tvl + + let tvlChange: Decimal? = defiCoin.tvlChangeValue(timePeriod: timePeriod) + + switch diffType { + case .percent: diff = tvlChange.map { .percent(value: $0) } + case .currencyValue: diff = tvlChange.map { .change(value: $0 * defiCoin.tvl / 100, currency: currency) } + } + default: + tvl = defiCoin.chainTvls[platforms.chain] + diff = nil + } + + return (tvl, diff) + } +} + +extension MarketTvlViewModel { + enum State { + case loading + case loaded(defiCoins: [DefiCoin]) + case failed(error: Error) + } + + enum Platforms: Int, CaseIterable { + case all + case ethereum + case solana + case binance + case avalanche + case terra + case fantom + case arbitrum + case polygon + + var chain: String { + switch self { + case .all: return "" + case .ethereum: return "Ethereum" + case .solana: return "Solana" + case .binance: return "Binance" + case .avalanche: return "Avalanche" + case .terra: return "Terra" + case .fantom: return "Fantom" + case .arbitrum: return "Arbitrum" + case .polygon: return "Polygon" + } + } + + var title: String { + switch self { + case .all: return "market.tvl.platform_field.all".localized + default: return chain + } + } + } + + enum DiffType: Int, CaseIterable { + case percent + case currencyValue + + mutating func toggle() { + switch self { + case .percent: self = .currencyValue + case .currencyValue: self = .percent + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/DropdownSortHeaderView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/DropdownSortHeaderView.swift deleted file mode 100644 index 9ad9f54112..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/DropdownSortHeaderView.swift +++ /dev/null @@ -1,97 +0,0 @@ -import ComponentKit -import RxCocoa -import RxSwift -import SnapKit -import ThemeKit -import UIKit - -protocol IDropdownSortHeaderViewModel: AnyObject { - var dropdownTitle: String { get } - var dropdownViewItems: [AlertViewItem] { get } - var dropdownValueDriver: Driver { get } - func onSelectDropdown(index: Int) - - var sortDirectionAscendingDriver: Driver { get } - func onToggleSortDirection() -} - -class DropdownSortHeaderView: UITableViewHeaderFooterView { - private let viewModel: IDropdownSortHeaderViewModel - private let disposeBag = DisposeBag() - - weak var viewController: UIViewController? - - private let dropdownButton = SecondaryButton() - private let sortButton = SecondaryCircleButton() - - init(viewModel: IDropdownSortHeaderViewModel, hasTopSeparator: Bool = true) { - self.viewModel = viewModel - - super.init(reuseIdentifier: nil) - - backgroundView = UIView() - backgroundView?.backgroundColor = .themeNavigationBarBackground - - if hasTopSeparator { - let separatorView = UIView() - contentView.addSubview(separatorView) - separatorView.snp.makeConstraints { maker in - maker.leading.trailing.equalToSuperview() - maker.top.equalToSuperview() - maker.height.equalTo(CGFloat.heightOnePixel) - } - - separatorView.backgroundColor = .themeSteel20 - } - - contentView.addSubview(dropdownButton) - dropdownButton.snp.makeConstraints { maker in - maker.leading.equalToSuperview() - maker.centerY.equalToSuperview() - } - - dropdownButton.set(style: .transparent, image: UIImage(named: "arrow_small_down_20")) - dropdownButton.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - dropdownButton.addTarget(self, action: #selector(onTapDropdownButton), for: .touchUpInside) - - contentView.addSubview(sortButton) - sortButton.snp.makeConstraints { maker in - maker.trailing.equalToSuperview().inset(CGFloat.margin16) - maker.centerY.equalToSuperview() - } - - sortButton.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - sortButton.addTarget(self, action: #selector(onTapSortButton), for: .touchUpInside) - - subscribe(disposeBag, viewModel.dropdownValueDriver) { [weak self] in self?.syncDropdownButton(title: $0) } - subscribe(disposeBag, viewModel.sortDirectionAscendingDriver) { [weak self] in self?.syncSortButton(ascending: $0) } - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc private func onTapDropdownButton() { - let alertController = AlertRouter.module( - title: viewModel.dropdownTitle, - viewItems: viewModel.dropdownViewItems - ) { [weak self] index in - self?.viewModel.onSelectDropdown(index: index) - } - - viewController?.present(alertController, animated: true) - } - - @objc private func onTapSortButton() { - viewModel.onToggleSortDirection() - } - - private func syncDropdownButton(title: String) { - dropdownButton.setTitle(title, for: .normal) - } - - private func syncSortButton(ascending: Bool) { - sortButton.set(image: UIImage(named: ascending ? "sort_l2h_20" : "sort_h2l_20")) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/GradientPercentCircle.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/GradientPercentCircle.swift deleted file mode 100644 index 3fb4b9fe38..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/GradientPercentCircle.swift +++ /dev/null @@ -1,101 +0,0 @@ -import SnapKit -import UIKit - -class GradientPercentCircle: UIView { - static let width: CGFloat = 44 - static let height: CGFloat = 44 - static let gradient = UIImage(named: "Market Metrics Gradient Layer") - static let gradientWidth: CGFloat = 156 - - private let gradientLayer = CALayer() - private let outBoundFrame = CGRect(x: width, y: 0, width: gradientWidth, height: height) - - private var currentValue: CGFloat? = nil - - init() { - super.init(frame: .zero) - - backgroundColor = .clear - layer.cornerRadius = Self.width / 2 - clipsToBounds = true - - gradientLayer.contents = Self.gradient?.cgImage - gradientLayer.frame = outBoundFrame - - layer.addSublayer(gradientLayer) - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func position(value: CGFloat) -> CGPoint { - let value = max(-1, min(value, 1)) - - return CGPoint(x: Self.gradientWidth / 2 - ceil(Self.width * (1 - value)), y: Self.height / 2) - } - - private func hide(layer: CALayer, animated: Bool) { - let endX: CGFloat = (currentValue ?? 0) >= 0 ? 1 : -1 - - layer.add(CALayer.moveAnimation(layer: layer, to: position(value: endX)), forKey: CALayer.moveAnimationKey) - layer.add(CALayer.opacityAnimation(layer: layer, hide: true), forKey: CALayer.opacityAnimationKey) - - if !animated { - layer.removeAllAnimations() - } - } - - private func showCompletion(layer: CALayer, value: CGFloat, animated: Bool) { - layer.add(CALayer.moveAnimation(layer: layer, to: position(value: value)), forKey: CALayer.moveAnimationKey) - layer.add(CALayer.opacityAnimation(layer: layer, hide: false), forKey: CALayer.opacityAnimationKey) - - if !animated { - layer.removeAllAnimations() - } - } - - private func show(layer: CALayer, value: CGFloat, animated: Bool) { - CALayer.perform({ - layer.position = position(value: value >= 0 ? 1 : -1) - layer.opacity = 0 - - layer.removeAllAnimations() - }, completion: { [weak self] in - self?.showCompletion(layer: layer, value: value, animated: animated) - - }) - } - - private func move(layer: CALayer, toValue: CGFloat, animated: Bool) { - layer.add(CALayer.moveAnimation(layer: layer, to: position(value: toValue)), forKey: CALayer.moveAnimationKey) - if !animated { - layer.removeAllAnimations() - } - } -} - -extension GradientPercentCircle { - public func set(value: CGFloat?, animated: Bool = true) { - guard let percentValue = value else { - if currentValue != nil { // alpha change from current to nil (hide) - hide(layer: gradientLayer, animated: animated) - currentValue = nil - } - - return - } - let value = percentValue / 100 - - guard currentValue != nil else { // alpha change from nil to new (show) - show(layer: gradientLayer, value: value, animated: animated) - currentValue = value - - return - } - - move(layer: gradientLayer, toValue: value, animated: animated) // move gradient position - currentValue = value - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/MarketMetricView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/MarketMetricView.swift deleted file mode 100644 index edede2a9d5..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/MarketMetricView.swift +++ /dev/null @@ -1,172 +0,0 @@ -import Chart -import ComponentKit -import SnapKit -import ThemeKit -import UIKit - -class MarketMetricView: UIView { - static let height: CGFloat = 104 - - private let titleLabel = UILabel() - private let badgeView = BadgeView() - private let valueLabel = UILabel() - private let diffLabel = DiffLabel() - private let chartView: RateChartView - private let button = UIButton() - - var onTap: (() -> Void)? { - didSet { - button.isUserInteractionEnabled = onTap != nil - } - } - - var alreadyHasData: Bool = false - - init(configuration: ChartConfiguration) { - chartView = RateChartView(configuration: configuration) - - super.init(frame: .zero) - - addSubview(button) - button.snp.makeConstraints { maker in - maker.edges.equalToSuperview() - } - - button.addTarget(self, action: #selector(didTapButton), for: .touchUpInside) - button.isUserInteractionEnabled = false - - updateUI() - - backgroundColor = .themeLawrence - layer.cornerRadius = .cornerRadius12 - layer.cornerCurve = .continuous - clipsToBounds = true - - addSubview(chartView) - chartView.snp.makeConstraints { maker in - maker.leading.trailing.bottom.equalToSuperview().inset(CGFloat.margin12) - maker.height.equalTo(configuration.mainHeight) - } - - chartView.isUserInteractionEnabled = false - - addSubview(titleLabel) - titleLabel.snp.makeConstraints { maker in - maker.leading.top.equalToSuperview().inset(CGFloat.margin12) - } - - titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - titleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) - titleLabel.font = .caption - titleLabel.textColor = .themeGray - - addSubview(badgeView) - badgeView.snp.makeConstraints { maker in - maker.top.trailing.equalToSuperview().inset(CGFloat.margin12) - maker.leading.equalTo(titleLabel.snp.trailing) - maker.centerY.equalTo(titleLabel.snp.centerY) - } - - badgeView.set(style: .small) - badgeView.isHidden = true - - addSubview(valueLabel) - valueLabel.snp.makeConstraints { maker in - maker.leading.equalToSuperview().inset(CGFloat.margin12) - maker.top.equalTo(titleLabel.snp.bottom).offset(CGFloat.margin8) - } - - valueLabel.font = .subhead1 - - addSubview(diffLabel) - diffLabel.snp.makeConstraints { maker in - maker.trailing.equalToSuperview().inset(CGFloat.margin12) - maker.leading.equalTo(valueLabel.snp.trailing).offset(CGFloat.margin8) - maker.top.equalTo(titleLabel.snp.bottom).offset(CGFloat.margin8) - } - - diffLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) - diffLabel.textAlignment = .right - diffLabel.font = .subhead1 - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func updateUI() { - button.setBackgroundColor(color: UIColor.themeLawrencePressed, forState: .highlighted) - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - updateUI() - } - - @objc private func didTapButton() { - onTap?() - } -} - -extension MarketMetricView { - var title: String? { - get { titleLabel.text } - set { titleLabel.text = newValue } - } - - var badge: String? { - get { badgeView.text } - set { - badgeView.isHidden = (newValue ?? "").isEmpty - badgeView.text = newValue - } - } - - func set(value: String?, diff: Decimal?, chartData: ChartData?, trend: MovementTrend) { - valueLabel.textColor = value == nil ? .themeGray50 : .themeBran - valueLabel.text = value ?? "n/a".localized - - guard let diffValue = diff else { - diffLabel.set(value: nil) - - return - } - let diff = diffValue - - diffLabel.set(value: diff) - - chartView.setCurve(colorType: trend.chartColorType) - if let chartData { - chartView.set(chartData: chartData, animated: alreadyHasData) - alreadyHasData = true - } else { - alreadyHasData = false - // clear - } - } - - func set(value: String?, diff: String, diffColor: UIColor, chartData: ChartData?, trend: MovementTrend) { - valueLabel.textColor = value == nil ? .themeGray50 : .themeBran - valueLabel.text = value ?? "n/a".localized - - diffLabel.set(text: diff, color: diffColor) - - chartView.setCurve(colorType: trend.chartColorType) - if let chartData { - chartView.set(chartData: chartData, indicators: [], animated: alreadyHasData) - alreadyHasData = true - } else { - alreadyHasData = false - // clear - } - } - - func clear() { - valueLabel.text = nil - diffLabel.clear() - - alreadyHasData = false - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/MarketMultiSortHeaderView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/MarketMultiSortHeaderView.swift deleted file mode 100644 index 3ad3445173..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/MarketMultiSortHeaderView.swift +++ /dev/null @@ -1,117 +0,0 @@ -import ComponentKit -import SnapKit -import ThemeKit -import UIExtensions -import UIKit - -protocol IMarketMultiSortHeaderViewModel { - var sortItems: [String] { get } - var sortIndex: Int { get } - - var leftSelectorItems: [String] { get } - var leftSelectorIndex: Int { get } - - var rightSelectorItems: [String] { get } - var rightSelectorIndex: Int { get } - - func onSelectSort(index: Int) - func onSelectLeft(index: Int) - func onSelectRight(index: Int) -} - -class MarketMultiSortHeaderView: UITableViewHeaderFooterView { - static let height: CGFloat = .heightSingleLineCell - - private let viewModel: IMarketMultiSortHeaderViewModel - weak var viewController: UIViewController? - - private let sortButton = SecondaryButton() - - init(viewModel: IMarketMultiSortHeaderViewModel, hasLeftSelector: Bool = false, hasTopSeparator: Bool = true) { - self.viewModel = viewModel - - super.init(reuseIdentifier: nil) - - backgroundView = UIView() - backgroundView?.backgroundColor = .themeNavigationBarBackground - - if hasTopSeparator { - let separatorView = UIView() - contentView.addSubview(separatorView) - separatorView.snp.makeConstraints { maker in - maker.leading.trailing.equalToSuperview() - maker.top.equalToSuperview() - maker.height.equalTo(CGFloat.heightOnePixel) - } - - separatorView.backgroundColor = .themeSteel20 - } - - contentView.addSubview(sortButton) - sortButton.snp.makeConstraints { maker in - maker.leading.equalToSuperview() - maker.centerY.equalToSuperview() - } - - sortButton.set(style: .transparent, image: UIImage(named: "arrow_small_down_20")) - sortButton.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - - syncSortButtonTitle() - sortButton.addTarget(self, action: #selector(tapSortButton), for: .touchUpInside) - - let rightSelector = SelectorButton() - - contentView.addSubview(rightSelector) - rightSelector.snp.makeConstraints { maker in - maker.trailing.equalToSuperview().inset(CGFloat.margin16) - maker.centerY.equalToSuperview() - maker.height.equalTo(28) - } - - rightSelector.set(items: viewModel.rightSelectorItems) - rightSelector.setSelected(index: viewModel.rightSelectorIndex) - rightSelector.onSelect = { [weak self] index in - self?.viewModel.onSelectRight(index: index) - } - - if hasLeftSelector { - let leftSelector = SelectorButton() - - contentView.addSubview(leftSelector) - leftSelector.snp.makeConstraints { maker in - maker.trailing.equalTo(rightSelector.snp.leading).offset(-CGFloat.margin8) - maker.centerY.equalToSuperview() - maker.height.equalTo(28) - } - - leftSelector.set(items: viewModel.leftSelectorItems) - leftSelector.setSelected(index: viewModel.leftSelectorIndex) - leftSelector.onSelect = { [weak self] index in - self?.viewModel.onSelectLeft(index: index) - } - } - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc private func tapSortButton() { - let alertController = AlertRouter.module( - title: "market.sort_by".localized, - viewItems: viewModel.sortItems.enumerated().map { index, sortingField in - AlertViewItem(text: sortingField, selected: index == viewModel.sortIndex) - } - ) { [weak self] index in - self?.viewModel.onSelectSort(index: index) - self?.syncSortButtonTitle() - } - - viewController?.present(alertController, animated: true) - } - - private func syncSortButtonTitle() { - sortButton.setTitle(viewModel.sortItems[viewModel.sortIndex], for: .normal) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/MarketMultiSortHeaderViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/MarketMultiSortHeaderViewModel.swift deleted file mode 100644 index dde0dcb384..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/MarketMultiSortHeaderViewModel.swift +++ /dev/null @@ -1,62 +0,0 @@ -import RxRelay -import RxSwift - -protocol IMarketMultiSortHeaderService: AnyObject { - var marketTop: MarketModule.MarketTop { get set } - var sortingField: MarketModule.SortingField { get set } -} - -extension IMarketMultiSortHeaderService { - var marketTop: MarketModule.MarketTop { - get { .top100 } - set {} - } -} - -class MarketMultiSortHeaderViewModel { - private let service: IMarketMultiSortHeaderService - private let decorator: MarketListMarketFieldDecorator - - init(service: IMarketMultiSortHeaderService, decorator: MarketListMarketFieldDecorator) { - self.service = service - self.decorator = decorator - } -} - -extension MarketMultiSortHeaderViewModel: IMarketMultiSortHeaderViewModel { - var sortItems: [String] { - MarketModule.SortingField.allCases.map(\.title) - } - - var sortIndex: Int { - MarketModule.SortingField.allCases.firstIndex(of: service.sortingField) ?? 0 - } - - var leftSelectorItems: [String] { - MarketModule.MarketTop.allCases.map(\.title) - } - - var leftSelectorIndex: Int { - MarketModule.MarketTop.allCases.firstIndex(of: service.marketTop) ?? 0 - } - - var rightSelectorItems: [String] { - MarketModule.MarketField.allCases.map(\.title) - } - - var rightSelectorIndex: Int { - MarketModule.MarketField.allCases.firstIndex(of: decorator.marketField) ?? 0 - } - - func onSelectSort(index: Int) { - service.sortingField = MarketModule.SortingField.allCases[index] - } - - func onSelectLeft(index: Int) { - service.marketTop = MarketModule.MarketTop.allCases[index] - } - - func onSelectRight(index: Int) { - decorator.marketField = MarketModule.MarketField.allCases[index] - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/MarketSingleSortHeaderView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/MarketSingleSortHeaderView.swift deleted file mode 100644 index 26e47af632..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/MarketSingleSortHeaderView.swift +++ /dev/null @@ -1,76 +0,0 @@ -import ComponentKit -import RxCocoa -import RxSwift -import SnapKit -import ThemeKit -import UIExtensions -import UIKit - -class MarketSingleSortHeaderView: UITableViewHeaderFooterView { - static let height: CGFloat = .heightSingleLineCell - - private let viewModel: MarketSingleSortHeaderViewModel - private let disposeBag = DisposeBag() - - private let sortButton = SecondaryCircleButton() - - init(viewModel: MarketSingleSortHeaderViewModel, hasTopSeparator: Bool = true) { - self.viewModel = viewModel - - super.init(reuseIdentifier: nil) - - backgroundView = UIView() - backgroundView?.backgroundColor = .themeNavigationBarBackground - - if hasTopSeparator { - let separatorView = UIView() - contentView.addSubview(separatorView) - separatorView.snp.makeConstraints { maker in - maker.leading.trailing.equalToSuperview() - maker.top.equalToSuperview() - maker.height.equalTo(CGFloat.heightOnePixel) - } - - separatorView.backgroundColor = .themeSteel20 - } - - contentView.addSubview(sortButton) - sortButton.snp.makeConstraints { maker in - maker.leading.equalToSuperview().inset(CGFloat.margin16) - maker.centerY.equalToSuperview() - } - - sortButton.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - sortButton.addTarget(self, action: #selector(onTapSortButton), for: .touchUpInside) - - let fieldSelector = SelectorButton() - - contentView.addSubview(fieldSelector) - fieldSelector.snp.makeConstraints { maker in - maker.trailing.equalToSuperview().inset(CGFloat.margin16) - maker.centerY.equalToSuperview() - maker.height.equalTo(28) - } - - fieldSelector.set(items: viewModel.allFields) - fieldSelector.setSelected(index: viewModel.currentFieldIndex) - fieldSelector.onSelect = { [weak self] index in - self?.viewModel.onSelectField(index: index) - } - - subscribe(disposeBag, viewModel.sortDirectionDriver) { [weak self] in self?.syncSortButton(ascending: $0) } - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc private func onTapSortButton() { - viewModel.onToggleSortDirection() - } - - private func syncSortButton(ascending: Bool) { - sortButton.set(image: UIImage(named: ascending ? "sort_l2h_20" : "sort_h2l_20")) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/MarketSingleSortHeaderViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/MarketSingleSortHeaderViewModel.swift deleted file mode 100644 index 1799b6c29b..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/MarketSingleSortHeaderViewModel.swift +++ /dev/null @@ -1,54 +0,0 @@ -import RxCocoa -import RxRelay -import RxSwift - -protocol IMarketSingleSortHeaderService: AnyObject { - var sortDirectionAscending: Bool { get set } -} - -protocol IMarketSingleSortHeaderDecorator: AnyObject { - var allFields: [String] { get } - var currentFieldIndex: Int { get } - func setCurrentField(index: Int) -} - -class MarketSingleSortHeaderViewModel { - private let service: IMarketSingleSortHeaderService - private let decorator: IMarketSingleSortHeaderDecorator - - private let sortDirectionRelay: BehaviorRelay - - init(service: IMarketSingleSortHeaderService, decorator: IMarketSingleSortHeaderDecorator) { - self.service = service - self.decorator = decorator - - sortDirectionRelay = BehaviorRelay(value: service.sortDirectionAscending) - } -} - -extension MarketSingleSortHeaderViewModel { - var allFields: [String] { - decorator.allFields - } - - var sortDirectionAscending: Bool { - service.sortDirectionAscending - } - - var currentFieldIndex: Int { - decorator.currentFieldIndex - } - - var sortDirectionDriver: Driver { - sortDirectionRelay.asDriver() - } - - func onToggleSortDirection() { - service.sortDirectionAscending = !service.sortDirectionAscending - sortDirectionRelay.accept(service.sortDirectionAscending) - } - - func onSelectField(index: Int) { - decorator.setCurrentField(index: index) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/MarketTvlSortHeaderView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/MarketTvlSortHeaderView.swift deleted file mode 100644 index 8834ba0b78..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/MarketTvlSortHeaderView.swift +++ /dev/null @@ -1,115 +0,0 @@ -import ComponentKit -import RxCocoa -import RxSwift -import SnapKit -import ThemeKit -import UIExtensions -import UIKit - -class MarketTvlSortHeaderView: UITableViewHeaderFooterView { - static let height: CGFloat = .heightSingleLineCell - - private let viewModel: MarketTvlSortHeaderViewModel - private let disposeBag = DisposeBag() - - weak var viewController: UIViewController? - - private let dropdownButton = SecondaryButton() - private let sortButton = SecondaryCircleButton() - private let marketTvlFieldButton = SecondaryCircleButton() - - init(viewModel: MarketTvlSortHeaderViewModel, hasTopSeparator: Bool = true) { - self.viewModel = viewModel - - super.init(reuseIdentifier: nil) - - backgroundView = UIView() - backgroundView?.backgroundColor = .themeNavigationBarBackground - - if hasTopSeparator { - let separatorView = UIView() - contentView.addSubview(separatorView) - separatorView.snp.makeConstraints { maker in - maker.leading.trailing.equalToSuperview() - maker.top.equalToSuperview() - maker.height.equalTo(CGFloat.heightOnePixel) - } - - separatorView.backgroundColor = .themeSteel20 - } - - contentView.addSubview(dropdownButton) - dropdownButton.snp.makeConstraints { maker in - maker.leading.equalToSuperview() - maker.centerY.equalToSuperview() - } - - dropdownButton.set(style: .transparent, image: UIImage(named: "arrow_small_down_20")) - dropdownButton.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - dropdownButton.addTarget(self, action: #selector(onTapDropdownButton), for: .touchUpInside) - - marketTvlFieldButton.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - marketTvlFieldButton.addTarget(self, action: #selector(onTapMarketTvlFieldButton), for: .touchUpInside) - - contentView.addSubview(marketTvlFieldButton) - marketTvlFieldButton.snp.makeConstraints { maker in - maker.trailing.equalToSuperview().inset(CGFloat.margin16) - maker.centerY.equalToSuperview() - } - - contentView.addSubview(sortButton) - sortButton.snp.makeConstraints { maker in - maker.trailing.equalTo(marketTvlFieldButton.snp.leading).offset(-CGFloat.margin16) - maker.centerY.equalToSuperview() - } - - sortButton.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - sortButton.addTarget(self, action: #selector(onTapSortButton), for: .touchUpInside) - - subscribe(disposeBag, viewModel.platformFieldDriver) { [weak self] in self?.syncDropdownButton(title: $0) } - subscribe(disposeBag, viewModel.sortDirectionDriver) { [weak self] in self?.syncSortButton(ascending: $0) } - subscribe(disposeBag, viewModel.marketTvlFieldDriver) { [weak self] in self?.syncMarketTvlFieldButton(marketTvlField: $0) } - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc private func onTapDropdownButton() { - let alertController = AlertRouter.module( - title: "market.global.tvl_in_defi.filter_by_chain".localized, - viewItems: viewModel.platformFieldViewItems - ) { [weak self] index in - self?.viewModel.onSelectMarketPlatformField(index: index) - } - - viewController?.present(alertController, animated: true) - } - - @objc private func onTapSortButton() { - viewModel.onToggleSortDirection() - } - - private func syncMarketTvlFieldButton(marketTvlField: MarketModule.MarketTvlField) { - let imageName: String - switch marketTvlField { - case .value: imageName = "usd_20" - case .diff: imageName = "percent_20" - } - - marketTvlFieldButton.set(image: UIImage(named: imageName)) - } - - @objc private func onTapMarketTvlFieldButton() { - viewModel.onToggleMarketTvlField() - } - - private func syncDropdownButton(title: String) { - dropdownButton.setTitle(title, for: .normal) - } - - private func syncSortButton(ascending: Bool) { - sortButton.set(image: UIImage(named: ascending ? "sort_l2h_20" : "sort_h2l_20")) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/MarketTvlSortHeaderViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/MarketTvlSortHeaderViewModel.swift deleted file mode 100644 index e64773cc15..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Views/MarketTvlSortHeaderViewModel.swift +++ /dev/null @@ -1,75 +0,0 @@ -import RxCocoa -import RxRelay -import RxSwift - -class MarketTvlSortHeaderViewModel { - private let service: MarketGlobalTvlMetricService - private let decorator: MarketListTvlDecorator - - private let platformFieldRelay: BehaviorRelay - private let sortDirectionAscendingRelay: BehaviorRelay - private let marketTvlFieldRelay: BehaviorRelay - - init(service: MarketGlobalTvlMetricService, decorator: MarketListTvlDecorator) { - self.service = service - self.decorator = decorator - - platformFieldRelay = BehaviorRelay(value: service.marketPlatformField.title) - sortDirectionAscendingRelay = BehaviorRelay(value: service.sortDirectionAscending) - marketTvlFieldRelay = BehaviorRelay(value: service.marketTvlField) - } -} - -extension MarketTvlSortHeaderViewModel { - var platformFieldViewItems: [AlertViewItem] { - MarketModule.MarketPlatformField.allCases.map { platformField in - AlertViewItem(text: platformField.title, selected: service.marketPlatformField == platformField) - } - } - - var marketTvlFields: [MarketModule.MarketTvlField] { - MarketModule.MarketTvlField.allCases - } - - var sortDirectionAscending: Bool { - service.sortDirectionAscending - } - - var platformFieldIndex: Int { - MarketModule.MarketPlatformField.allCases.firstIndex(of: service.marketPlatformField) ?? 0 - } - - var marketTvlFieldIndex: Int { - MarketModule.MarketTvlField.allCases.firstIndex(of: service.marketTvlField) ?? 0 - } - - var platformFieldDriver: Driver { - platformFieldRelay.asDriver() - } - - var sortDirectionDriver: Driver { - sortDirectionAscendingRelay.asDriver() - } - - var marketTvlFieldDriver: Driver { - marketTvlFieldRelay.asDriver() - } - - func onToggleSortDirection() { - service.sortDirectionAscending = !service.sortDirectionAscending - sortDirectionAscendingRelay.accept(service.sortDirectionAscending) - } - - func onToggleMarketTvlField() { - switch service.marketTvlField { - case .value: service.marketTvlField = .diff - case .diff: service.marketTvlField = .value - } - marketTvlFieldRelay.accept(service.marketTvlField) - } - - func onSelectMarketPlatformField(index: Int) { - service.marketPlatformField = MarketModule.MarketPlatformField.allCases[index] - platformFieldRelay.accept(service.marketPlatformField.title) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Volume/MarketVolumeView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Volume/MarketVolumeView.swift new file mode 100644 index 0000000000..c2bda31409 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Volume/MarketVolumeView.swift @@ -0,0 +1,190 @@ +import Kingfisher +import MarketKit +import SwiftUI + +struct MarketVolumeView: View { + @StateObject var viewModel: MarketVolumeViewModel + @StateObject var chartViewModel: MetricChartViewModel + @StateObject var watchlistViewModel: WatchlistViewModel + @Binding var isPresented: Bool + + @State private var presentedFullCoin: FullCoin? + + init(isPresented: Binding) { + _viewModel = StateObject(wrappedValue: MarketVolumeViewModel()) + _chartViewModel = StateObject(wrappedValue: MetricChartViewModel.instance(type: .volume24h)) + _watchlistViewModel = StateObject(wrappedValue: WatchlistViewModel(page: .globalMetricsVolume)) + _isPresented = isPresented + } + + var body: some View { + ThemeNavigationView { + ThemeView { + switch viewModel.state { + case .loading: + VStack(spacing: 0) { + header() + Spacer() + ProgressView() + Spacer() + } + case let .loaded(marketInfos): + ScrollViewReader { proxy in + ThemeList(bottomSpacing: .margin16, invisibleTopView: true) { + header() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + + chart() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + + list(marketInfos: marketInfos) + } + .onChange(of: viewModel.sortOrder) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + } + case .failed: + VStack(spacing: 0) { + header() + + SyncErrorView { + viewModel.sync() + } + } + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("button.close".localized) { + isPresented = false + } + } + } + .sheet(item: $presentedFullCoin) { fullCoin in + CoinPageViewNew(coinUid: fullCoin.coin.uid).ignoresSafeArea() + .onFirstAppear { stat(page: .globalMetricsVolume, event: .openCoin(coinUid: fullCoin.coin.uid)) } + } + } + } + + @ViewBuilder private func header() -> some View { + HStack(spacing: .margin32) { + VStack(spacing: .margin8) { + Text("market.volume.title".localized).themeHeadline1() + Text("market.volume.description".localized).themeSubhead2() + } + .padding(.vertical, .margin12) + + KFImage.url(URL(string: "total_volume".headerImageUrl)) + .resizable() + .frame(width: 76, height: 108) + } + .padding(.leading, .margin16) + } + + @ViewBuilder private func chart() -> some View { + ChartView(viewModel: chartViewModel, configuration: .baseChart) + .frame(maxWidth: .infinity) + .onFirstAppear { + chartViewModel.start() + } + } + + @ViewBuilder private func listHeader(disabled: Bool = false) -> some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + Button(action: { + viewModel.sortOrder.toggle() + }) { + Text("market.volume.volume".localized) + } + .buttonStyle(SecondaryButtonStyle(style: .default, rightAccessory: .custom(image: sortIcon()))) + .disabled(disabled) + } + .padding(.horizontal, .margin16) + .padding(.vertical, .margin8) + } + } + + @ViewBuilder private func list(marketInfos: [MarketInfo]) -> some View { + Section { + ListForEach(marketInfos) { marketInfo in + let coin = marketInfo.fullCoin.coin + + ClickableRow(action: { + presentedFullCoin = marketInfo.fullCoin + }) { + itemContent( + coin: coin, + volume: marketInfo.totalVolume, + price: marketInfo.price.flatMap { ValueFormatter.instance.formatFull(currency: viewModel.currency, value: $0) } ?? "n/a".localized, + rank: marketInfo.marketCapRank, + diff: marketInfo.priceChangeValue(timePeriod: HsTimePeriod.day1) + ) + } + .watchlistSwipeActions(viewModel: watchlistViewModel, coinUid: coin.uid) + } + } header: { + listHeader() + .listRowInsets(EdgeInsets()) + .background(Color.themeTyler) + } + } + + @ViewBuilder private func loadingList() -> some View { + Section { + ListForEach(Array(0 ... 10)) { index in + ListRow { + itemContent( + coin: nil, + volume: 123_456, + price: "$123.45", + rank: 12, + diff: index % 2 == 0 ? 12.34 : -12.34 + ) + .redacted() + } + } + } header: { + listHeader(disabled: true) + .listRowInsets(EdgeInsets()) + .background(Color.themeTyler) + } + } + + @ViewBuilder private func itemContent(coin: Coin?, volume: Decimal?, price: String, rank: Int?, diff: Decimal?) -> some View { + CoinIconView(coin: coin) + + VStack(spacing: 1) { + HStack(spacing: .margin8) { + Text(coin?.code ?? "CODE").textBody() + Spacer() + Text(price).textBody() + } + + HStack(spacing: .margin8) { + HStack(spacing: .margin4) { + if let rank { + BadgeViewNew(text: "\(rank)") + } + + if let volume, let formatted = ValueFormatter.instance.formatShort(currency: viewModel.currency, value: volume) { + Text(formatted).textSubhead2() + } + } + Spacer() + DiffText(diff) + } + } + } + + private func sortIcon() -> Image { + switch viewModel.sortOrder { + case .asc: return Image("arrow_medium_2_up_20") + case .desc: return Image("arrow_medium_2_down_20") + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Volume/MarketVolumeViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Volume/MarketVolumeViewModel.swift new file mode 100644 index 0000000000..5dbaaed754 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Volume/MarketVolumeViewModel.swift @@ -0,0 +1,92 @@ +import Combine +import Foundation +import HsExtensions +import MarketKit + +class MarketVolumeViewModel: ObservableObject { + private let marketKit = App.shared.marketKit + private let currencyManager = App.shared.currencyManager + + private var cancellables = Set() + private var tasks = Set() + + private var internalState: State = .loading { + didSet { + syncState() + } + } + + @Published var state: State = .loading + + var sortOrder: MarketModule.SortOrder = .desc { + didSet { + stat(page: .globalMetricsVolume, event: .toggleSortDirection) + syncState() + } + } + + init() { + currencyManager.$baseCurrency + .sink { [weak self] _ in + self?.sync() + } + .store(in: &cancellables) + + sync() + } + + private func syncState() { + switch internalState { + case .loading: + state = .loading + case let .loaded(marketInfos): + let sortBy: MarketModule.SortBy + + switch sortOrder { + case .asc: sortBy = .lowestVolume + case .desc: sortBy = .highestVolume + } + + state = .loaded(marketInfos: marketInfos.sorted(sortBy: sortBy, timePeriod: .day1)) + case let .failed(error): + state = .failed(error: error) + } + } +} + +extension MarketVolumeViewModel { + var currency: Currency { + currencyManager.baseCurrency + } + + func sync() { + tasks = Set() + + if case .failed = internalState { + internalState = .loading + } + + Task { [weak self, marketKit, currency] in + do { + let marketInfos = try await marketKit.marketInfos(top: MarketModule.Top.top100.rawValue, currencyCode: currency.code) + + await MainActor.run { [weak self] in + self?.internalState = .loaded(marketInfos: marketInfos) + } + } catch { + await MainActor.run { [weak self] in + self?.internalState = .failed(error: error) + } + } + } + .store(in: &tasks) + } +} + +extension MarketVolumeViewModel { + enum State { + case loading + case loaded(marketInfos: [MarketInfo]) + case failed(error: Error) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistSignalBadge.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistSignalBadge.swift new file mode 100644 index 0000000000..6600998459 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistSignalBadge.swift @@ -0,0 +1,37 @@ +import MarketKit +import SwiftUI + +struct MarketWatchlistSignalBadge: View { + let signal: TechnicalAdvice.Advice + + var body: some View { + Text(signal.shortTitle) + .font(.themeMicroSB) + .foregroundColor(foregroundColor) + .padding(.horizontal, .margin6) + .padding(.vertical, .margin2) + .background(RoundedRectangle(cornerRadius: .cornerRadius8, style: .continuous).fill(backgroundColor)) + .clipShape(RoundedRectangle(cornerRadius: .cornerRadius8, style: .continuous)) + } + + private var foregroundColor: Color { + switch signal { + case .neutral: return .themeBran + case .buy: return .themeRemus + case .sell: return .themeLucian + case .strongBuy, .strongSell: return .themeTyler + case .overbought, .oversold: return .themeJacob + } + } + + private var backgroundColor: Color { + switch signal { + case .neutral: return .themeSteel20 + case .buy: return .themeGreen.opacity(0.2) + case .sell: return .themeRed.opacity(0.2) + case .strongBuy: return .themeRemus + case .strongSell: return .themeLucian + case .overbought, .oversold: return .themeYellow20 + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistSignalsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistSignalsView.swift new file mode 100644 index 0000000000..6439a7c018 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistSignalsView.swift @@ -0,0 +1,94 @@ +import MarketKit +import SwiftUI + +struct MarketWatchlistSignalsView: View { + @ObservedObject var viewModel: MarketWatchlistViewModel + @Binding var isPresented: Bool + + @State private var maxBadgeWidth: CGFloat = .zero + + var body: some View { + ThemeNavigationView { + ThemeView { + BottomGradientWrapper { + ScrollView { + VStack(spacing: .margin16) { + Text("market.watchlist.signals.description".localized) + .themeSubhead2() + .padding(EdgeInsets(top: 0, leading: .margin16, bottom: .margin8, trailing: .margin16)) + + ListSection { + row(signal: .strongBuy) + row(signal: .buy) + row(signal: .neutral) + row(signal: .sell) + row(signal: .strongSell) + row(signal: .overbought) + } + .themeListStyle(.bordered) + .onPreferenceChange(MaxWidthPreferenceKey.self) { + maxBadgeWidth = $0 + } + + HighlightedTextView(text: "market.watchlist.signals.warning".localized, style: .warning) + } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) + } + } bottomContent: { + Button(action: { + viewModel.showSignals = true + isPresented = false + }) { + Text("market.watchlist.signals.turn_on".localized) + } + .buttonStyle(PrimaryButtonStyle(style: .yellow)) + } + } + .navigationTitle("market.watchlist.signals".localized) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("button.cancel".localized) { + isPresented = false + } + } + } + } + } + + @ViewBuilder private func row(signal: TechnicalAdvice.Advice) -> some View { + ListRow { + MarketWatchlistSignalBadge(signal: signal) + .background( + GeometryReader { geometry in + Color.clear.preference(key: MaxWidthPreferenceKey.self, value: geometry.size.width) + } + .scaledToFill() + ) + .frame(width: maxBadgeWidth) + + Text(description(signal: signal)).themeSubhead2(color: .themeLeah) + } + } + + private func description(signal: TechnicalAdvice.Advice) -> String { + switch signal { + case .neutral: return "market.watchlist.signals.neutral.description".localized + case .buy: return "market.watchlist.signals.buy.description".localized + case .sell: return "market.watchlist.signals.sell.description".localized + case .strongBuy: return "market.watchlist.signals.strong_buy.description".localized + case .strongSell: return "market.watchlist.signals.strong_sell.description".localized + case .overbought, .oversold: return "market.watchlist.signals.risky.description".localized + } + } + + private struct MaxWidthPreferenceKey: PreferenceKey { + static var defaultValue: CGFloat = .zero + + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + let nextValue = nextValue() + guard nextValue > value else { return } + value = nextValue + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistView.swift new file mode 100644 index 0000000000..d4e0f74b00 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistView.swift @@ -0,0 +1,228 @@ +import Kingfisher +import MarketKit +import SwiftUI + +struct MarketWatchlistView: View { + @ObservedObject var viewModel: MarketWatchlistViewModel + + @State private var sortBySelectorPresented = false + @State private var timePeriodSelectorPresented = false + @State private var presentedFullCoin: FullCoin? + @State private var signalsPresented = false + + @State private var editMode: EditMode = .inactive + + var body: some View { + ThemeView { + switch viewModel.state { + case .loading: + VStack(spacing: 0) { + header(disabled: true) + loadingList() + } + case let .loaded(marketInfos, signals): + if marketInfos.isEmpty { + PlaceholderViewNew(image: Image("rate_48"), text: "market.watchlist.empty".localized) + } else { + VStack(spacing: 0) { + header() + list(marketInfos: marketInfos, signals: signals) + } + } + case .failed: + SyncErrorView { + Task { + await viewModel.refresh() + } + } + } + } + .sheet(item: $presentedFullCoin) { fullCoin in + CoinPageViewNew(coinUid: fullCoin.coin.uid).ignoresSafeArea() + .onFirstAppear { stat(page: .markets, section: .watchlist, event: .openCoin(coinUid: fullCoin.coin.uid)) } + } + } + + @ViewBuilder private func header(disabled: Bool = false) -> some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + Button(action: { + sortBySelectorPresented = true + }) { + Text(viewModel.sortBy.title) + } + .buttonStyle(SecondaryButtonStyle(style: .default, rightAccessory: .dropDown)) + .disabled(disabled) + + if viewModel.sortBy == .manual { + Button(action: { + if editMode == .active { + editMode = .inactive + } else { + editMode = .active + } + }) { + Image("edit2_20").renderingMode(.template) + } + .buttonStyle(SecondaryCircleButtonStyle(style: .default, isActive: editMode == .active)) + .disabled(disabled) + } + + Button(action: { + timePeriodSelectorPresented = true + }) { + Text(viewModel.timePeriod.shortTitle) + } + .buttonStyle(SecondaryButtonStyle(style: .default, rightAccessory: .dropDown)) + .disabled(disabled) + + if viewModel.showSignals { + signalsButton() + .buttonStyle(SecondaryActiveButtonStyle()) + .disabled(disabled) + } else { + signalsButton() + .buttonStyle(SecondaryButtonStyle()) + .disabled(disabled) + } + } + .padding(.horizontal, .margin16) + .padding(.vertical, .margin8) + } + .alert( + isPresented: $sortBySelectorPresented, + title: "market.sort_by.title".localized, + viewItems: WatchlistSortBy.allCases.map { .init(text: $0.title, selected: viewModel.sortBy == $0) }, + onTap: { index in + guard let index else { + return + } + + viewModel.sortBy = WatchlistSortBy.allCases[index] + } + ) + .alert( + isPresented: $timePeriodSelectorPresented, + title: "market.time_period.title".localized, + viewItems: viewModel.timePeriods.map { .init(text: $0.title, selected: viewModel.timePeriod == $0) }, + onTap: { index in + guard let index else { + return + } + + viewModel.timePeriod = viewModel.timePeriods[index] + } + ) + .sheet(isPresented: $signalsPresented) { + MarketWatchlistSignalsView(viewModel: viewModel, isPresented: $signalsPresented) + } + } + + @ViewBuilder private func signalsButton() -> some View { + Button(action: { + if viewModel.showSignals { + viewModel.showSignals = false + } else { + signalsPresented = true + } + }) { + Text("market.watchlist.signals".localized) + } + } + + @ViewBuilder private func list(marketInfos: [MarketInfo], signals: [String: TechnicalAdvice.Advice]) -> some View { + ScrollViewReader { proxy in + ThemeList( + marketInfos, + invisibleTopView: true, + onMove: viewModel.sortBy == .manual ? { source, destination in + viewModel.move(source: source, destination: destination) + } : nil + ) { marketInfo in + let coin = marketInfo.fullCoin.coin + + ClickableRow(action: { + presentedFullCoin = marketInfo.fullCoin + }) { + itemContent( + coin: coin, + marketCap: marketInfo.marketCap, + price: marketInfo.price.flatMap { ValueFormatter.instance.formatFull(currency: viewModel.currency, value: $0) } ?? "n/a".localized, + rank: marketInfo.marketCapRank, + diff: marketInfo.priceChangeValue(timePeriod: viewModel.timePeriod), + signal: viewModel.showSignals ? signals[coin.uid] : nil + ) + } + .swipeActions { + Button(role: .destructive) { + viewModel.remove(coinUid: coin.uid) + } label: { + Image("star_off_24").renderingMode(.template) + } + .tint(.themeLucian) + } + } + .environment(\.editMode, $editMode) + .refreshable { + await viewModel.refresh() + } + .animation(.default, value: editMode) + .onChange(of: viewModel.sortBy) { _ in + editMode = .inactive + withAnimation { proxy.scrollTo(themeListTopViewId) } + } + .onChange(of: viewModel.timePeriod) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + } + } + + @ViewBuilder private func loadingList() -> some View { + ThemeList(Array(0 ... 10)) { index in + ListRow { + itemContent( + coin: nil, + marketCap: 123_456, + price: "$123.45", + rank: 12, + diff: index % 2 == 0 ? 12.34 : -12.34, + signal: nil + ) + .redacted() + } + } + .themeListStyle(.transparent) + .simultaneousGesture(DragGesture(minimumDistance: 0), including: .all) + } + + @ViewBuilder private func itemContent(coin: Coin?, marketCap: Decimal?, price: String, rank: Int?, diff: Decimal?, signal: TechnicalAdvice.Advice?) -> some View { + CoinIconView(coin: coin) + + VStack(spacing: 1) { + HStack(spacing: .margin8) { + HStack(spacing: .margin8) { + Text(coin?.code ?? "CODE").textBody() + + if let signal { + MarketWatchlistSignalBadge(signal: signal) + } + } + + Spacer() + Text(price).textBody() + } + + HStack(spacing: .margin8) { + HStack(spacing: .margin4) { + if let rank { + BadgeViewNew(text: "\(rank)") + } + + if let marketCap, let formatted = ValueFormatter.instance.formatShort(currency: viewModel.currency, value: marketCap) { + Text(formatted).textSubhead2() + } + } + Spacer() + DiffText(diff) + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistViewModel.swift new file mode 100644 index 0000000000..577a0282f8 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistViewModel.swift @@ -0,0 +1,200 @@ +import Combine +import Foundation +import HsExtensions +import MarketKit + +class MarketWatchlistViewModel: ObservableObject { + private let marketKit = App.shared.marketKit + private let currencyManager = App.shared.currencyManager + private let watchlistManager = App.shared.watchlistManager + private let userDefaultsStorage = App.shared.userDefaultsStorage + private let appManager = App.shared.appManager + private var cancellables = Set() + private var tasks = Set() + + private var coinUids = [String]() + + private var internalState: State = .loading { + didSet { + syncState() + } + } + + @Published var state: State = .loading + + @Published var sortBy: WatchlistSortBy { + didSet { + stat(page: .markets, section: .watchlist, event: .switchSortType(sortType: sortBy.statSortType)) + syncState() + watchlistManager.sortBy = sortBy + } + } + + @Published var timePeriod: WatchlistTimePeriod { + didSet { + stat(page: .markets, section: .watchlist, event: .switchPeriod(period: timePeriod.statPeriod)) + syncState() + + if timePeriod != oldValue { + watchlistManager.timePeriod = timePeriod + } + } + } + + @Published var showSignals: Bool { + didSet { + stat(page: .markets, section: .watchlist, event: .showSignals(shown: showSignals)) + syncState() + watchlistManager.showSignals = showSignals + } + } + + init() { + sortBy = watchlistManager.sortBy + timePeriod = watchlistManager.timePeriod + showSignals = watchlistManager.showSignals + + watchlistManager.$timePeriod + .sink { [weak self] timePeriod in + self?.timePeriod = timePeriod + } + .store(in: &cancellables) + } + + private func syncCoinUids() { + let coinUids = watchlistManager.coinUids + + if case .loaded = internalState, coinUids == self.coinUids { + return + } + + self.coinUids = coinUids + + if case let .loaded(marketInfos, signals) = internalState { + let newMarketInfos = marketInfos.filter { marketInfo in + coinUids.contains(marketInfo.fullCoin.coin.uid) + } + + if newMarketInfos.count == coinUids.count { + internalState = .loaded(marketInfos: newMarketInfos, signals: signals) + return + } + } + + syncMarketInfos() + } + + private func syncMarketInfos() { + tasks = Set() + + Task { [weak self] in + await self?._syncMarketInfos() + }.store(in: &tasks) + } + + private func _syncMarketInfos() async { + if coinUids.isEmpty { + await MainActor.run { [weak self] in + self?.internalState = .loaded(marketInfos: [], signals: [:]) + } + return + } + + if case .failed = internalState { + await MainActor.run { [weak self] in + self?.internalState = .loading + } + } + + do { + async let _marketInfos = try marketKit.marketInfos(coinUids: coinUids, currencyCode: currency.code) + async let _signals = try marketKit.signals(coinUids: coinUids) + + let (marketInfos, signals) = try await (_marketInfos, _signals) + + let marketInfoMap = marketInfos.reduce(into: [String: MarketInfo]()) { $0[$1.fullCoin.coin.uid] = $1 } + let orderedMarketInfos = coinUids.compactMap { marketInfoMap[$0] } + + await MainActor.run { [weak self] in + self?.internalState = .loaded(marketInfos: orderedMarketInfos, signals: signals) + } + } catch { + await MainActor.run { [weak self] in + self?.internalState = .failed(error: error) + } + } + } + + private func syncState() { + switch internalState { + case .loading: + state = .loading + case let .loaded(marketInfos, signals): + state = .loaded(marketInfos: marketInfos.sorted(sortBy: sortBy, timePeriod: timePeriod), signals: signals) + case let .failed(error): + state = .failed(error: error) + } + } +} + +extension MarketWatchlistViewModel { + var currency: Currency { + currencyManager.baseCurrency + } + + var timePeriods: [WatchlistTimePeriod] { + watchlistManager.timePeriods + } + + func load() { + currencyManager.$baseCurrency + .sink { [weak self] _ in + self?.syncMarketInfos() + } + .store(in: &cancellables) + + appManager.willEnterForegroundPublisher + .sink { [weak self] in self?.syncMarketInfos() } + .store(in: &cancellables) + + watchlistManager.coinUidsPublisher + .sink { [weak self] _ in self?.syncCoinUids() } + .store(in: &cancellables) + + syncCoinUids() + } + + func refresh() async { + await _syncMarketInfos() + } + + func remove(coinUid: String) { + watchlistManager.remove(coinUid: coinUid) + stat(page: .markets, section: .watchlist, event: .removeFromWatchlist(coinUid: coinUid)) + } + + func move(source: IndexSet, destination: Int) { + guard case let .loaded(marketInfos, signals) = internalState else { + return + } + + var newCoinUids = coinUids + var newMarketInfos = marketInfos + + newCoinUids.move(fromOffsets: source, toOffset: destination) + newMarketInfos.move(fromOffsets: source, toOffset: destination) + + coinUids = newCoinUids + internalState = .loaded(marketInfos: newMarketInfos, signals: signals) + + watchlistManager.set(coinUids: coinUids) + } +} + +extension MarketWatchlistViewModel { + enum State { + case loading + case loaded(marketInfos: [MarketInfo], signals: [String: TechnicalAdvice.Advice]) + case failed(error: Error) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/WatchlistModifier.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/WatchlistModifier.swift new file mode 100644 index 0000000000..a257dd8456 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/WatchlistModifier.swift @@ -0,0 +1,33 @@ +import SwiftUI + +struct WatchlistModifier: ViewModifier { + @ObservedObject var viewModel: WatchlistViewModel + let coinUid: String + + func body(content: Content) -> some View { + content + .swipeActions { + if viewModel.coinUids.contains(coinUid) { + Button { + viewModel.remove(coinUid: coinUid) + } label: { + Image("star_off_24").renderingMode(.template) + } + .tint(.themeLucian) + } else { + Button { + viewModel.add(coinUid: coinUid) + } label: { + Image("star_24").renderingMode(.template) + } + .tint(.themeJacob) + } + } + } +} + +extension View { + func watchlistSwipeActions(viewModel: WatchlistViewModel, coinUid: String) -> some View { + modifier(WatchlistModifier(viewModel: viewModel, coinUid: coinUid)) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/WatchlistViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/WatchlistViewModel.swift new file mode 100644 index 0000000000..9643d04e02 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/WatchlistViewModel.swift @@ -0,0 +1,38 @@ +import Combine +import ComponentKit + +class WatchlistViewModel: ObservableObject { + private let watchlistManager = App.shared.watchlistManager + private let page: StatPage + private let section: StatSection? + private var cancellables = Set() + + @Published var coinUids: Set + + init(page: StatPage, section: StatSection? = nil) { + coinUids = Set(watchlistManager.coinUids) + + self.page = page + self.section = section + + watchlistManager.coinUidsPublisher + .sink { [weak self] in self?.coinUids = Set($0) } + .store(in: &cancellables) + } +} + +extension WatchlistViewModel { + func add(coinUid: String) { + watchlistManager.add(coinUid: coinUid) + HudHelper.instance.show(banner: .addedToWatchlist) + + stat(page: page, section: section, event: .addToWatchlist(coinUid: coinUid)) + } + + func remove(coinUid: String) { + watchlistManager.remove(coinUid: coinUid) + HudHelper.instance.show(banner: .removedFromWatchlist) + + stat(page: page, section: section, event: .removeFromWatchlist(coinUid: coinUid)) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartFactory.swift index e467a20489..26020cd1a0 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartFactory.swift @@ -6,9 +6,12 @@ class MetricChartFactory { private static let noChangesLimitPercent: Decimal = 0.2 private let dateFormatter = DateFormatter() + private let currencyManager = App.shared.currencyManager + private let hardcodedRightMode: String? - init(currentLocale: Locale) { + init(currentLocale: Locale, hardcodedRightMode: String? = nil) { dateFormatter.locale = currentLocale + self.hardcodedRightMode = hardcodedRightMode } private static func format(value: Decimal?, valueType: MetricChartModule.ValueType, exactlyValue: Bool = false) -> String? { @@ -18,7 +21,7 @@ class MetricChartFactory { switch valueType { case .percent: - return ValueFormatter.instance.format(percentValue: value, showSign: false) + return ValueFormatter.instance.format(percentValue: value, signType: .never) case let .currencyValue(currency): return ValueFormatter.instance.formatFull(currency: currency, value: value) case .counter: @@ -37,9 +40,9 @@ class MetricChartFactory { return [valueString, coin.code].compactMap { $0 }.joined(separator: " ") case let .compactCurrencyValue(currency): if exactlyValue { - return ValueFormatter.instance.formatFull(currency: currency, value: value) + return ValueFormatter.instance.formatFull(currency: currency, value: value, signType: .always) } else { - return ValueFormatter.instance.formatShort(currency: currency, value: value) + return ValueFormatter.instance.formatShort(currency: currency, value: value, signType: .auto) } } } @@ -73,18 +76,38 @@ extension MetricChartFactory { let chartTrend: MovementTrend var value: String? - var valueDiff: Decimal? + var valueDiff: ValueDiff? var rightSideMode: ChartModule.RightSideMode = .none switch itemData.type { case .regular: value = Self.format(value: lastItem.value, valueType: valueType) - chartTrend = (lastItem.value - firstItem.value).isSignMinus ? .down : .up - valueDiff = (lastItem.value - firstItem.value) / firstItem.value * 100 + let diff = (lastItem.value - firstItem.value) / firstItem.value * 100 + chartTrend = diff.isSignMinus ? .down : .up - if let first = itemData.indicators[MarketGlobalModule.dominance]?.first, let last = itemData.indicators[MarketGlobalModule.dominance]?.last { + let valueString = ValueFormatter.instance.format(percentValue: diff, signType: .always) + valueDiff = valueString.map { ValueDiff(value: $0, trend: chartTrend) } + + if let hardcodedRightMode { + rightSideMode = .custom(title: hardcodedRightMode, value: nil) + } else if let first = itemData.indicators[MarketGlobalModule.dominance]?.first, let last = itemData.indicators[MarketGlobalModule.dominance]?.last { rightSideMode = .dominance(value: last, diff: (last - first) / first * 100) } + case .etf: + if let last = itemData.indicators[MarketGlobalModule.totalInflow]?.last { // etf chart + value = Self.format(value: last, valueType: valueType) + } + + let valueString = ValueFormatter.instance.formatShort(currency: currencyManager.baseCurrency, value: lastItem.value, signType: .always) + valueDiff = valueString.map { ValueDiff(value: $0, trend: lastItem.value.isSignMinus ? .down : .up) } + chartTrend = .neutral + + if let last = itemData.indicators[MarketGlobalModule.totalAssets]?.last { + rightSideMode = .custom( + title: "market.etf.total_net_assets".localized, + value: ValueFormatter.instance.formatShort(currency: currencyManager.baseCurrency, value: last) + ) + } case let .aggregated(aggregatedValue): value = Self.format(value: aggregatedValue, valueType: valueType) chartTrend = .neutral @@ -111,6 +134,24 @@ extension MetricChartFactory { indicators.append(dominanceIndicator) } + if let totalAssets = itemData.indicators[MarketGlobalModule.totalAssets] { + let totalIndicator = PrecalculatedIndicator( + id: MarketGlobalModule.totalAssets, + enabled: true, + values: totalAssets, + configuration: ChartIndicator.LineConfiguration.totalAssets + ) + indicators.append(totalIndicator) + } + if let totalInflow = itemData.indicators[MarketGlobalModule.totalInflow] { + let totalIndicator = PrecalculatedIndicator( + id: MarketGlobalModule.totalInflow, + enabled: false, + values: totalInflow, + configuration: ChartIndicator.LineConfiguration.totalInflow + ) + indicators.append(totalIndicator) + } return ChartModule.ViewItem( value: value, @@ -131,16 +172,34 @@ extension MetricChartFactory { let date = Date(timeIntervalSince1970: chartItem.timestamp) let formattedDate = DateHelper.instance.formatFullTime(from: date) - let formattedValue = Self.format(value: value, valueType: valueType) var rightSideMode: ChartModule.RightSideMode = .none if let dominance = chartItem.indicators[ChartIndicator.LineConfiguration.dominanceId] { rightSideMode = .dominance(value: dominance, diff: nil) + } else if let totalAssets = chartItem.indicators[ChartIndicator.LineConfiguration.totalAssetId] { + let value = ValueFormatter.instance.formatShort(currency: currencyManager.baseCurrency, value: totalAssets) + rightSideMode = .custom(title: "market.etf.total_net_assets".localized, value: value) } else if let volume = chartItem.indicators[ChartData.volume] { rightSideMode = .volume(value: Self.format(value: volume, valueType: valueType)) } + // if etf chart + if let totalInflow = chartItem.indicators[ChartIndicator.LineConfiguration.totalInflowId] { + let formattedValue = ValueFormatter.instance.formatShort(currency: currencyManager.baseCurrency, value: totalInflow) + let diffString = ValueFormatter.instance.formatShort(currency: currencyManager.baseCurrency, value: value, signType: .always) + let diff = diffString.map { ValueDiff(value: $0, trend: value.isSignMinus ? .down : .up) } + + return ChartModule.SelectedPointViewItem( + value: formattedValue, + diff: diff, + date: formattedDate, + rightSideMode: rightSideMode + ) + } + + let formattedValue = Self.format(value: value, valueType: valueType) + return ChartModule.SelectedPointViewItem( value: formattedValue, date: formattedDate, diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartModule.swift index f8cccb2b53..f07e4ce333 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartModule.swift @@ -48,6 +48,7 @@ enum MetricChartModule { enum ItemType { case regular + case etf case aggregated(value: Decimal?) } @@ -74,7 +75,7 @@ enum MetricChartModule { extension HsPeriodType { var title: String { switch self { - case let .byPeriod(interval): return interval.title + case let .byPeriod(interval): return interval.shortTitle default: return "chart.time_duration.all".localized } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartViewModel.swift index dc6cd3a5cc..fb12b97092 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartViewModel.swift @@ -7,7 +7,7 @@ import RxCocoa import RxRelay import RxSwift -class MetricChartViewModel { +class MetricChartViewModel: ObservableObject { private let service: MetricChartService private let factory: MetricChartFactory private var cancellables = Set() @@ -19,9 +19,12 @@ class MetricChartViewModel { private let errorRelay = BehaviorRelay(value: false) private let needUpdateIntervalsRelay = BehaviorRelay(value: 0) + @Published var periodType: HsPeriodType + init(service: MetricChartService, factory: MetricChartFactory) { self.service = service self.factory = factory + periodType = service.interval service.$interval .sink { [weak self] in self?.sync(interval: $0) } @@ -100,15 +103,16 @@ extension MetricChartViewModel: IChartViewModel { errorRelay.asDriver() } - var intervals: [String] { service.intervals.timePeriods.map { $0.title.uppercased() } } + var intervals: [String] { service.intervals.timePeriods.map { $0.shortTitle.uppercased() } } func onSelectInterval(at index: Int) { let chartTypes = service.intervals guard chartTypes.count > index else { return } - - service.interval = chartTypes[index] + let interval = chartTypes[index] + service.interval = interval + periodType = interval } func start() { @@ -139,3 +143,36 @@ extension MetricChartViewModel: IChartViewTouchDelegate { pointSelectedItemRelay.accept(nil) } } + +extension MetricChartViewModel { + static func instance(type: MarketGlobalModule.MetricsType) -> MetricChartViewModel { + let fetcher = MarketGlobalFetcher(currencyManager: App.shared.currencyManager, marketKit: App.shared.marketKit, metricsType: type) + let service = MetricChartService( + chartFetcher: fetcher, + interval: .byPeriod(.day1), + statPage: type.statPage + ) + + let factory = MetricChartFactory(currentLocale: LanguageManager.shared.currentLocale) + return MetricChartViewModel(service: service, factory: factory) + } + + static var etfInstance: MetricChartViewModel { + let fetcher = MarketEtfFetcher(marketKit: App.shared.marketKit, currencyManager: App.shared.currencyManager) + let service = MetricChartService( + chartFetcher: fetcher, + interval: .byPeriod(.day1), + statPage: StatPage.globalMetricsEtf + ) + + let factory = MetricChartFactory(currentLocale: LanguageManager.shared.currentLocale) + return MetricChartViewModel(service: service, factory: factory) + } + + static func platformInstance(platform: TopPlatform) -> MetricChartViewModel { + let marketCapFetcher = TopPlatformMarketCapFetcher(marketKit: App.shared.marketKit, currencyManager: App.shared.currencyManager, topPlatform: platform) + let chartService = MetricChartService(chartFetcher: marketCapFetcher, interval: .byPeriod(.week1), statPage: .topPlatform) + let factory = MetricChartFactory(currentLocale: LanguageManager.shared.currentLocale, hardcodedRightMode: "top_platform.total_cap".localized) + return MetricChartViewModel(service: chartService, factory: factory) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/AddressView/AddressViewNew.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/AddressView/AddressViewNew.swift index e105afdc95..007dba3090 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/AddressView/AddressViewNew.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/AddressView/AddressViewNew.swift @@ -61,10 +61,12 @@ struct AddressViewNew: View { ScanQrViewNew(pasteEnabled: true) { viewModel.didFetch(qrText: $0) } + .ignoresSafeArea() } .sheet(isPresented: $viewModel.contactsPresented) { if let blockchainType = viewModel.blockchainType { ContactBookView(mode: .select(blockchainType, viewModel), presented: true) + .ignoresSafeArea() } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Approve/MultiSwapApproveView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Approve/MultiSwapApproveView.swift index 69d3bcba6f..8eee8a02bd 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Approve/MultiSwapApproveView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Approve/MultiSwapApproveView.swift @@ -21,34 +21,36 @@ struct MultiSwapApproveView: View { ThemeView { if let transactionData = viewModel.transactionData { BottomGradientWrapper { - VStack(spacing: .margin12) { - Text("swap.unlock.subtitle".localized) - .themeHeadline1() - .padding(.horizontal, .margin16) - .padding(.bottom, .margin12) + ScrollView { + VStack(spacing: .margin12) { + Text("swap.unlock.subtitle".localized) + .themeHeadline1() + .padding(.horizontal, .margin16) + .padding(.bottom, .margin12) - ListSection { - ClickableRow(action: { - viewModel.set(unlimitedAmount: false) - }) { - let coinValue = CoinValue(kind: .token(token: viewModel.token), value: viewModel.amount) - let amountString = ValueFormatter.instance.formatFull(coinValue: coinValue) ?? "" - row(text: amountString, selected: !viewModel.unlimitedAmount) + ListSection { + ClickableRow(action: { + viewModel.set(unlimitedAmount: false) + }) { + let coinValue = CoinValue(kind: .token(token: viewModel.token), value: viewModel.amount) + let amountString = ValueFormatter.instance.formatFull(coinValue: coinValue) ?? "" + row(text: amountString, selected: !viewModel.unlimitedAmount) + } + ClickableRow(action: { + viewModel.set(unlimitedAmount: true) + }) { + row(text: "swap.unlock.unlimited".localized, selected: viewModel.unlimitedAmount) + } } - ClickableRow(action: { - viewModel.set(unlimitedAmount: true) - }) { - row(text: "swap.unlock.unlimited".localized, selected: viewModel.unlimitedAmount) - } - } - Text("swap.unlock.description".localized) - .themeSubhead2() - .padding(.horizontal, .margin16) + Text("swap.unlock.description".localized) + .themeSubhead2() + .padding(.horizontal, .margin16) - Spacer() + Spacer() + } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) } - .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) } bottomContent: { Button(action: { unlockPresented = true @@ -61,7 +63,7 @@ struct MultiSwapApproveView: View { NavigationLink( isActive: $unlockPresented, destination: { - SendConfirmationNewView(sendData: .evm(blockchainType: viewModel.token.blockchainType, transactionData: transactionData)) { + RegularSendView(sendData: .evm(blockchainType: viewModel.token.blockchainType, transactionData: transactionData)) { onSuccess() isPresented = false } @@ -84,14 +86,7 @@ struct MultiSwapApproveView: View { @ViewBuilder func row(text: String, selected: Bool) -> some View { HStack(spacing: .margin16) { - Image("check_2_20") - .themeIcon(color: .themeJacob) - .opacity(selected ? 1 : 0) - .frame(width: .iconSize20, height: .iconSize20, alignment: .center) - .overlay( - RoundedRectangle(cornerRadius: .cornerRadius4, style: .continuous) - .stroke(Color.themeGray, lineWidth: .heightOneDp + .heightOnePixel) - ) + CheckBoxUiView(checked: .init(get: { selected }, set: { _ in })) Text(text).themeSubhead2(color: .themeLeah) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Approve/MultiSwapApproveViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Approve/MultiSwapApproveViewModel.swift index 27cf42a96d..28dd2927f7 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Approve/MultiSwapApproveViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Approve/MultiSwapApproveViewModel.swift @@ -18,8 +18,6 @@ class MultiSwapApproveViewModel: ObservableObject { self.spenderAddress = spenderAddress approveDataProvider = App.shared.adapterManager.adapter(for: token) as? IApproveDataProvider } - - private func syncState() {} } extension MultiSwapApproveViewModel { @@ -34,13 +32,5 @@ extension MultiSwapApproveViewModel { } self.unlimitedAmount = unlimitedAmount - - syncState() - } -} - -extension MultiSwapApproveViewModel { - enum InitError: Error { - case noApproveDataProvider } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Approve/MultiSwapRevokeView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Approve/MultiSwapRevokeView.swift index 77beb6d67a..fcb8e692b3 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Approve/MultiSwapRevokeView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Approve/MultiSwapRevokeView.swift @@ -19,7 +19,7 @@ struct MultiSwapRevokeView: View { var body: some View { if let transactionData = viewModel.transactionData { - SendConfirmationNewView(sendData: .evm(blockchainType: viewModel.token.blockchainType, transactionData: transactionData)) { + RegularSendView(sendData: .evm(blockchainType: viewModel.token.blockchainType, transactionData: transactionData)) { onSuccess() isPresented = false } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/IMultiSwapConfirmationQuote.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/IMultiSwapConfirmationQuote.swift index 10da1decb5..15dc2bce23 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/IMultiSwapConfirmationQuote.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/IMultiSwapConfirmationQuote.swift @@ -5,7 +5,7 @@ protocol IMultiSwapConfirmationQuote { var amountOut: Decimal { get } var feeData: FeeData? { get } var canSwap: Bool { get } - func cautions(feeToken: Token?) -> [CautionNew] - func priceSectionFields(tokenIn: Token, tokenOut: Token, feeToken: Token?, currency: Currency, tokenInRate: Decimal?, tokenOutRate: Decimal?, feeTokenRate: Decimal?) -> [SendConfirmField] - func otherSections(tokenIn: Token, tokenOut: Token, feeToken: Token?, currency: Currency, tokenInRate: Decimal?, tokenOutRate: Decimal?, feeTokenRate: Decimal?) -> [[SendConfirmField]] + func cautions(baseToken: Token) -> [CautionNew] + func priceSectionFields(tokenIn: Token, tokenOut: Token, baseToken: Token, currency: Currency, tokenInRate: Decimal?, tokenOutRate: Decimal?, baseTokenRate: Decimal?) -> [SendField] + func otherSections(tokenIn: Token, tokenOut: Token, baseToken: Token, currency: Currency, tokenInRate: Decimal?, tokenOutRate: Decimal?, baseTokenRate: Decimal?) -> [[SendField]] } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/MultiSwapConfirmationView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/MultiSwapConfirmationView.swift deleted file mode 100644 index d5c6dbf464..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/MultiSwapConfirmationView.swift +++ /dev/null @@ -1,227 +0,0 @@ -import ComponentKit -import Kingfisher -import MarketKit -import SwiftUI - -struct MultiSwapConfirmationView: View { - @StateObject private var viewModel: MultiSwapConfirmationViewModel - @Binding private var swapPresentationMode: PresentationMode - - @State private var feeSettingsPresented = false - - init(tokenIn: Token, tokenOut: Token, amountIn: Decimal, provider: IMultiSwapProvider, swapPresentationMode: Binding) { - _viewModel = .init(wrappedValue: MultiSwapConfirmationViewModel(tokenIn: tokenIn, tokenOut: tokenOut, amountIn: amountIn, provider: provider)) - _swapPresentationMode = swapPresentationMode - } - - var body: some View { - ThemeView { - switch viewModel.state { - case .quoting: - VStack(spacing: .margin12) { - ProgressView() - Text("swap.confirmation.quoting".localized).textSubhead2() - } - case let .success(quote): - quoteView(quote: quote) - case let .failed(error): - errorView(error: error) - } - } - .sheet(isPresented: $feeSettingsPresented) { - if let transactionService = viewModel.transactionService, let feeToken = viewModel.feeToken { - transactionService.settingsView( - feeData: Binding(get: { viewModel.state.quote?.feeData }, set: { _ in }), - loading: Binding(get: { viewModel.state.isQuoting }, set: { _ in }), - feeToken: feeToken, - currency: viewModel.currency, - feeTokenRate: $viewModel.feeTokenRate - ) - } - } - .navigationTitle("swap.confirmation.title".localized) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button(action: { - feeSettingsPresented = true - }) { - Image("manage_2_20").renderingMode(.template) - } - .disabled(viewModel.state.isQuoting) - } - } - .onReceive(viewModel.errorSubject) { error in - HudHelper.instance.showError(subtitle: error) - } - } - - @ViewBuilder private func quoteView(quote: IMultiSwapConfirmationQuote) -> some View { - VStack { - ScrollView { - VStack(spacing: .margin16) { - ListSection { - tokenRow(title: "swap.you_pay".localized, token: viewModel.tokenIn, amount: viewModel.amountIn, rate: viewModel.rateIn, type: .neutral) - tokenRow(title: "swap.you_get".localized, token: viewModel.tokenOut, amount: quote.amountOut, rate: viewModel.rateOut, type: .incoming) - } - - let priceSectionFields = quote.priceSectionFields( - tokenIn: viewModel.tokenIn, - tokenOut: viewModel.tokenOut, - feeToken: viewModel.feeToken, - currency: viewModel.currency, - tokenInRate: viewModel.rateIn, - tokenOutRate: viewModel.rateOut, - feeTokenRate: viewModel.feeTokenRate - ) - - if viewModel.price != nil || !priceSectionFields.isEmpty { - ListSection { - if let price = viewModel.price { - ListRow { - Text("swap.price".localized).textSubhead2() - - Spacer() - - Button(action: { - viewModel.flipPrice() - }) { - HStack(spacing: .margin8) { - Text(price) - .textSubhead1(color: .themeLeah) - .multilineTextAlignment(.trailing) - - Image("arrow_swap_3_20").themeIcon() - } - } - } - } - - if !priceSectionFields.isEmpty { - ForEach(priceSectionFields.indices, id: \.self) { index in - priceSectionFields[index].listRow - } - } - } - } - - let otherSections = quote.otherSections( - tokenIn: viewModel.tokenIn, - tokenOut: viewModel.tokenOut, - feeToken: viewModel.feeToken, - currency: viewModel.currency, - tokenInRate: viewModel.rateIn, - tokenOutRate: viewModel.rateOut, - feeTokenRate: viewModel.feeTokenRate - ) - - if !otherSections.isEmpty { - ForEach(otherSections.indices, id: \.self) { sectionIndex in - let section = otherSections[sectionIndex] - - if !section.isEmpty { - ListSection { - ForEach(section.indices, id: \.self) { index in - section[index].listRow - } - } - } - } - } - - let cautions = (viewModel.transactionService?.cautions ?? []) + quote.cautions(feeToken: viewModel.feeToken) - - if !cautions.isEmpty { - VStack(spacing: .margin12) { - ForEach(cautions.indices, id: \.self) { index in - HighlightedTextView(caution: cautions[index]) - } - } - } - } - .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) - } - - if viewModel.quoteTimeLeft > 0 || viewModel.swapping { - SlideButton( - styling: .text( - start: "swap.confirmation.slide_to_swap".localized, - end: "swap.confirmation.swapping".localized, - success: "swap.confirmation.swapped".localized - ), - action: { - try await viewModel.swap() - }, completion: { - HudHelper.instance.show(banner: .swapped) - swapPresentationMode.dismiss() - } - ) - .padding(.vertical, .margin16) - .padding(.horizontal, .margin16) - } else { - Button(action: { - viewModel.syncQuote() - }) { - Text("swap.confirmation.refresh".localized) - } - .buttonStyle(PrimaryButtonStyle(style: .gray)) - .padding(.vertical, .margin16) - .padding(.horizontal, .margin16) - } - - let (bottomText, bottomTextColor) = bottomText() - - Text(bottomText) - .textSubhead1(color: bottomTextColor) - .padding(.bottom, .margin8) - } - } - - @ViewBuilder private func errorView(error: Error) -> some View { - VStack { - ScrollView { - VStack(spacing: .margin16) { - HighlightedTextView(caution: CautionNew(text: error.smartDescription, type: .error)) - } - .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) - } - - Button(action: { - viewModel.syncQuote() - }) { - Text("swap.confirmation.refresh".localized) - } - .buttonStyle(PrimaryButtonStyle(style: .gray)) - .padding(.vertical, .margin16) - .padding(.horizontal, .margin16) - - Text("swap.confirmation.quote_failed".localized) - .textSubhead1() - .padding(.bottom, .margin8) - } - } - - @ViewBuilder private func tokenRow(title: String, token: Token, amount: Decimal, rate: Decimal?, type: SendConfirmField.AmountType) -> some View { - let field = SendConfirmField.amount( - title: title, - token: token, - coinValueType: .regular(coinValue: CoinValue(kind: .token(token: token), value: amount)), - currencyValue: rate.map { CurrencyValue(currency: viewModel.currency, value: amount * $0) }, - type: type - ) - - field.listRow - } - - private func bottomText() -> (String, Color) { - if let quote = viewModel.state.quote, !quote.canSwap { - return ("swap.confirmation.invalid_quote".localized, .themeGray) - } else if viewModel.swapping { - return ("swap.confirmation.please_wait".localized, .themeGray) - } else if viewModel.quoteTimeLeft > 0 { - return ("swap.confirmation.quote_expires_in".localized("\(viewModel.quoteTimeLeft)"), .themeJacob) - } else { - return ("swap.confirmation.quote_expired".localized, .themeGray) - } - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/MultiSwapConfirmationViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/MultiSwapConfirmationViewModel.swift deleted file mode 100644 index f9b373624d..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/MultiSwapConfirmationViewModel.swift +++ /dev/null @@ -1,230 +0,0 @@ -import Combine -import Foundation -import HsExtensions -import MarketKit - -class MultiSwapConfirmationViewModel: ObservableObject { - let quoteExpirationDuration: Int = 10 - - private let currencyManager = App.shared.currencyManager - private let marketKit = App.shared.marketKit - private let accountManager = App.shared.accountManager - private let walletManager = App.shared.walletManager - private let transactionServiceFactory = TransactionServiceFactory() - - private var quoteTask: AnyTask? - private var timer: AnyCancellable? - private var cancellables = Set() - - let tokenIn: Token - let tokenOut: Token - let amountIn: Decimal - let provider: IMultiSwapProvider - let transactionService: ITransactionService? - let currency: Currency - let feeToken: Token? - - @Published var transactionSettingsModified = false - - @Published var rateIn: Decimal? - @Published var rateOut: Decimal? - @Published var feeTokenRate: Decimal? - - @Published var state: State = .quoting { - didSet { - syncPrice() - - timer?.cancel() - - if let quote = state.quote, quote.canSwap { - quoteTimeLeft = quoteExpirationDuration - - timer = Timer.publish(every: 1, on: .main, in: .common) - .autoconnect() - .sink { [weak self] _ in - self?.handleTimerTick() - } - } - } - } - - @Published var quoteTimeLeft: Int = 0 - - @Published var price: String? - private var priceFlipped = false - - @Published var swapping = false - - let errorSubject = PassthroughSubject() - - init(tokenIn: Token, tokenOut: Token, amountIn: Decimal, provider: IMultiSwapProvider) { - self.tokenIn = tokenIn - self.tokenOut = tokenOut - self.amountIn = amountIn - self.provider = provider - - transactionService = transactionServiceFactory.transactionService(blockchainType: tokenIn.blockchainType) - - currency = currencyManager.baseCurrency - - feeToken = try? marketKit.token(query: TokenQuery(blockchainType: tokenIn.blockchainType, tokenType: .native)) - - transactionService?.updatePublisher - .sink { [weak self] in - self?.syncTransactionSettingsModified() - self?.syncQuote() - } - .store(in: &cancellables) - - if let feeToken { - feeTokenRate = marketKit.coinPrice(coinUid: feeToken.coin.uid, currencyCode: currency.code)?.value - marketKit.coinPricePublisher(tag: "swap", coinUid: feeToken.coin.uid, currencyCode: currency.code) - .receive(on: DispatchQueue.main) - .sink { [weak self] price in self?.feeTokenRate = price.value } - .store(in: &cancellables) - } - - rateIn = marketKit.coinPrice(coinUid: tokenIn.coin.uid, currencyCode: currency.code)?.value - marketKit.coinPricePublisher(tag: "swap", coinUid: tokenIn.coin.uid, currencyCode: currency.code) - .receive(on: DispatchQueue.main) - .sink { [weak self] price in self?.rateIn = price.value } - .store(in: &cancellables) - - rateOut = marketKit.coinPrice(coinUid: tokenOut.coin.uid, currencyCode: currency.code)?.value - marketKit.coinPricePublisher(tag: "swap", coinUid: tokenOut.coin.uid, currencyCode: currency.code) - .receive(on: DispatchQueue.main) - .sink { [weak self] price in self?.rateOut = price.value } - .store(in: &cancellables) - - syncQuote() - } - - private func handleTimerTick() { - quoteTimeLeft -= 1 - - if quoteTimeLeft == 0 { - timer?.cancel() - } - } - - private func syncPrice() { - if let quote = state.quote { - let amountOut = quote.amountOut - var showAsIn = amountIn < amountOut - - if priceFlipped { - showAsIn.toggle() - } - - let tokenA = showAsIn ? tokenIn : tokenOut - let tokenB = showAsIn ? tokenOut : tokenIn - let amountA = showAsIn ? amountIn : amountOut - let amountB = showAsIn ? amountOut : amountIn - - let formattedValue = ValueFormatter.instance.formatFull(value: amountB / amountA, decimalCount: tokenB.decimals) - price = formattedValue.map { "1 \(tokenA.coin.code) = \($0) \(tokenB.coin.code)" } - } else { - price = nil - } - } - - private func syncTransactionSettingsModified() { - transactionSettingsModified = transactionService?.modified ?? false - } - - @MainActor private func set(swapping: Bool) { - self.swapping = swapping - } -} - -extension MultiSwapConfirmationViewModel { - func syncQuote() { - guard let transactionService else { - return - } - - quoteTask = nil - - if !state.isQuoting { - state = .quoting - } - - quoteTask = Task { [weak self, tokenIn, tokenOut, amountIn, provider, transactionService] in - var state: State - - do { - try await transactionService.sync() - - let quote = try await provider.confirmationQuote( - tokenIn: tokenIn, - tokenOut: tokenOut, - amountIn: amountIn, - transactionSettings: transactionService.transactionSettings - ) - - state = .success(quote: quote) - } catch { - state = .failed(error: error) - } - - if !Task.isCancelled { - await MainActor.run { [weak self, state] in - self?.state = state - } - } - } - .erased() - } - - func flipPrice() { - priceFlipped.toggle() - syncPrice() - } - - func swap() async throws { - do { - guard let quote = state.quote else { - throw SwapError.noQuote - } - - await set(swapping: true) - - try await provider.swap(tokenIn: tokenIn, tokenOut: tokenOut, amountIn: amountIn, quote: quote) - - if !walletManager.activeWallets.contains(where: { $0.token == tokenOut }), let activeAccount = accountManager.activeAccount { - let wallet = Wallet(token: tokenOut, account: activeAccount) - walletManager.save(wallets: [wallet]) - } - } catch { - await set(swapping: false) - errorSubject.send(error.smartDescription) - throw error - } - } -} - -extension MultiSwapConfirmationViewModel { - enum State { - case quoting - case success(quote: IMultiSwapConfirmationQuote) - case failed(error: Error) - - var quote: IMultiSwapConfirmationQuote? { - switch self { - case let .success(quote): return quote - default: return nil - } - } - - var isQuoting: Bool { - switch self { - case .quoting: return true - default: return false - } - } - } - - enum SwapError: Error { - case noQuote - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/MultiSwapSendHandler.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/MultiSwapSendHandler.swift new file mode 100644 index 0000000000..ece27b8059 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/MultiSwapSendHandler.swift @@ -0,0 +1,180 @@ +import BigInt +import Eip20Kit +import EvmKit +import Foundation +import MarketKit + +class MultiSwapSendHandler { + private let currencyManager = App.shared.currencyManager + private let marketKit = App.shared.marketKit + private let accountManager = App.shared.accountManager + private let walletManager = App.shared.walletManager + + let baseToken: Token + let tokenIn: Token + let tokenOut: Token + let amountIn: Decimal + let provider: IMultiSwapProvider + + init(baseToken: Token, tokenIn: Token, tokenOut: Token, amountIn: Decimal, provider: IMultiSwapProvider) { + self.baseToken = baseToken + self.tokenIn = tokenIn + self.tokenOut = tokenOut + self.amountIn = amountIn + self.provider = provider + } +} + +extension MultiSwapSendHandler: ISendHandler { + var syncingText: String? { + "swap.confirmation.quoting".localized + } + + var expirationDuration: Int? { + 15 + } + + func sendData(transactionSettings: TransactionSettings?) async throws -> ISendData { + let quote = try await provider.confirmationQuote( + tokenIn: tokenIn, + tokenOut: tokenOut, + amountIn: amountIn, + transactionSettings: transactionSettings + ) + + return SendData(tokenIn: tokenIn, tokenOut: tokenOut, amountIn: amountIn, quote: quote) + } + + func send(data: ISendData) async throws { + guard let data = data as? SendData else { + throw SendError.invalidData + } + + try await provider.swap(tokenIn: tokenIn, tokenOut: tokenOut, amountIn: amountIn, quote: data.quote) + + if !walletManager.activeWallets.contains(where: { $0.token == tokenOut }), let activeAccount = accountManager.activeAccount { + let wallet = Wallet(token: tokenOut, account: activeAccount) + walletManager.save(wallets: [wallet]) + } + } +} + +extension MultiSwapSendHandler { + class SendData: ISendData { + let tokenIn: Token + let tokenOut: Token + let amountIn: Decimal + let quote: IMultiSwapConfirmationQuote + + init(tokenIn: Token, tokenOut: Token, amountIn: Decimal, quote: IMultiSwapConfirmationQuote) { + self.tokenIn = tokenIn + self.tokenOut = tokenOut + self.amountIn = amountIn + self.quote = quote + } + + var feeData: FeeData? { + quote.feeData + } + + var canSend: Bool { + quote.canSwap + } + + var rateCoins: [Coin] { + [tokenIn.coin, tokenOut.coin] + } + + var customSendButtonTitle: String? { + nil + } + + func cautions(baseToken: Token) -> [CautionNew] { + quote.cautions(baseToken: baseToken) + } + + func sections(baseToken: Token, currency: Currency, rates: [String: Decimal]) -> [[SendField]] { + var sections: [[SendField]] = [ + [ + .amount( + title: "swap.you_pay".localized, + token: tokenIn, + coinValueType: .regular(coinValue: CoinValue(kind: .token(token: tokenIn), value: amountIn)), + currencyValue: rates[tokenIn.coin.uid].map { CurrencyValue(currency: currency, value: amountIn * $0) }, + type: .neutral + ), + .amount( + title: "swap.you_get".localized, + token: tokenOut, + coinValueType: .regular(coinValue: CoinValue(kind: .token(token: tokenOut), value: quote.amountOut)), + currencyValue: rates[tokenOut.coin.uid].map { CurrencyValue(currency: currency, value: quote.amountOut * $0) }, + type: .incoming + ), + ], + ] + + var priceSection: [SendField] = [ + .price( + title: "swap.price".localized, + tokenA: tokenIn, + tokenB: tokenOut, + amountA: amountIn, + amountB: quote.amountOut + ), + ] + + let priceSectionFields = quote.priceSectionFields( + tokenIn: tokenIn, + tokenOut: tokenOut, + baseToken: baseToken, + currency: currency, + tokenInRate: rates[tokenIn.coin.uid], + tokenOutRate: rates[tokenOut.coin.uid], + baseTokenRate: rates[baseToken.coin.uid] + ) + + if !priceSectionFields.isEmpty { + priceSection.append(contentsOf: priceSectionFields) + } + + sections.append(priceSection) + + sections.append(contentsOf: quote.otherSections( + tokenIn: tokenIn, + tokenOut: tokenOut, + baseToken: baseToken, + currency: currency, + tokenInRate: rates[tokenIn.coin.uid], + tokenOutRate: rates[tokenOut.coin.uid], + baseTokenRate: rates[baseToken.coin.uid] + )) + + return sections + } + } + + enum SendError: Error { + case invalidData + } +} + +extension MultiSwapSendHandler { + static func instance(tokenIn: Token, tokenOut: Token, amountIn: Decimal, provider: IMultiSwapProvider) -> MultiSwapSendHandler? { + let baseToken: Token? + + switch tokenIn.type { + case .native, .derived, .addressType: + baseToken = tokenIn + case .eip20, .bep2, .spl: + baseToken = try? App.shared.marketKit.token(query: TokenQuery(blockchainType: tokenIn.blockchainType, tokenType: .native)) + case .unsupported: + baseToken = nil + } + + guard let baseToken else { + return nil + } + + return MultiSwapSendHandler(baseToken: baseToken, tokenIn: tokenIn, tokenOut: tokenOut, amountIn: amountIn, provider: provider) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/MultiSwapSendView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/MultiSwapSendView.swift new file mode 100644 index 0000000000..7968190a82 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/MultiSwapSendView.swift @@ -0,0 +1,74 @@ +import ComponentKit +import MarketKit +import SwiftUI + +struct MultiSwapSendView: View { + @StateObject var sendViewModel: SendViewModel + @Binding private var swapPresentationMode: PresentationMode + + init(tokenIn: Token, tokenOut: Token, amountIn: Decimal, provider: IMultiSwapProvider, swapPresentationMode: Binding) { + _sendViewModel = .init(wrappedValue: SendViewModel(sendData: .swap(tokenIn: tokenIn, tokenOut: tokenOut, amountIn: amountIn, provider: provider))) + _swapPresentationMode = swapPresentationMode + } + + var body: some View { + ThemeView { + BottomGradientWrapper { + SendView(viewModel: sendViewModel) + } bottomContent: { + switch sendViewModel.state { + case .syncing: + EmptyView() + case .success: + VStack(spacing: .margin24) { + if sendViewModel.timeLeft > 0 || sendViewModel.sending { + SlideButton( + styling: .text(start: "swap.confirmation.slide_to_swap".localized, end: "", success: ""), + action: { + try await sendViewModel.send() + }, completion: { + HudHelper.instance.show(banner: .swapped) + swapPresentationMode.dismiss() + } + ) + } else { + Button(action: { + sendViewModel.sync() + }) { + Text("send.confirmation.refresh".localized) + } + .buttonStyle(PrimaryButtonStyle(style: .gray)) + } + + let (bottomText, bottomTextColor) = bottomText() + + Text(bottomText).textSubhead1(color: bottomTextColor) + } + case .failed: + Button(action: { + sendViewModel.sync() + }) { + Text("send.confirmation.refresh".localized) + } + .buttonStyle(PrimaryButtonStyle(style: .gray)) + + Text("swap.confirmation.quote_failed".localized).textSubhead1() + } + } + } + .navigationTitle("send.confirmation.title".localized) + .navigationBarTitleDisplayMode(.inline) + } + + private func bottomText() -> (String, Color) { + if let data = sendViewModel.state.data, !data.canSend { + return ("swap.confirmation.invalid_quote".localized, .themeGray) + } else if sendViewModel.sending { + return ("swap.confirmation.please_wait".localized, .themeGray) + } else if sendViewModel.timeLeft > 0 { + return ("swap.confirmation.quote_expires_in".localized("\(sendViewModel.timeLeft)"), .themeJacob) + } else { + return ("swap.confirmation.quote_expired".localized, .themeGray) + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/MultiSwapView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/MultiSwapView.swift index 57cf585d6e..2e5e8db7a7 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/MultiSwapView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/MultiSwapView.swift @@ -10,7 +10,7 @@ struct MultiSwapView: View { @State private var selectTokenInPresented: Bool @State private var selectTokenOutPresented = false @State private var quotesPresented = false - @State private var confirmPresented = false + @State private var sendPresented = false @State private var settingsPresented = false @State private var preSwapStep: MultiSwapPreSwapStep? @State private var presentedSettingId: String? @@ -27,11 +27,13 @@ struct MultiSwapView: View { ThemeView { ScrollView { VStack(spacing: .margin12) { - amountsView() - buttonView() + VStack(spacing: .margin16) { + VStack(spacing: .margin8) { + amountsView() + availableBalanceView(value: balanceValue()) + } - if let balanceValue = balanceValue() { - availableBalanceView(value: balanceValue) + buttonView() } if let currentQuote = viewModel.currentQuote, let tokenIn = viewModel.tokenIn, let tokenOut = viewModel.tokenOut { @@ -69,14 +71,14 @@ struct MultiSwapView: View { } NavigationLink( - isActive: $confirmPresented, + isActive: $sendPresented, destination: { if let tokenIn = viewModel.tokenIn, let tokenOut = viewModel.tokenOut, let amountIn = viewModel.amountIn, let currentQuote = viewModel.currentQuote { - MultiSwapConfirmationView( + MultiSwapSendView( tokenIn: tokenIn, tokenOut: tokenOut, amountIn: amountIn, @@ -88,7 +90,7 @@ struct MultiSwapView: View { ) { EmptyView() } - .onChange(of: confirmPresented) { presented in + .onChange(of: sendPresented) { presented in if !presented { viewModel.autoQuoteIfRequired() } @@ -298,15 +300,7 @@ struct MultiSwapView: View { @ViewBuilder private func selectorButton(token: Token?, action: @escaping () -> Void) -> some View { Button(action: action) { HStack(spacing: .margin8) { - KFImage.url(token.flatMap { - URL(string: $0.coin.imageUrl) - }) - .resizable() - .placeholder { - Circle().fill(Color.themeSteel20) - } - .clipShape(Circle()) - .frame(width: .iconSize32, height: .iconSize32) + CoinIconView(coin: token.map(\.coin)) if let token { VStack(alignment: .leading, spacing: 1) { @@ -339,7 +333,7 @@ struct MultiSwapView: View { if let preSwapStep { self.preSwapStep = preSwapStep } else { - confirmPresented = true + sendPresented = true } }) { HStack(spacing: .margin8) { @@ -354,20 +348,15 @@ struct MultiSwapView: View { .buttonStyle(PrimaryButtonStyle(style: style)) } - @ViewBuilder private func availableBalanceView(value: String) -> some View { - ListSection { - HStack(spacing: .margin8) { - Text("send.available_balance".localized).textSubhead2() - Spacer() - Text(value) - .textSubhead2(color: .themeLeah) - .multilineTextAlignment(.trailing) - } - .padding(.vertical, .margin12) - .padding(.horizontal, .margin16) - .frame(minHeight: 40) + @ViewBuilder private func availableBalanceView(value: String?) -> some View { + HStack(spacing: .margin8) { + Text("send.available_balance".localized).textCaption() + Spacer() + Text(value ?? "---") + .textCaption() + .multilineTextAlignment(.trailing) } - .themeListStyle(.bordered) + .padding(.horizontal, .margin16) } @ViewBuilder private func quoteView(currentQuote: MultiSwapViewModel.Quote, tokenIn: Token, tokenOut: Token) -> some View { @@ -512,10 +501,7 @@ struct MultiSwapView: View { } private func balanceValue() -> String? { - guard viewModel.currentQuote == nil, - let availableBalance = viewModel.availableBalance, - let tokenIn = viewModel.tokenIn - else { + guard let availableBalance = viewModel.availableBalance, let tokenIn = viewModel.tokenIn else { return nil } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/MultiSwapViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/MultiSwapViewModel.swift index 795289f4b3..064c2395a8 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/MultiSwapViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/MultiSwapViewModel.swift @@ -42,7 +42,7 @@ class MultiSwapViewModel: ObservableObject { if let internalTokenIn { rateIn = marketKit.coinPrice(coinUid: internalTokenIn.coin.uid, currencyCode: currency.code)?.value - rateInCancellable = marketKit.coinPricePublisher(tag: "swap", coinUid: internalTokenIn.coin.uid, currencyCode: currency.code) + rateInCancellable = marketKit.coinPricePublisher(coinUid: internalTokenIn.coin.uid, currencyCode: currency.code) .receive(on: DispatchQueue.main) .sink { [weak self] price in self?.rateIn = price.value } } else { @@ -118,7 +118,7 @@ class MultiSwapViewModel: ObservableObject { if let internalTokenOut { rateOut = marketKit.coinPrice(coinUid: internalTokenOut.coin.uid, currencyCode: currency.code)?.value - rateOutCancellable = marketKit.coinPricePublisher(tag: "swap", coinUid: internalTokenOut.coin.uid, currencyCode: currency.code) + rateOutCancellable = marketKit.coinPricePublisher(coinUid: internalTokenOut.coin.uid, currencyCode: currency.code) .receive(on: DispatchQueue.main) .sink { [weak self] price in self?.rateOut = price.value } } else { @@ -476,13 +476,13 @@ extension MultiSwapViewModel { } func setAmountIn(percent: Int) { - guard let availableBalance else { + guard let tokenIn, let availableBalance else { return } enteringFiat = false - amountIn = availableBalance * Decimal(percent) / 100 + amountIn = (availableBalance * Decimal(percent) / 100).rounded(decimal: tokenIn.decimals) } func clearAmountIn() { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/BaseEvmMultiSwapConfirmationQuote.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/BaseEvmMultiSwapConfirmationQuote.swift index 181814ea64..42c69f1c93 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/BaseEvmMultiSwapConfirmationQuote.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/BaseEvmMultiSwapConfirmationQuote.swift @@ -14,16 +14,16 @@ class BaseEvmMultiSwapConfirmationQuote: BaseSendEvmData, IMultiSwapConfirmation gasPrice != nil && evmFeeData != nil } - func cautions(feeToken _: Token?) -> [CautionNew] { + func cautions(baseToken _: Token) -> [CautionNew] { [] } - func priceSectionFields(tokenIn _: Token, tokenOut _: Token, feeToken _: Token?, currency _: Currency, tokenInRate _: Decimal?, tokenOutRate _: Decimal?, feeTokenRate _: Decimal?) -> [SendConfirmField] { + func priceSectionFields(tokenIn _: Token, tokenOut _: Token, baseToken _: Token, currency _: Currency, tokenInRate _: Decimal?, tokenOutRate _: Decimal?, baseTokenRate _: Decimal?) -> [SendField] { [] } - func otherSections(tokenIn: Token, tokenOut: Token, feeToken: Token?, currency: Currency, tokenInRate: Decimal?, tokenOutRate: Decimal?, feeTokenRate: Decimal?) -> [[SendConfirmField]] { - var sections = [[SendConfirmField]]() + func otherSections(tokenIn: Token, tokenOut: Token, baseToken: Token, currency: Currency, tokenInRate: Decimal?, tokenOutRate: Decimal?, baseTokenRate: Decimal?) -> [[SendField]] { + var sections = [[SendField]]() if let nonce { sections.append( @@ -33,15 +33,13 @@ class BaseEvmMultiSwapConfirmationQuote: BaseSendEvmData, IMultiSwapConfirmation ) } - if let feeToken { - let additionalFeeFields = additionalFeeFields(tokenIn: tokenIn, tokenOut: tokenOut, feeToken: feeToken, currency: currency, tokenInRate: tokenInRate, tokenOutRate: tokenOutRate, feeTokenRate: feeTokenRate) - sections.append(feeFields(feeToken: feeToken, currency: currency, feeTokenRate: feeTokenRate) + additionalFeeFields) - } + let additionalFeeFields = additionalFeeFields(tokenIn: tokenIn, tokenOut: tokenOut, baseToken: baseToken, currency: currency, tokenInRate: tokenInRate, tokenOutRate: tokenOutRate, baseTokenRate: baseTokenRate) + sections.append(feeFields(feeToken: baseToken, currency: currency, feeTokenRate: baseTokenRate) + additionalFeeFields) return sections } - func additionalFeeFields(tokenIn _: Token, tokenOut _: Token, feeToken _: Token?, currency _: Currency, tokenInRate _: Decimal?, tokenOutRate _: Decimal?, feeTokenRate _: Decimal?) -> [SendConfirmField] { + func additionalFeeFields(tokenIn _: Token, tokenOut _: Token, baseToken _: Token, currency _: Currency, tokenInRate _: Decimal?, tokenOutRate _: Decimal?, baseTokenRate _: Decimal?) -> [SendField] { [] } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/BaseMultiSwap/MultiSwapSettings/AddressMultiSwapSettingsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/BaseMultiSwap/MultiSwapSettings/AddressMultiSwapSettingsViewModel.swift index bf8f7a3f4d..88735e7e80 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/BaseMultiSwap/MultiSwapSettings/AddressMultiSwapSettingsViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/BaseMultiSwap/MultiSwapSettings/AddressMultiSwapSettingsViewModel.swift @@ -11,6 +11,7 @@ class AddressMultiSwapSettingsViewModel: ObservableObject, IMultiSwapSettingsFie @Published var addressResult: AddressInput.Result = .idle { didSet { + syncAddressCautionState() syncSubject.send() } } @@ -39,12 +40,16 @@ class AddressMultiSwapSettingsViewModel: ObservableObject, IMultiSwapSettingsFie } private func syncAddressCautionState() { - guard !isAddressActive else { - addressCautionState = .none - return - } switch addressResult { - case let .invalid(failure): addressCautionState = .caution(.init(text: failure.error.localizedDescription, type: .error)) + case let .invalid(failure): + if let error = failure.error as? AddressParserChain.ParserError { + switch error { + case .validationError: + addressCautionState = isAddressActive ? .none : .caution(.init(text: failure.error.localizedDescription, type: .error)) + case .fetchError: + addressCautionState = .caution(.init(text: failure.error.localizedDescription, type: .error)) + } + } default: addressCautionState = .none } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/BaseUniswapMultiSwapConfirmationQuote.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/BaseUniswapMultiSwapConfirmationQuote.swift index 8807efc86c..3a14455519 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/BaseUniswapMultiSwapConfirmationQuote.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/BaseUniswapMultiSwapConfirmationQuote.swift @@ -23,11 +23,11 @@ class BaseUniswapMultiSwapConfirmationQuote: BaseEvmMultiSwapConfirmationQuote { super.canSwap && transactionData != nil } - override func cautions(feeToken: MarketKit.Token?) -> [CautionNew] { - var cautions = super.cautions(feeToken: feeToken) + override func cautions(baseToken: MarketKit.Token) -> [CautionNew] { + var cautions = super.cautions(baseToken: baseToken) if let transactionError { - cautions.append(caution(transactionError: transactionError, feeToken: feeToken)) + cautions.append(caution(transactionError: transactionError, feeToken: baseToken)) } cautions.append(contentsOf: quote.cautions()) @@ -35,8 +35,8 @@ class BaseUniswapMultiSwapConfirmationQuote: BaseEvmMultiSwapConfirmationQuote { return cautions } - override func priceSectionFields(tokenIn: MarketKit.Token, tokenOut: MarketKit.Token, feeToken: MarketKit.Token?, currency: Currency, tokenInRate: Decimal?, tokenOutRate: Decimal?, feeTokenRate: Decimal?) -> [SendConfirmField] { - var fields = super.priceSectionFields(tokenIn: tokenIn, tokenOut: tokenOut, feeToken: feeToken, currency: currency, tokenInRate: tokenInRate, tokenOutRate: tokenOutRate, feeTokenRate: feeTokenRate) + override func priceSectionFields(tokenIn: MarketKit.Token, tokenOut: MarketKit.Token, baseToken: MarketKit.Token, currency: Currency, tokenInRate: Decimal?, tokenOutRate: Decimal?, baseTokenRate: Decimal?) -> [SendField] { + var fields = super.priceSectionFields(tokenIn: tokenIn, tokenOut: tokenOut, baseToken: baseToken, currency: currency, tokenInRate: tokenInRate, tokenOutRate: tokenOutRate, baseTokenRate: baseTokenRate) if let priceImpact = quote.trade.priceImpact, BaseUniswapMultiSwapProvider.PriceImpactLevel(priceImpact: priceImpact) != .negligible { fields.append( diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/BaseUniswapMultiSwapProvider.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/BaseUniswapMultiSwapProvider.swift index 859b6ff543..fe0d0e362e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/BaseUniswapMultiSwapProvider.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/BaseUniswapMultiSwapProvider.swift @@ -17,17 +17,17 @@ class BaseUniswapMultiSwapProvider: BaseEvmMultiSwapProvider { let quote = try await internalQuote(tokenIn: tokenIn, tokenOut: tokenOut, amountIn: amountIn) let blockchainType = tokenIn.blockchainType - let gasPrice = transactionSettings?.gasPrice + let gasPriceData = transactionSettings?.gasPriceData var txData: TransactionData? var evmFeeData: EvmFeeData? var transactionError: Error? - if let evmKitWrapper = evmBlockchainManager.evmKitManager(blockchainType: blockchainType).evmKitWrapper, let gasPrice { + if let evmKitWrapper = evmBlockchainManager.evmKitManager(blockchainType: blockchainType).evmKitWrapper, let gasPriceData { do { let evmKit = evmKitWrapper.evmKit let transactionData = try transactionData(receiveAddress: evmKit.receiveAddress, chain: evmKit.chain, trade: quote.trade, tradeOptions: quote.tradeOptions) txData = transactionData - evmFeeData = try await evmFeeEstimator.estimateFee(evmKitWrapper: evmKitWrapper, transactionData: transactionData, gasPrice: gasPrice) + evmFeeData = try await evmFeeEstimator.estimateFee(evmKitWrapper: evmKitWrapper, transactionData: transactionData, gasPriceData: gasPriceData) } catch { transactionError = error } @@ -37,7 +37,7 @@ class BaseUniswapMultiSwapProvider: BaseEvmMultiSwapProvider { quote: quote, transactionData: txData, transactionError: transactionError, - gasPrice: gasPrice, + gasPrice: gasPriceData?.userDefined, evmFeeData: evmFeeData, nonce: transactionSettings?.nonce ) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/OneInchMultiSwapConfirmationQuote.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/OneInchMultiSwapConfirmationQuote.swift index a5594b2f8c..6cee0088f6 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/OneInchMultiSwapConfirmationQuote.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/OneInchMultiSwapConfirmationQuote.swift @@ -24,14 +24,14 @@ class OneInchMultiSwapConfirmationQuote: BaseEvmMultiSwapConfirmationQuote { super.canSwap && swap != nil && !insufficientFeeBalance } - override func cautions(feeToken: MarketKit.Token?) -> [CautionNew] { - var cautions = super.cautions(feeToken: feeToken) + override func cautions(baseToken: MarketKit.Token) -> [CautionNew] { + var cautions = super.cautions(baseToken: baseToken) if insufficientFeeBalance { cautions.append( .init( title: "fee_settings.errors.insufficient_balance".localized, - text: "ethereum_transaction.error.insufficient_balance_with_fee".localized(feeToken?.coin.code ?? ""), + text: "ethereum_transaction.error.insufficient_balance_with_fee".localized(baseToken.coin.code), type: .error ) ) @@ -42,8 +42,8 @@ class OneInchMultiSwapConfirmationQuote: BaseEvmMultiSwapConfirmationQuote { return cautions } - override func priceSectionFields(tokenIn: MarketKit.Token, tokenOut: MarketKit.Token, feeToken: MarketKit.Token?, currency: Currency, tokenInRate: Decimal?, tokenOutRate: Decimal?, feeTokenRate: Decimal?) -> [SendConfirmField] { - var fields = super.priceSectionFields(tokenIn: tokenIn, tokenOut: tokenOut, feeToken: feeToken, currency: currency, tokenInRate: tokenInRate, tokenOutRate: tokenOutRate, feeTokenRate: feeTokenRate) + override func priceSectionFields(tokenIn: MarketKit.Token, tokenOut: MarketKit.Token, baseToken: MarketKit.Token, currency: Currency, tokenInRate: Decimal?, tokenOutRate: Decimal?, baseTokenRate: Decimal?) -> [SendField] { + var fields = super.priceSectionFields(tokenIn: tokenIn, tokenOut: tokenOut, baseToken: baseToken, currency: currency, tokenInRate: tokenInRate, tokenOutRate: tokenOutRate, baseTokenRate: baseTokenRate) if let recipient = quote.recipient { fields.append( diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/OneInchMultiSwapProvider.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/OneInchMultiSwapProvider.swift index 55e33411bb..487b5596bd 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/OneInchMultiSwapProvider.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/OneInchMultiSwapProvider.swift @@ -10,6 +10,8 @@ class OneInchMultiSwapProvider: BaseEvmMultiSwapProvider { private let kit: OneInchKit.Kit private let networkManager = App.shared.networkManager private let evmFeeEstimator = EvmFeeEstimator() + private let commission: Decimal? = AppConfig.oneInchCommission + private let commissionAddress: String? = AppConfig.oneInchCommissionAddress init(kit: OneInchKit.Kit, storage: MultiSwapSettingStorage) { self.kit = kit @@ -48,13 +50,13 @@ class OneInchMultiSwapProvider: BaseEvmMultiSwapProvider { let quote = try await internalQuote(tokenIn: tokenIn, tokenOut: tokenOut, amountIn: amountIn) let blockchainType = tokenIn.blockchainType - let gasPrice = transactionSettings?.gasPrice + let gasPriceData = transactionSettings?.gasPriceData var evmFeeData: EvmFeeData? var resolvedSwap: Swap? var insufficientFeeBalance = false if let evmKitWrapper = evmBlockchainManager.evmKitManager(blockchainType: blockchainType).evmKitWrapper, - let gasPrice, + let gasPriceData, let amount = rawAmount(amount: amountIn, token: tokenIn) { let evmKit = evmKitWrapper.evmKit @@ -66,15 +68,17 @@ class OneInchMultiSwapProvider: BaseEvmMultiSwapProvider { toToken: address(token: tokenOut), amount: amount, slippage: slippage, + referrer: commissionAddress, + fee: commission, recipient: storage.recipient(blockchainType: blockchainType).flatMap { try? EvmKit.Address(hex: $0.raw) }, - gasPrice: gasPrice + gasPrice: gasPriceData.userDefined ) resolvedSwap = swap let evmBalance = evmKit.accountState?.balance ?? 0 let txAmount = swap.transaction.value - let feeAmount = BigUInt(swap.transaction.gasLimit * gasPrice.max) + let feeAmount = BigUInt(swap.transaction.gasLimit * gasPriceData.userDefined.max) let totalAmount = txAmount + feeAmount insufficientFeeBalance = totalAmount > evmBalance @@ -82,7 +86,7 @@ class OneInchMultiSwapProvider: BaseEvmMultiSwapProvider { evmFeeData = try await evmFeeEstimator.estimateFee( evmKitWrapper: evmKitWrapper, transactionData: swap.transactionData, - gasPrice: gasPrice, + gasPriceData: gasPriceData, predefinedGasLimit: swap.transaction.gasLimit ) } @@ -149,7 +153,8 @@ class OneInchMultiSwapProvider: BaseEvmMultiSwapProvider { chain: chain, fromToken: addressFrom, toToken: addressTo, - amount: amount + amount: amount, + fee: commission ) return await OneInchMultiSwapQuote( diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/ThorChainMultiSwapBtcConfirmationQuote.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/ThorChainMultiSwapBtcConfirmationQuote.swift index e5979b1785..db3901cb32 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/ThorChainMultiSwapBtcConfirmationQuote.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/ThorChainMultiSwapBtcConfirmationQuote.swift @@ -1,3 +1,4 @@ +import BitcoinCore import Foundation import MarketKit @@ -5,13 +6,15 @@ class ThorChainMultiSwapBtcConfirmationQuote: BaseSendBtcData, IMultiSwapConfirm let swapQuote: ThorChainMultiSwapProvider.SwapQuote let recipient: Address? let slippage: Decimal + let transactionError: Error? - init(swapQuote: ThorChainMultiSwapProvider.SwapQuote, recipient: Address?, slippage: Decimal, satoshiPerByte: Int?, bytes: Int?) { + init(swapQuote: ThorChainMultiSwapProvider.SwapQuote, recipient: Address?, slippage: Decimal, satoshiPerByte: Int?, fee: Decimal?, transactionError: Error?) { self.swapQuote = swapQuote self.recipient = recipient self.slippage = slippage + self.transactionError = transactionError - super.init(satoshiPerByte: satoshiPerByte, bytes: bytes) + super.init(satoshiPerByte: satoshiPerByte, fee: fee) } var amountOut: Decimal { @@ -19,16 +22,20 @@ class ThorChainMultiSwapBtcConfirmationQuote: BaseSendBtcData, IMultiSwapConfirm } var feeData: FeeData? { - bytes.map { .bitcoin(bytes: $0) } + fee.map { .bitcoin(bitcoinFeeData: BitcoinFeeData(fee: $0)) } } var canSwap: Bool { - satoshiPerByte != nil && bytes != nil + satoshiPerByte != nil && fee != nil } - func cautions(feeToken _: MarketKit.Token?) -> [CautionNew] { + func cautions(baseToken: MarketKit.Token) -> [CautionNew] { var cautions = [CautionNew]() + if let transactionError { + cautions.append(caution(transactionError: transactionError, feeToken: baseToken)) + } + switch MultiSwapSlippage.validate(slippage: slippage) { case .none: () case let .caution(caution): cautions.append(caution.cautionNew(title: "swap.advanced_settings.slippage".localized)) @@ -37,8 +44,8 @@ class ThorChainMultiSwapBtcConfirmationQuote: BaseSendBtcData, IMultiSwapConfirm return cautions } - func priceSectionFields(tokenIn _: MarketKit.Token, tokenOut: MarketKit.Token, feeToken _: MarketKit.Token?, currency _: Currency, tokenInRate _: Decimal?, tokenOutRate _: Decimal?, feeTokenRate _: Decimal?) -> [SendConfirmField] { - var fields = [SendConfirmField]() + func priceSectionFields(tokenIn _: MarketKit.Token, tokenOut: MarketKit.Token, baseToken _: MarketKit.Token, currency _: Currency, tokenInRate _: Decimal?, tokenOutRate _: Decimal?, baseTokenRate _: Decimal?) -> [SendField] { + var fields = [SendField]() if let recipient { fields.append( @@ -75,14 +82,10 @@ class ThorChainMultiSwapBtcConfirmationQuote: BaseSendBtcData, IMultiSwapConfirm return fields } - func otherSections(tokenIn _: Token, tokenOut: Token, feeToken: Token?, currency: Currency, tokenInRate _: Decimal?, tokenOutRate: Decimal?, feeTokenRate: Decimal?) -> [[SendConfirmField]] { - var sections = [[SendConfirmField]]() - - var feeFields = [SendConfirmField]() + func otherSections(tokenIn _: Token, tokenOut: Token, baseToken: Token, currency: Currency, tokenInRate _: Decimal?, tokenOutRate: Decimal?, baseTokenRate: Decimal?) -> [[SendField]] { + var sections = [[SendField]]() - if let feeToken { - feeFields.append(contentsOf: super.feeFields(feeToken: feeToken, currency: currency, feeTokenRate: feeTokenRate)) - } + var feeFields = super.feeFields(feeToken: baseToken, currency: currency, feeTokenRate: baseTokenRate) if swapQuote.affiliateFee > 0 { feeFields.append( @@ -122,8 +125,12 @@ class ThorChainMultiSwapBtcConfirmationQuote: BaseSendBtcData, IMultiSwapConfirm ) } - if let feeToken, let tokenOutRate, - let feeAmountData = amountData(feeToken: feeToken, currency: currency, feeTokenRate: feeTokenRate), + if !feeFields.isEmpty { + sections.append(feeFields) + } + + if let tokenOutRate, + let feeAmountData = amountData(feeToken: baseToken, currency: currency, feeTokenRate: baseTokenRate), let feeCurrencyValue = feeAmountData.currencyValue { let totalFee = feeCurrencyValue.value + (swapQuote.affiliateFee + swapQuote.liquidityFee + swapQuote.outboundFee) * tokenOutRate @@ -142,23 +149,6 @@ class ThorChainMultiSwapBtcConfirmationQuote: BaseSendBtcData, IMultiSwapConfirm } } - if let tokenOutRate { - let totalFee = (swapQuote.affiliateFee + swapQuote.liquidityFee + swapQuote.outboundFee) * tokenOutRate - let currencyValue = CurrencyValue(currency: currency, value: totalFee) - - if let formatted = ValueFormatter.instance.formatFull(currencyValue: currencyValue) { - sections.append( - [ - .levelValue( - title: "swap.total_fee".localized, - value: formatted, - level: .regular - ), - ] - ) - } - } - return sections } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/ThorChainMultiSwapEvmConfirmationQuote.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/ThorChainMultiSwapEvmConfirmationQuote.swift index 0a1fa4a29a..1fa65cdcdd 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/ThorChainMultiSwapEvmConfirmationQuote.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/ThorChainMultiSwapEvmConfirmationQuote.swift @@ -23,11 +23,11 @@ class ThorChainMultiSwapEvmConfirmationQuote: BaseEvmMultiSwapConfirmationQuote swapQuote.expectedAmountOut } - override func cautions(feeToken: MarketKit.Token?) -> [CautionNew] { - var cautions = super.cautions(feeToken: feeToken) + override func cautions(baseToken: MarketKit.Token) -> [CautionNew] { + var cautions = super.cautions(baseToken: baseToken) if let transactionError { - cautions.append(caution(transactionError: transactionError, feeToken: feeToken)) + cautions.append(caution(transactionError: transactionError, feeToken: baseToken)) } switch MultiSwapSlippage.validate(slippage: slippage) { @@ -38,8 +38,8 @@ class ThorChainMultiSwapEvmConfirmationQuote: BaseEvmMultiSwapConfirmationQuote return cautions } - override func priceSectionFields(tokenIn: MarketKit.Token, tokenOut: MarketKit.Token, feeToken: MarketKit.Token?, currency: Currency, tokenInRate: Decimal?, tokenOutRate: Decimal?, feeTokenRate: Decimal?) -> [SendConfirmField] { - var fields = super.priceSectionFields(tokenIn: tokenIn, tokenOut: tokenOut, feeToken: feeToken, currency: currency, tokenInRate: tokenInRate, tokenOutRate: tokenOutRate, feeTokenRate: feeTokenRate) + override func priceSectionFields(tokenIn: MarketKit.Token, tokenOut: MarketKit.Token, baseToken: MarketKit.Token, currency: Currency, tokenInRate: Decimal?, tokenOutRate: Decimal?, baseTokenRate: Decimal?) -> [SendField] { + var fields = super.priceSectionFields(tokenIn: tokenIn, tokenOut: tokenOut, baseToken: baseToken, currency: currency, tokenInRate: tokenInRate, tokenOutRate: tokenOutRate, baseTokenRate: baseTokenRate) if let recipient { fields.append( @@ -76,11 +76,11 @@ class ThorChainMultiSwapEvmConfirmationQuote: BaseEvmMultiSwapConfirmationQuote return fields } - override func otherSections(tokenIn: Token, tokenOut: Token, feeToken: Token?, currency: Currency, tokenInRate: Decimal?, tokenOutRate: Decimal?, feeTokenRate: Decimal?) -> [[SendConfirmField]] { - var sections = super.otherSections(tokenIn: tokenIn, tokenOut: tokenOut, feeToken: feeToken, currency: currency, tokenInRate: tokenInRate, tokenOutRate: tokenOutRate, feeTokenRate: feeTokenRate) + override func otherSections(tokenIn: Token, tokenOut: Token, baseToken: Token, currency: Currency, tokenInRate: Decimal?, tokenOutRate: Decimal?, baseTokenRate: Decimal?) -> [[SendField]] { + var sections = super.otherSections(tokenIn: tokenIn, tokenOut: tokenOut, baseToken: baseToken, currency: currency, tokenInRate: tokenInRate, tokenOutRate: tokenOutRate, baseTokenRate: baseTokenRate) - if let feeToken, let tokenOutRate, let evmFeeData, - let evmFeeAmountData = evmFeeData.totalAmountData(gasPrice: gasPrice, feeToken: feeToken, currency: currency, feeTokenRate: feeTokenRate), + if let tokenOutRate, let evmFeeData, + let evmFeeAmountData = evmFeeData.totalAmountData(gasPrice: gasPrice, feeToken: baseToken, currency: currency, feeTokenRate: baseTokenRate), let evmFeeCurrencyValue = evmFeeAmountData.currencyValue { let totalFee = evmFeeCurrencyValue.value + (swapQuote.affiliateFee + swapQuote.liquidityFee + swapQuote.outboundFee) * tokenOutRate @@ -102,8 +102,8 @@ class ThorChainMultiSwapEvmConfirmationQuote: BaseEvmMultiSwapConfirmationQuote return sections } - override func additionalFeeFields(tokenIn: Token, tokenOut: Token, feeToken: Token?, currency: Currency, tokenInRate: Decimal?, tokenOutRate: Decimal?, feeTokenRate: Decimal?) -> [SendConfirmField] { - var fields = super.additionalFeeFields(tokenIn: tokenIn, tokenOut: tokenOut, feeToken: feeToken, currency: currency, tokenInRate: tokenInRate, tokenOutRate: tokenOutRate, feeTokenRate: feeTokenRate) + override func additionalFeeFields(tokenIn: Token, tokenOut: Token, baseToken: Token, currency: Currency, tokenInRate: Decimal?, tokenOutRate: Decimal?, baseTokenRate: Decimal?) -> [SendField] { + var fields = super.additionalFeeFields(tokenIn: tokenIn, tokenOut: tokenOut, baseToken: baseToken, currency: currency, tokenInRate: tokenInRate, tokenOutRate: tokenOutRate, baseTokenRate: baseTokenRate) if swapQuote.affiliateFee > 0 { fields.append( diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/ThorChainMultiSwapProvider.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/ThorChainMultiSwapProvider.swift index 073db48cf5..f5601a4d9f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/ThorChainMultiSwapProvider.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/Providers/ThorChainMultiSwapProvider.swift @@ -1,5 +1,6 @@ import Alamofire import BigInt +import BitcoinCore import EvmKit import Foundation import HsToolKit @@ -10,15 +11,20 @@ import SwiftUI class ThorChainMultiSwapProvider: IMultiSwapProvider { private let baseUrl = "https://thornode.ninerealms.com" - private let networkManager = App.shared.networkManager -// private let networkManager = NetworkManager(logger: Logger(minLogLevel: .debug)) +// private let networkManager = App.shared.networkManager + private let networkManager = NetworkManager(logger: Logger(minLogLevel: .debug)) private let marketKit = App.shared.marketKit private let evmBlockchainManager = App.shared.evmBlockchainManager + private let btcBlockchainManager = App.shared.btcBlockchainManager private let accountManager = App.shared.accountManager private let adapterManager = App.shared.adapterManager private let storage: MultiSwapSettingStorage private let allowanceHelper = MultiSwapAllowanceHelper() private let evmFeeEstimator = EvmFeeEstimator() + private let utxoFilters = UtxoFilters( + scriptTypes: [.p2pkh, .p2wpkhSh, .p2wpkh], + maxOutputsCountForInputs: 10 + ) var assets = [Asset]() @@ -112,13 +118,13 @@ class ThorChainMultiSwapProvider: IMultiSwapProvider { } let blockchainType = tokenIn.blockchainType - let gasPrice = transactionSettings?.gasPrice + let gasPriceData = transactionSettings?.gasPriceData var evmFeeData: EvmFeeData? var transactionError: Error? - if let evmKitWrapper = evmBlockchainManager.evmKitManager(blockchainType: blockchainType).evmKitWrapper, let gasPrice { + if let evmKitWrapper = evmBlockchainManager.evmKitManager(blockchainType: blockchainType).evmKitWrapper, let gasPriceData { do { - evmFeeData = try await evmFeeEstimator.estimateFee(evmKitWrapper: evmKitWrapper, transactionData: transactionData, gasPrice: gasPrice) + evmFeeData = try await evmFeeEstimator.estimateFee(evmKitWrapper: evmKitWrapper, transactionData: transactionData, gasPriceData: gasPriceData) } catch { transactionError = error } @@ -130,21 +136,46 @@ class ThorChainMultiSwapProvider: IMultiSwapProvider { slippage: slippage, transactionData: transactionData, transactionError: transactionError, - gasPrice: gasPrice, + gasPrice: gasPriceData?.userDefined, evmFeeData: evmFeeData, nonce: transactionSettings?.nonce ) case .bitcoin, .bitcoinCash, .litecoin: - let satoshiPerByte = transactionSettings?.satoshiPerByte + var transactionError: Error? + var satoshiPerByte: Int? + var sendInfo: SendInfo? - let bytes: Int? = nil // TODO: estimate + if let _satoshiPerByte = transactionSettings?.satoshiPerByte, + let adapter = adapterManager.adapter(for: tokenIn) as? BitcoinBaseAdapter + { + do { + satoshiPerByte = _satoshiPerByte + let params = SendParameters( + address: swapQuote.inboundAddress, + value: adapter.convertToSatoshi(value: amountIn), + feeRate: _satoshiPerByte, + sortType: .none, + rbfEnabled: true, + memo: swapQuote.memo, + unspentOutputs: nil, + dustThreshold: swapQuote.dustThreshold, + utxoFilters: utxoFilters, + changeToFirstInput: true + ) + + sendInfo = try adapter.sendInfo(params: params) + } catch { + transactionError = error + } + } return ThorChainMultiSwapBtcConfirmationQuote( swapQuote: swapQuote, recipient: storage.recipient(blockchainType: tokenIn.blockchainType), slippage: slippage, satoshiPerByte: satoshiPerByte, - bytes: bytes + fee: sendInfo?.fee, + transactionError: transactionError ) default: throw SwapError.unsupportedTokenIn @@ -340,6 +371,8 @@ extension ThorChainMultiSwapProvider { let outboundFee: Decimal let liquidityFee: Decimal + let dustThreshold: Int + init(map: Map) throws { inboundAddress = try map.value("inbound_address") expectedAmountOut = try map.value("expected_amount_out", using: Transform.stringToDecimalTransform) / pow(10, 8) @@ -349,6 +382,8 @@ extension ThorChainMultiSwapProvider { affiliateFee = try map.value("fees.affiliate", using: Transform.stringToDecimalTransform) / pow(10, 8) outboundFee = try map.value("fees.outbound", using: Transform.stringToDecimalTransform) / pow(10, 8) liquidityFee = try map.value("fees.liquidity", using: Transform.stringToDecimalTransform) / pow(10, 8) + + dustThreshold = try map.value("dust_threshold", using: Transform.stringToIntTransform) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/ISendConfirmationData.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/ISendConfirmationData.swift deleted file mode 100644 index 191e28f2d1..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/ISendConfirmationData.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Foundation -import MarketKit - -protocol ISendConfirmationData { - var feeData: FeeData? { get } - var canSend: Bool { get } - var customSendButtonTitle: String? { get } - var customSendingButtonTitle: String? { get } - var customSentButtonTitle: String? { get } - func cautions(feeToken: Token?) -> [CautionNew] - func sections(feeToken: Token?, currency: Currency, feeTokenRate: Decimal?) -> [[SendConfirmField]] -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/ISendHandler.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/ISendHandler.swift deleted file mode 100644 index ac08b8b921..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/ISendHandler.swift +++ /dev/null @@ -1,7 +0,0 @@ -import MarketKit - -protocol ISendHandler { - var blockchainType: BlockchainType { get } - func confirmationData(transactionSettings: TransactionSettings?) async throws -> ISendConfirmationData - func send(data: ISendConfirmationData) async throws -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/SendConfirmationNewView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/SendConfirmationNewView.swift deleted file mode 100644 index 8b76f70eaa..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/SendConfirmationNewView.swift +++ /dev/null @@ -1,102 +0,0 @@ -import ComponentKit -import Kingfisher -import MarketKit -import SwiftUI - -struct SendConfirmationNewView: View { - @StateObject var viewModel: SendConfirmationNewViewModel - private let onSend: () -> Void - - @State private var feeSettingsPresented = false - - init(sendData: SendDataNew, onSend: @escaping () -> Void) { - _viewModel = .init(wrappedValue: SendConfirmationNewViewModel(sendData: sendData)) - self.onSend = onSend - } - - var body: some View { - ThemeView { - if viewModel.syncing { - ProgressView() - } else if let data = viewModel.data { - dataView(data: data) - } - } - .sheet(isPresented: $feeSettingsPresented) { - if let transactionService = viewModel.transactionService, let feeToken = viewModel.feeToken { - transactionService.settingsView( - feeData: Binding(get: { viewModel.data?.feeData }, set: { _ in }), - loading: $viewModel.syncing, - feeToken: feeToken, - currency: viewModel.currency, - feeTokenRate: $viewModel.feeTokenRate - ) - } - } - .navigationTitle("send.confirmation.title".localized) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button(action: { - feeSettingsPresented = true - }) { - Image("manage_2_20").renderingMode(.template) - } - .disabled(viewModel.syncing) - } - } - .onReceive(viewModel.errorSubject) { error in - HudHelper.instance.showError(subtitle: error) - } - } - - @ViewBuilder private func dataView(data: ISendConfirmationData) -> some View { - VStack { - ScrollView { - VStack(spacing: .margin16) { - let sections = data.sections(feeToken: viewModel.feeToken, currency: viewModel.currency, feeTokenRate: viewModel.feeTokenRate) - - if !sections.isEmpty { - ForEach(sections.indices, id: \.self) { sectionIndex in - let section = sections[sectionIndex] - - if !section.isEmpty { - ListSection { - ForEach(section.indices, id: \.self) { index in - section[index].listRow - } - } - } - } - } - - let cautions = (viewModel.transactionService?.cautions ?? []) + data.cautions(feeToken: viewModel.feeToken) - - if !cautions.isEmpty { - VStack(spacing: .margin12) { - ForEach(cautions.indices, id: \.self) { index in - HighlightedTextView(caution: cautions[index]) - } - } - } - } - .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) - } - - SlideButton( - styling: .text( - start: data.customSendButtonTitle ?? "send.confirmation.slide_to_send".localized, - end: data.customSendingButtonTitle ?? "send.confirmation.sending".localized, - success: data.customSentButtonTitle ?? "send.confirmation.sent".localized - ), - action: { - try await viewModel.send() - }, completion: { - onSend() - } - ) - .padding(.vertical, .margin16) - .padding(.horizontal, .margin16) - } - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/SendConfirmationNewViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/SendConfirmationNewViewModel.swift deleted file mode 100644 index d2a3f4a07c..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/SendConfirmationNewViewModel.swift +++ /dev/null @@ -1,114 +0,0 @@ -import Combine -import Foundation -import HsExtensions -import MarketKit - -class SendConfirmationNewViewModel: ObservableObject { - private let currencyManager = App.shared.currencyManager - private let marketKit = App.shared.marketKit - private let transactionServiceFactory = TransactionServiceFactory() - private let sendHandlerFactory = SendHandlerFactory() - - private var syncTask: AnyTask? - private var cancellables = Set() - - let handler: ISendHandler? - let transactionService: ITransactionService? - let currency: Currency - let feeToken: Token? - - @Published var feeTokenRate: Decimal? - - @Published var data: ISendConfirmationData? - @Published var syncing = false - @Published var sending = false - - let errorSubject = PassthroughSubject() - - init(sendData: SendDataNew) { - handler = sendHandlerFactory.handler(sendData: sendData) - currency = currencyManager.baseCurrency - - if let handler { - transactionService = transactionServiceFactory.transactionService(blockchainType: handler.blockchainType) - feeToken = try? marketKit.token(query: TokenQuery(blockchainType: handler.blockchainType, tokenType: .native)) - } else { - transactionService = nil - feeToken = nil - } - - transactionService?.updatePublisher - .sink { [weak self] in self?.sync() } - .store(in: &cancellables) - - if let feeToken { - feeTokenRate = marketKit.coinPrice(coinUid: feeToken.coin.uid, currencyCode: currency.code)?.value - marketKit.coinPricePublisher(tag: "send", coinUid: feeToken.coin.uid, currencyCode: currency.code) - .receive(on: DispatchQueue.main) - .sink { [weak self] price in self?.feeTokenRate = price.value } - .store(in: &cancellables) - } - - sync() - } - - @MainActor private func set(sending: Bool) { - self.sending = sending - } -} - -extension SendConfirmationNewViewModel { - func sync() { - guard let handler, let transactionService else { - return - } - - syncTask = nil - data = nil - - if !syncing { - syncing = true - } - - syncTask = Task { [weak self, handler, transactionService] in - try await transactionService.sync() - - let data = try await handler.confirmationData(transactionSettings: transactionService.transactionSettings) - - if !Task.isCancelled { - await MainActor.run { [weak self, data] in - self?.syncing = false - self?.data = data - } - } - } - .erased() - } - - func send() async throws { - do { - guard let handler else { - throw SendError.noHandler - } - - guard let data else { - throw SendError.noData - } - - await set(sending: true) - - try await handler.send(data: data) - } catch { - await set(sending: false) - errorSubject.send(error.smartDescription) - throw error - } - } -} - -extension SendConfirmationNewViewModel { - enum SendError: Error { - case noHandler - case noData - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/SendDataNew.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/SendDataNew.swift deleted file mode 100644 index 83f9d0a7c1..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/SendDataNew.swift +++ /dev/null @@ -1,8 +0,0 @@ -import EvmKit -import Foundation -import MarketKit - -enum SendDataNew { - case evm(blockchainType: BlockchainType, transactionData: TransactionData) - case bitcoin(amount: Decimal, recipient: String) -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/SendEvmHandler.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/SendEvmHandler.swift deleted file mode 100644 index 2d1d026963..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/SendEvmHandler.swift +++ /dev/null @@ -1,237 +0,0 @@ -import BigInt -import Eip20Kit -import EvmKit -import Foundation -import MarketKit - -class SendEvmHandler { - let coinServiceFactory: EvmCoinServiceFactory - let transactionData: TransactionData - let evmKitWrapper: EvmKitWrapper - let evmFeeEstimator = EvmFeeEstimator() - - init(coinServiceFactory: EvmCoinServiceFactory, transactionData: TransactionData, evmKitWrapper: EvmKitWrapper) { - self.coinServiceFactory = coinServiceFactory - self.transactionData = transactionData - self.evmKitWrapper = evmKitWrapper - } -} - -extension SendEvmHandler: ISendHandler { - var blockchainType: BlockchainType { - evmKitWrapper.blockchainType - } - - func confirmationData(transactionSettings: TransactionSettings?) async throws -> ISendConfirmationData { - let gasPrice = transactionSettings?.gasPrice - var evmFeeData: EvmFeeData? - var transactionError: Error? - - if let gasPrice { - do { - evmFeeData = try await evmFeeEstimator.estimateFee(evmKitWrapper: evmKitWrapper, transactionData: transactionData, gasPrice: gasPrice) - } catch { - transactionError = error - } - } - - let decoration = evmKitWrapper.evmKit.decorate(transactionData: transactionData) - let (sections, customSendButtonTitle, customSendingButtonTitle, customSentButtonTitle) = decoration.map { resolve(decoration: $0) } ?? ([], nil, nil, nil) - - return ConfirmationData( - baseSections: sections, - transactionError: transactionError, - customSendButtonTitle: customSendButtonTitle, - customSendingButtonTitle: customSendingButtonTitle, - customSentButtonTitle: customSentButtonTitle, - gasPrice: gasPrice, - evmFeeData: evmFeeData, - nonce: transactionSettings?.nonce - ) - } - - func send(data: ISendConfirmationData) async throws { - guard let data = data as? ConfirmationData else { - throw SendError.invalidData - } - - guard let gasPrice = data.gasPrice else { - throw SendError.noGasPrice - } - - guard let gasLimit = data.evmFeeData?.surchargedGasLimit else { - throw SendError.noGasLimit - } - - _ = try await evmKitWrapper.send( - transactionData: transactionData, - gasPrice: gasPrice, - gasLimit: gasLimit, - nonce: data.nonce - ) - } - - private func resolve(decoration: TransactionDecoration?) -> ([[SendConfirmField]], String?, String?, String?) { - switch decoration { - case let decoration as ApproveEip20Decoration: - let sections = eip20ApproveSections( - spender: decoration.spender, - value: decoration.value, - contractAddress: decoration.contractAddress - ) - - let isRevoke = decoration.value == 0 - - return ( - sections, - isRevoke ? "send.confirmation.slide_to_revoke".localized : "send.confirmation.slide_to_approve".localized, - isRevoke ? "send.confirmation.revoking".localized : "send.confirmation.approving".localized, - isRevoke ? "send.confirmation.revoked".localized : "send.confirmation.approved".localized - ) - default: - return ([], nil, nil, nil) - } - } - - private func eip20ApproveSections(spender: EvmKit.Address, value: BigUInt, contractAddress: EvmKit.Address) -> [[SendConfirmField]] { - guard let coinService = coinServiceFactory.coinService(contractAddress: contractAddress) else { - return [] - } - - let isRevokeAllowance = value == 0 // Check approved new value or revoked last allowance - - let amountField: SendConfirmField - - if isRevokeAllowance { - amountField = .amount( - title: "approve.confirmation.you_revoke".localized, - token: coinService.token, - coinValueType: .withoutAmount(kind: .token(token: coinService.token)), - currencyValue: nil, - type: .neutral - ) - } else { - amountField = self.amountField( - coinService: coinService, - title: "approve.confirmation.you_approve".localized, - value: value, - type: .neutral - ) - } - - return [ - [ - amountField, - .address( - title: "approve.confirmation.spender".localized, - value: spender.eip55, - blockchainType: coinService.token.blockchainType - ), - ], - ] - } - - private func amountField(coinService: CoinService, title: String, value: BigUInt, type: SendConfirmField.AmountType) -> SendConfirmField { - amountField(coinService: coinService, title: title, amountData: coinService.amountData(value: value, sign: type.sign), type: type) - } - - private func amountField(coinService: CoinService, title: String, amountData: AmountData, type: SendConfirmField.AmountType) -> SendConfirmField { - let token = coinService.token - let value = amountData.coinValue - - return .amount( - title: title, - token: token, - coinValueType: value.isMaxValue ? .infinity(kind: value.kind) : .regular(coinValue: value), - currencyValue: value.isMaxValue ? nil : amountData.currencyValue, - type: type - ) - } -} - -extension SendEvmHandler { - class ConfirmationData: BaseSendEvmData, ISendConfirmationData { - let baseSections: [[SendConfirmField]] - let transactionError: Error? - let customSendButtonTitle: String? - let customSendingButtonTitle: String? - let customSentButtonTitle: String? - - init(baseSections: [[SendConfirmField]], transactionError: Error?, customSendButtonTitle: String?, customSendingButtonTitle: String?, customSentButtonTitle: String?, gasPrice: GasPrice?, evmFeeData: EvmFeeData?, nonce: Int?) { - self.baseSections = baseSections - self.transactionError = transactionError - self.customSendButtonTitle = customSendButtonTitle - self.customSendingButtonTitle = customSendingButtonTitle - self.customSentButtonTitle = customSentButtonTitle - - super.init(gasPrice: gasPrice, evmFeeData: evmFeeData, nonce: nonce) - } - - var feeData: FeeData? { - evmFeeData.map { .evm(evmFeeData: $0) } - } - - var canSend: Bool { - evmFeeData != nil - } - - func cautions(feeToken: Token?) -> [CautionNew] { - var cautions = [CautionNew]() - - if let transactionError { - cautions.append(caution(transactionError: transactionError, feeToken: feeToken)) - } - - return cautions - } - - func sections(feeToken: Token?, currency: Currency, feeTokenRate: Decimal?) -> [[SendConfirmField]] { - var sections = baseSections - - if let nonce { - sections.append( - [ - .levelValue(title: "send.confirmation.nonce".localized, value: String(nonce), level: .regular), - ] - ) - } - - if let feeToken { - sections.append(feeFields(feeToken: feeToken, currency: currency, feeTokenRate: feeTokenRate)) - } - - return sections - } - } -} - -extension SendEvmHandler { - enum SendError: Error { - case invalidData - case noGasPrice - case noGasLimit - } -} - -extension SendEvmHandler { - static func instance(blockchainType: BlockchainType, transactionData: TransactionData) -> SendEvmHandler? { - guard let coinServiceFactory = EvmCoinServiceFactory( - blockchainType: blockchainType, - marketKit: App.shared.marketKit, - currencyManager: App.shared.currencyManager, - coinManager: App.shared.coinManager - ) else { - return nil - } - - guard let evmKitWrapper = App.shared.evmBlockchainManager.evmKitManager(blockchainType: blockchainType).evmKitWrapper else { - return nil - } - - return SendEvmHandler( - coinServiceFactory: coinServiceFactory, - transactionData: transactionData, - evmKitWrapper: evmKitWrapper - ) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/SendHandlerFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/SendHandlerFactory.swift deleted file mode 100644 index c878565969..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/SendHandlerFactory.swift +++ /dev/null @@ -1,10 +0,0 @@ -struct SendHandlerFactory { - func handler(sendData: SendDataNew) -> ISendHandler? { - switch sendData { - case let .evm(blockchainType, transactionData): - return SendEvmHandler.instance(blockchainType: blockchainType, transactionData: transactionData) - case .bitcoin: - return nil - } - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/TransactionServiceFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/TransactionServiceFactory.swift deleted file mode 100644 index 88149df1b6..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/TransactionServiceFactory.swift +++ /dev/null @@ -1,16 +0,0 @@ -import MarketKit - -struct TransactionServiceFactory { - private let evmBlockchainManager = App.shared.evmBlockchainManager - - func transactionService(blockchainType: BlockchainType) -> ITransactionService? { - if EvmBlockchainManager.blockchainTypes.contains(blockchainType), - let evmKit = evmBlockchainManager.evmKitManager(blockchainType: blockchainType).evmKitWrapper?.evmKit, - let transactionService = EvmTransactionService(blockchainType: blockchainType, userAddress: evmKit.receiveAddress) - { - return transactionService - } - - return nil - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/TokenSelect/MultiSwapTokenSelectView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/TokenSelect/MultiSwapTokenSelectView.swift index 280d03b42b..b61c077ad1 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/TokenSelect/MultiSwapTokenSelectView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/TokenSelect/MultiSwapTokenSelectView.swift @@ -23,17 +23,12 @@ struct MultiSwapTokenSelectView: View { VStack(spacing: 0) { SearchBar(text: $viewModel.searchText, prompt: "placeholder.search".localized) - ThemeList(items: viewModel.items) { item in + ThemeList(viewModel.items, bottomSpacing: .margin16) { item in ClickableRow(action: { currentToken = item.token isPresented = false }) { - KFImage.url(URL(string: item.token.coin.imageUrl)) - .resizable() - .placeholder { - Image(item.token.placeholderImageName) - } - .frame(width: .iconSize32, height: .iconSize32) + CoinIconView(coin: item.token.coin, placeholderImage: Image(item.token.placeholderImageName)) VStack(spacing: 1) { HStack(spacing: .margin8) { @@ -67,7 +62,6 @@ struct MultiSwapTokenSelectView: View { } } } - .themeListStyle(.transparent) } .navigationTitle(title) .navigationBarTitleDisplayMode(.inline) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Nft/NftModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Nft/NftModule.swift index 720b57b3ef..2aabf8b0e5 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Nft/NftModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Nft/NftModule.swift @@ -4,8 +4,8 @@ import UIKit enum NftModule { static func viewController() -> UIViewController? { let coinPriceService = WalletCoinPriceService( - tag: "nft", currencyManager: App.shared.currencyManager, + priceChangeModeManager: App.shared.priceChangeModeManager, marketKit: App.shared.marketKit ) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Nft/NftService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Nft/NftService.swift index fa024c1ce1..93a3b9ee15 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Nft/NftService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Nft/NftService.swift @@ -1,3 +1,4 @@ +import Combine import Foundation import MarketKit import RxRelay @@ -13,6 +14,7 @@ class NftService { private let coinPriceService: WalletCoinPriceService private let disposeBag = DisposeBag() private var adapterDisposeBag = DisposeBag() + private var cancellables = Set() var mode: Mode = .lastSale { didSet { @@ -52,7 +54,7 @@ class NftService { self.coinPriceService = coinPriceService subscribe(disposeBag, nftAdapterManager.adaptersUpdatedObservable) { [weak self] in self?.handle(adapterMap: $0) } - subscribe(disposeBag, balanceConversionManager.conversionTokenObservable) { [weak self] _ in self?.syncTotalItem() } + balanceConversionManager.$conversionToken.sink { [weak self] _ in self?.syncTotalItem() }.store(in: &cancellables) _handle(adapterMap: nftAdapterManager.adapterMap) } @@ -261,17 +263,16 @@ class NftService { } extension NftService: IWalletCoinPriceServiceDelegate { - func didUpdateBaseCurrency() { + func didUpdate(itemsMap: [String: WalletCoinPriceService.Item]?) { queue.async { - self.updatePriceItems(items: self.items, map: self.coinPriceService.itemMap(coinUids: Array(self.allCoinUids(items: self.items)))) - self.items = self.sort(items: self.items) - self.syncTotalItem() - } - } + let _itemsMap: [String: WalletCoinPriceService.Item] + if let itemsMap { + _itemsMap = itemsMap + } else { + _itemsMap = self.coinPriceService.itemMap(coinUids: Array(self.allCoinUids(items: self.items))) + } - func didUpdate(itemsMap: [String: WalletCoinPriceService.Item]) { - queue.async { - self.updatePriceItems(items: self.items, map: itemsMap) + self.updatePriceItems(items: self.items, map: _itemsMap) self.items = self.sort(items: self.items) self.syncTotalItem() } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/NftAsset/Overview/NftAssetOverviewModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/NftAsset/Overview/NftAssetOverviewModule.swift index e3efd2f134..d59f513038 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/NftAsset/Overview/NftAssetOverviewModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/NftAsset/Overview/NftAssetOverviewModule.swift @@ -3,8 +3,8 @@ import UIKit enum NftAssetOverviewModule { static func viewController(providerCollectionUid: String, nftUid: NftUid) -> NftAssetOverviewViewController { let coinPriceService = WalletCoinPriceService( - tag: "nft-asset-overview", currencyManager: App.shared.currencyManager, + priceChangeModeManager: App.shared.priceChangeModeManager, marketKit: App.shared.marketKit ) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/NftAsset/Overview/NftAssetOverviewService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/NftAsset/Overview/NftAssetOverviewService.swift index 3e74354e07..2b67c5019b 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/NftAsset/Overview/NftAssetOverviewService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/NftAsset/Overview/NftAssetOverviewService.swift @@ -160,24 +160,18 @@ class NftAssetOverviewService { } extension NftAssetOverviewService: IWalletCoinPriceServiceDelegate { - func didUpdateBaseCurrency() { + func didUpdate(itemsMap: [String: WalletCoinPriceService.Item]?) { queue.async { guard case let .completed(item) = self.state else { return } - self._fillCoinPrices(item: item, coinUids: self._allCoinUids(item: item)) - self.state = .completed(item) - } - } - - func didUpdate(itemsMap: [String: WalletCoinPriceService.Item]) { - queue.async { - guard case let .completed(item) = self.state else { - return + if let itemsMap { + self._fillCoinPrices(item: item, map: itemsMap) + } else { + self._fillCoinPrices(item: item, coinUids: self._allCoinUids(item: item)) } - self._fillCoinPrices(item: item, map: itemsMap) self.state = .completed(item) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/NftCollection/Activity/NftActivityModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/NftCollection/Activity/NftActivityModule.swift index 763dc905d8..9a72c5b0df 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/NftCollection/Activity/NftActivityModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/NftCollection/Activity/NftActivityModule.swift @@ -4,7 +4,11 @@ import UIKit enum NftActivityModule { static func viewController(eventListType: NftEventListType, defaultEventType: NftEventMetadata.EventType? = .sale) -> NftActivityViewController { - let coinPriceService = WalletCoinPriceService(tag: "nft-activity", currencyManager: App.shared.currencyManager, marketKit: App.shared.marketKit) + let coinPriceService = WalletCoinPriceService( + currencyManager: App.shared.currencyManager, + priceChangeModeManager: App.shared.priceChangeModeManager, + marketKit: App.shared.marketKit + ) let service = NftActivityService(eventListType: eventListType, defaultEventType: defaultEventType, nftMetadataManager: App.shared.nftMetadataManager, coinPriceService: coinPriceService) let viewModel = NftActivityViewModel(service: service) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/NftCollection/Activity/NftActivityService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/NftCollection/Activity/NftActivityService.swift index 72f81da1d7..aa57eeaf4e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/NftCollection/Activity/NftActivityService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/NftCollection/Activity/NftActivityService.swift @@ -186,24 +186,20 @@ class NftActivityService { } extension NftActivityService: IWalletCoinPriceServiceDelegate { - func didUpdateBaseCurrency() { + func didUpdate(itemsMap: [String: WalletCoinPriceService.Item]?) { queue.async { guard case let .loaded(items, allLoaded) = self.state else { return } - self.updatePriceItems(items: items, map: self.coinPriceService.itemMap(coinUids: Array(self.allCoinUids(items: items)))) - self.state = .loaded(items: items, allLoaded: allLoaded) - } - } - - func didUpdate(itemsMap: [String: WalletCoinPriceService.Item]) { - queue.async { - guard case let .loaded(items, allLoaded) = self.state else { - return + let _itemsMap: [String: WalletCoinPriceService.Item] + if let itemsMap { + _itemsMap = itemsMap + } else { + _itemsMap = self.coinPriceService.itemMap(coinUids: Array(self.allCoinUids(items: items))) } - self.updatePriceItems(items: items, map: itemsMap) + self.updatePriceItems(items: items, map: _itemsMap) self.state = .loaded(items: items, allLoaded: allLoaded) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/NftCollection/Assets/NftCollectionAssetsModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/NftCollection/Assets/NftCollectionAssetsModule.swift index ecb102e183..822e9a4bdf 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/NftCollection/Assets/NftCollectionAssetsModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/NftCollection/Assets/NftCollectionAssetsModule.swift @@ -2,7 +2,11 @@ import MarketKit enum NftCollectionAssetsModule { static func viewController(blockchainType: BlockchainType, providerCollectionUid: String) -> NftCollectionAssetsViewController { - let coinPriceService = WalletCoinPriceService(tag: "nft-collection-assets", currencyManager: App.shared.currencyManager, marketKit: App.shared.marketKit) + let coinPriceService = WalletCoinPriceService( + currencyManager: App.shared.currencyManager, + priceChangeModeManager: App.shared.priceChangeModeManager, + marketKit: App.shared.marketKit + ) let service = NftCollectionAssetsService(blockchainType: blockchainType, providerCollectionUid: providerCollectionUid, nftMetadataManager: App.shared.nftMetadataManager, coinPriceService: coinPriceService) let viewModel = NftCollectionAssetsViewModel(service: service) return NftCollectionAssetsViewController(viewModel: viewModel) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/NftCollection/Assets/NftCollectionAssetsService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/NftCollection/Assets/NftCollectionAssetsService.swift index 89e1e4de25..8fa5fd2d3f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/NftCollection/Assets/NftCollectionAssetsService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/NftCollection/Assets/NftCollectionAssetsService.swift @@ -120,24 +120,20 @@ class NftCollectionAssetsService { } extension NftCollectionAssetsService: IWalletCoinPriceServiceDelegate { - func didUpdateBaseCurrency() { + func didUpdate(itemsMap: [String: WalletCoinPriceService.Item]?) { queue.async { guard case let .loaded(items, allLoaded) = self.state else { return } - self.updatePriceItems(items: items, map: self.coinPriceService.itemMap(coinUids: Array(self.allCoinUids(items: items)))) - self.state = .loaded(items: items, allLoaded: allLoaded) - } - } - - func didUpdate(itemsMap: [String: WalletCoinPriceService.Item]) { - queue.async { - guard case let .loaded(items, allLoaded) = self.state else { - return + let _itemsMap: [String: WalletCoinPriceService.Item] + if let itemsMap { + _itemsMap = itemsMap + } else { + _itemsMap = self.coinPriceService.itemMap(coinUids: Array(self.allCoinUids(items: items))) } - self.updatePriceItems(items: items, map: itemsMap) + self.updatePriceItems(items: items, map: _itemsMap) self.state = .loaded(items: items, allLoaded: allLoaded) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/NftCollection/Overview/NftCollectionOverviewViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/NftCollection/Overview/NftCollectionOverviewViewModel.swift index 0c7540b413..74c94b1d35 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/NftCollection/Overview/NftCollectionOverviewViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/NftCollection/Overview/NftCollectionOverviewViewModel.swift @@ -50,7 +50,7 @@ class NftCollectionOverviewViewModel { contracts: collection.contracts.map { contractViewItem(contract: $0) }, links: linkViewItems(collection: collection), statsViewItems: statViewItem(collection: collection), - royalty: collection.royalty.flatMap { ValueFormatter.instance.format(percentValue: $0, showSign: false) }, + royalty: collection.royalty.flatMap { ValueFormatter.instance.format(percentValue: $0, signType: .never) }, inceptionDate: collection.inceptionDate.map { DateFormatter.cachedFormatter(format: "MMMM d, yyyy").string(from: $0) } ) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/DuressMode/DuressModeIntroView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/DuressMode/DuressModeIntroView.swift index 0041684935..b45655e1d8 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/DuressMode/DuressModeIntroView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/DuressMode/DuressModeIntroView.swift @@ -8,35 +8,37 @@ struct DuressModeIntroView: View { var body: some View { ThemeView { BottomGradientWrapper { - VStack(spacing: 0) { - PageDescription(text: "enable_duress_mode.intro.description".localized) - + ScrollView { VStack(spacing: 0) { - ListSectionHeader(text: "enable_duress_mode.intro.notes".localized) - ListSection { - if let biometryType = viewModel.biometryType { + PageDescription(text: "enable_duress_mode.intro.description".localized) + + VStack(spacing: 0) { + ListSectionHeader(text: "enable_duress_mode.intro.notes".localized) + ListSection { + if let biometryType = viewModel.biometryType { + InfoRow( + icon: Image(biometryType.iconName), + title: biometryType.title, + description: "enable_duress_mode.intro.biometrics.description".localized(biometryType.title, biometryType.title) + ) + } + InfoRow( - icon: Image(biometryType.iconName), - title: biometryType.title, - description: "enable_duress_mode.intro.biometrics.description".localized(biometryType.title, biometryType.title) + icon: Image("dialpad_alt_2_24"), + title: "enable_duress_mode.intro.passcode_disabling".localized, + description: "enable_duress_mode.intro.passcode_disabling.description".localized ) - } - InfoRow( - icon: Image("dialpad_alt_2_24"), - title: "enable_duress_mode.intro.passcode_disabling".localized, - description: "enable_duress_mode.intro.passcode_disabling.description".localized - ) - - InfoRow( - icon: Image("edit_24"), - title: "enable_duress_mode.intro.passcode_change".localized, - description: "enable_duress_mode.intro.passcode_change.description".localized - ) + InfoRow( + icon: Image("edit_24"), + title: "enable_duress_mode.intro.passcode_change".localized, + description: "enable_duress_mode.intro.passcode_change.description".localized + ) + } + .themeListStyle(.bordered) } - .themeListStyle(.bordered) + .padding(EdgeInsets(top: 0, leading: .margin16, bottom: .margin32, trailing: .margin16)) } - .padding(EdgeInsets(top: 0, leading: .margin16, bottom: .margin32, trailing: .margin16)) } } bottomContent: { NavigationLink(destination: { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/DuressMode/DuressModeSelectView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/DuressMode/DuressModeSelectView.swift index a08173a7ee..4becc7d96e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/DuressMode/DuressModeSelectView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Passcode/DuressMode/DuressModeSelectView.swift @@ -7,33 +7,35 @@ struct DuressModeSelectView: View { var body: some View { ThemeView { BottomGradientWrapper { - VStack(spacing: 0) { - PageDescription(text: "enable_duress_mode.select.description".localized) + ScrollView { + VStack(spacing: 0) { + PageDescription(text: "enable_duress_mode.select.description".localized) - VStack(spacing: .margin24) { - if !viewModel.regularAccounts.isEmpty { - VStack(spacing: 0) { - ListSectionHeader(text: "enable_duress_mode.select.wallets".localized) - ListSection { - ForEach(viewModel.regularAccounts) { account in - AccountRow(account: account, selectedAccountIds: $viewModel.selectedAccountIds) + VStack(spacing: .margin24) { + if !viewModel.regularAccounts.isEmpty { + VStack(spacing: 0) { + ListSectionHeader(text: "enable_duress_mode.select.wallets".localized) + ListSection { + ForEach(viewModel.regularAccounts) { account in + AccountRow(account: account, selectedAccountIds: $viewModel.selectedAccountIds) + } } } } - } - if !viewModel.watchAccounts.isEmpty { - VStack(spacing: 0) { - ListSectionHeader(text: "enable_duress_mode.select.watch_wallets".localized) - ListSection { - ForEach(viewModel.watchAccounts) { account in - AccountRow(account: account, selectedAccountIds: $viewModel.selectedAccountIds) + if !viewModel.watchAccounts.isEmpty { + VStack(spacing: 0) { + ListSectionHeader(text: "enable_duress_mode.select.watch_wallets".localized) + ListSection { + ForEach(viewModel.watchAccounts) { account in + AccountRow(account: account, selectedAccountIds: $viewModel.selectedAccountIds) + } } } } } + .padding(EdgeInsets(top: 0, leading: .margin16, bottom: .margin32, trailing: .margin16)) } - .padding(EdgeInsets(top: 0, leading: .margin16, bottom: .margin32, trailing: .margin16)) } } bottomContent: { NavigationLink(destination: { @@ -70,15 +72,7 @@ struct DuressModeSelectView: View { Text(account.type.detailedDescription).themeSubhead2() } - ZStack { - RoundedRectangle(cornerRadius: .cornerRadius4, style: .continuous) - .stroke(Color.themeGray, lineWidth: 1.5) - .frame(width: .margin24, height: .margin24) - - if selectedAccountIds.contains(account.id) { - Image("check_2_20").themeIcon(color: .themeJacob) - } - } + CheckBoxUiView(checked: .init(get: { selectedAccountIds.contains(account.id) }, set: { _ in })) } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/BaseSendViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/BaseSendViewController.swift index af96757a6a..b6bc3e90c2 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/BaseSendViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/BaseSendViewController.swift @@ -8,7 +8,7 @@ import UIKit class BaseSendViewController: ThemeViewController, SectionsDataSource { private let disposeBag = DisposeBag() - let viewModel: SendViewModel + let viewModel: SendViewModelOld let tableView = SectionsTableView(style: .grouped) @@ -34,7 +34,7 @@ class BaseSendViewController: ThemeViewController, SectionsDataSource { init(confirmationFactory: ISendConfirmationFactory, feeSettingsFactory: ISendFeeSettingsFactory? = nil, - viewModel: SendViewModel, + viewModel: SendViewModelOld, availableBalanceViewModel: SendAvailableBalanceViewModel, amountInputViewModel: AmountInputViewModel, amountCautionViewModel: SendAmountCautionViewModel, @@ -76,7 +76,7 @@ class BaseSendViewController: ThemeViewController, SectionsDataSource { iconImageView.snp.makeConstraints { make in make.size.equalTo(CGFloat.iconSize24) } - iconImageView.setImage(withUrlString: viewModel.token.coin.imageUrl, placeholder: UIImage(named: viewModel.token.placeholderImageName)) + iconImageView.setImage(coin: viewModel.token.coin, placeholder: viewModel.token.placeholderImageName) iconImageView.tintColor = .themeGray } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Binance/SendBinanceService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Binance/SendBinanceService.swift index ddb17105e2..0d4e570a0e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Binance/SendBinanceService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Binance/SendBinanceService.swift @@ -9,7 +9,7 @@ class SendBinanceService { private let scheduler = SerialDispatchQueueScheduler(qos: .userInitiated, internalSerialQueueName: "\(AppConfig.label).send-bitcoin-service") let token: Token - let mode: SendBaseService.Mode + let mode: PreSendViewModel.Mode private let amountService: IAmountInputService private let amountCautionService: SendAmountCautionService @@ -24,7 +24,7 @@ class SendBinanceService { } } - init(amountService: IAmountInputService, amountCautionService: SendAmountCautionService, addressService: AddressService, memoService: SendMemoInputService, adapter: ISendBinanceAdapter, reachabilityManager: IReachabilityManager, token: Token, mode: SendBaseService.Mode) { + init(amountService: IAmountInputService, amountCautionService: SendAmountCautionService, addressService: AddressService, memoService: SendMemoInputService, adapter: ISendBinanceAdapter, reachabilityManager: IReachabilityManager, token: Token, mode: PreSendViewModel.Mode) { self.amountService = amountService self.amountCautionService = amountCautionService self.addressService = addressService @@ -38,7 +38,7 @@ class SendBinanceService { addressService.set(text: address) if let amount { addressService.publishAmountRelay.accept(amount) } case let .predefined(address): addressService.set(text: address) - case .send: () + case .regular: () } subscribe(MainScheduler.instance, disposeBag, reachabilityManager.reachabilityObservable) { [weak self] isReachable in diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Binance/SendBinanceViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Binance/SendBinanceViewController.swift index 6b6adf1aaf..edc5b1f4f8 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Binance/SendBinanceViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Binance/SendBinanceViewController.swift @@ -14,7 +14,7 @@ class SendBinanceViewController: BaseSendViewController { private let feeCautionCell = TitledHighlightedDescriptionCell() init(confirmationFactory: ISendConfirmationFactory, - viewModel: SendViewModel, + viewModel: SendViewModelOld, availableBalanceViewModel: SendAvailableBalanceViewModel, amountInputViewModel: AmountInputViewModel, amountCautionViewModel: SendAmountCautionViewModel, diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Bitcoin/OutputSelector/OutputSelectorView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Bitcoin/OutputSelector/OutputSelectorView.swift index bd2b0bdf51..3f9e211da3 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Bitcoin/OutputSelector/OutputSelectorView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Bitcoin/OutputSelector/OutputSelectorView.swift @@ -11,31 +11,33 @@ struct OutputSelectorView: View { var body: some View { ThemeView { BottomGradientWrapper { - VStack(spacing: .margin16) { - ListSection { - ListRow(minHeight: .heightDoubleLineCell) { - amount(subtitle: addressViewModel.address, viewItem: amountViewModel.viewItem) - } - ListRow(minHeight: .heightDoubleLineCell) { - fee(value: feeViewModel.value, spinnerVisible: feeViewModel.spinnerVisible) - } - if let changeViewItem = viewModel.changeViewItem { + ScrollView { + VStack(spacing: .margin16) { + ListSection { + ListRow(minHeight: .heightDoubleLineCell) { + amount(subtitle: addressViewModel.address, viewItem: amountViewModel.viewItem) + } ListRow(minHeight: .heightDoubleLineCell) { - change(viewItem: changeViewItem) + fee(value: feeViewModel.value, spinnerVisible: feeViewModel.spinnerVisible) + } + if let changeViewItem = viewModel.changeViewItem { + ListRow(minHeight: .heightDoubleLineCell) { + change(viewItem: changeViewItem) + } } } - } - .themeListStyle(.borderedLawrence) + .themeListStyle(.borderedLawrence) - ListSection { - ForEach(viewModel.outputsViewItems) { viewItem in - output(viewItem: viewItem) + ListSection { + ForEach(viewModel.outputsViewItems) { viewItem in + output(viewItem: viewItem) + } } + .padding(EdgeInsets(top: 0, leading: .margin16, bottom: 0, trailing: .margin16)) } - .padding(EdgeInsets(top: 0, leading: .margin16, bottom: 0, trailing: .margin16)) + .animation(.easeInOut, value: viewModel.changeViewItem) + .padding(EdgeInsets(top: .margin12, leading: 0, bottom: .margin32, trailing: 0)) } - .animation(.easeInOut, value: viewModel.changeViewItem) - .padding(EdgeInsets(top: .margin12, leading: 0, bottom: .margin32, trailing: 0)) } bottomContent: { Button(action: { viewModel.onTapDone() @@ -138,13 +140,7 @@ struct OutputSelectorView: View { viewModel.toggle(viewItem: viewItem) }) { HStack(spacing: .margin16) { - Image("check_2_20") - .themeIcon(color: .themeJacob) - .opacity(viewModel.selectedSet.contains(viewItem.id) ? 1 : 0) - .overlay( - RoundedRectangle(cornerRadius: .cornerRadius4, style: .continuous) - .stroke(Color.themeGray, lineWidth: .heightOneDp + .heightOnePixel) - ) + CheckBoxUiView(checked: .init(get: { viewModel.selectedSet.contains(viewItem.id) }, set: { _ in })) VStack(spacing: 1) { Text(viewItem.date).themeBody() diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Bitcoin/SendBitcoinAdapterService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Bitcoin/SendBitcoinAdapterService.swift index d8332f4eea..92abefff25 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Bitcoin/SendBitcoinAdapterService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Bitcoin/SendBitcoinAdapterService.swift @@ -134,7 +134,9 @@ class SendBitcoinAdapterService { } sync(feeRate: feeRateService.status) - minimumSendAmount = adapter.minimumSendAmount(address: addressService.state.address?.raw) + let params = SendParameters(address: addressService.state.address?.raw) + + minimumSendAmount = adapter.minimumSendAmount(params: params) maximumSendAmount = adapter.maximumSendAmount(pluginData: pluginData) } @@ -161,12 +163,19 @@ class SendBitcoinAdapterService { } private func update(feeRate: Int, amount: Decimal, address: String?, pluginData: [UInt8: IBitcoinPluginData], updatedFrom: UpdatedField) { - let memo = memoService.memo queue.async { [weak self] in + let params = SendParameters( + address: address, + value: self?.adapter.convertToSatoshi(value: amount), + feeRate: feeRate, + memo: self?.memoService.memo, + unspentOutputs: self?.customOutputs, + pluginData: pluginData + ) + do { - if let sendInfo = try self?.adapter - .sendInfo(amount: amount, feeRate: feeRate, address: address, memo: memo, unspentOutputs: self?.customOutputs, pluginData: pluginData) - { + if let adapter = self?.adapter { + let sendInfo = try adapter.sendInfo(params: params) self?.sendInfoState = .completed(sendInfo) } } catch { @@ -174,7 +183,7 @@ class SendBitcoinAdapterService { } if updatedFrom != .amount, - let availableBalance = self?.adapter.availableBalance(feeRate: feeRate, address: address, memo: memo, unspentOutputs: self?.customOutputs, pluginData: pluginData) + let availableBalance = self?.adapter.availableBalance(params: params) { self?.availableBalance = .completed(availableBalance) } @@ -182,7 +191,7 @@ class SendBitcoinAdapterService { self?.maximumSendAmount = self?.adapter.maximumSendAmount(pluginData: pluginData) } if updatedFrom == .address { - self?.minimumSendAmount = self?.adapter.minimumSendAmount(address: address) ?? 0 + self?.minimumSendAmount = self?.adapter.minimumSendAmount(params: params) ?? 0 } } } @@ -194,7 +203,7 @@ class SendBitcoinAdapterService { extension SendBitcoinAdapterService: ISendInfoValueService, ISendXFeeValueService, IAvailableBalanceService, ISendXSendAmountBoundsService { var unspentOutputs: [UnspentOutputInfo] { - adapter.unspentOutputs + adapter.unspentOutputs(filters: UtxoFilters()) } var customOutputsUpdatedPublisher: AnyPublisher { @@ -244,18 +253,18 @@ extension SendBitcoinAdapterService: ISendService { } let sortMode = btcBlockchainManager.transactionSortMode(blockchainType: adapter.blockchainType) - let rbfEnabled = btcBlockchainManager.transactionRbfEnabled(blockchainType: adapter.blockchainType) - return adapter.sendSingle( - amount: amountInputService.amount, + let params = SendParameters( address: address.raw, - memo: memoService.memo, + value: adapter.convertToSatoshi(value: amountInputService.amount), feeRate: feeRate, + sortType: adapter.convertToKitSortMode(sort: sortMode), + rbfEnabled: btcBlockchainManager.transactionRbfEnabled(blockchainType: adapter.blockchainType), + memo: memoService.memo, unspentOutputs: customOutputs, - pluginData: pluginData, - sortMode: sortMode, - rbfEnabled: rbfEnabled, - logger: logger + pluginData: pluginData ) + + return adapter.sendSingle(params: params, logger: logger) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Bitcoin/SendBitcoinService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Bitcoin/SendBitcoinService.swift index c39bef6be4..11fa7f7887 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Bitcoin/SendBitcoinService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Bitcoin/SendBitcoinService.swift @@ -9,7 +9,7 @@ class SendBitcoinService { private let scheduler = SerialDispatchQueueScheduler(qos: .userInitiated, internalSerialQueueName: "\(AppConfig.label).send-bitcoin-service") let token: Token - let mode: SendBaseService.Mode + let mode: PreSendViewModel.Mode private let amountService: IAmountInputService private let amountCautionService: SendAmountCautionService @@ -25,7 +25,7 @@ class SendBitcoinService { } } - init(amountService: IAmountInputService, amountCautionService: SendAmountCautionService, addressService: AddressService, adapterService: SendBitcoinAdapterService, feeRateService: FeeRateService, timeLockErrorService: SendTimeLockErrorService?, reachabilityManager: IReachabilityManager, token: Token, mode: SendBaseService.Mode) { + init(amountService: IAmountInputService, amountCautionService: SendAmountCautionService, addressService: AddressService, adapterService: SendBitcoinAdapterService, feeRateService: FeeRateService, timeLockErrorService: SendTimeLockErrorService?, reachabilityManager: IReachabilityManager, token: Token, mode: PreSendViewModel.Mode) { self.amountService = amountService self.amountCautionService = amountCautionService self.addressService = addressService @@ -40,7 +40,7 @@ class SendBitcoinService { addressService.set(text: address) if let amount { addressService.publishAmountRelay.accept(amount) } case let .predefined(address): addressService.set(text: address) - case .send: () + case .regular: () } subscribe(MainScheduler.instance, disposeBag, reachabilityManager.reachabilityObservable) { [weak self] isReachable in diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Bitcoin/SendBitcoinViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Bitcoin/SendBitcoinViewController.swift index bfc3fa2d04..64d3914558 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Bitcoin/SendBitcoinViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Bitcoin/SendBitcoinViewController.swift @@ -19,7 +19,7 @@ class SendBitcoinViewController: BaseSendViewController { init(confirmationFactory: ISendConfirmationFactory, feeSettingsFactory: ISendFeeSettingsFactory, outputSelectorFactory: ISendOutputSelectorFactory, - viewModel: SendViewModel, + viewModel: SendViewModelOld, availableBalanceViewModel: SendAvailableBalanceViewModel, amountInputViewModel: AmountInputViewModel, amountCautionViewModel: SendAmountCautionViewModel, diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/SendBaseService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/SendBaseService.swift index 8023ca8f2c..43e19f46fa 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/SendBaseService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/SendBaseService.swift @@ -9,19 +9,6 @@ extension SendBaseService { case notReady } - enum Mode { - case send - case prefilled(address: String, amount: Decimal?) - case predefined(address: String) - - var amount: Decimal? { - switch self { - case let .prefilled(_, amount): return amount - default: return nil - } - } - } - enum AmountError: Error { case invalidDecimal case insufficientBalance diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/SendModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/SendModule.swift index ed5539b41b..e5d0085354 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/SendModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/SendModule.swift @@ -8,7 +8,7 @@ protocol ITitledCautionViewModel { } enum SendModule { - static func controller(wallet: Wallet, mode: SendBaseService.Mode = .send) -> UIViewController? { + static func controller(wallet: Wallet, mode: PreSendViewModel.Mode = .regular) -> UIViewController? { guard let adapter = App.shared.adapterManager.adapter(for: wallet) else { return nil } @@ -33,7 +33,7 @@ enum SendModule { } } - private static func viewController(token: Token, mode: SendBaseService.Mode, adapter: ISendBitcoinAdapter) -> UIViewController? { + private static func viewController(token: Token, mode: PreSendViewModel.Mode, adapter: ISendBitcoinAdapter) -> UIViewController? { guard let feeRateProvider = App.shared.feeRateProviderFactory.provider(blockchainType: token.blockchainType) else { return nil } @@ -117,7 +117,7 @@ enum SendModule { feeService.feeValueService = bitcoinAdapterService // ViewModels - let viewModel = SendViewModel(service: service) + let viewModel = SendViewModelOld(service: service) let availableBalanceViewModel = SendAvailableBalanceViewModel(service: bitcoinAdapterService, coinService: coinService, switchService: switchService) let amountInputViewModel = AmountInputViewModel( service: amountInputService, @@ -174,7 +174,7 @@ enum SendModule { return viewController } - private static func viewController(token: Token, mode: SendBaseService.Mode, adapter: ISendBinanceAdapter) -> UIViewController? { + private static func viewController(token: Token, mode: PreSendViewModel.Mode, adapter: ISendBinanceAdapter) -> UIViewController? { let feeToken = App.shared.feeCoinProvider.feeToken(token: token) ?? token let switchService = AmountTypeSwitchService(userDefaultsStorage: App.shared.userDefaultsStorage) @@ -219,7 +219,7 @@ enum SendModule { feeService.feeValueService = service // ViewModels - let viewModel = SendViewModel(service: service) + let viewModel = SendViewModelOld(service: service) let availableBalanceViewModel = SendAvailableBalanceViewModel(service: service, coinService: coinService, switchService: switchService) let amountInputViewModel = AmountInputViewModel( service: amountInputService, @@ -267,7 +267,7 @@ enum SendModule { return viewController } - private static func viewController(token: Token, mode: SendBaseService.Mode, adapter: ISendZcashAdapter) -> UIViewController? { + private static func viewController(token: Token, mode: PreSendViewModel.Mode, adapter: ISendZcashAdapter) -> UIViewController? { let switchService = AmountTypeSwitchService(userDefaultsStorage: App.shared.userDefaultsStorage) let coinService = CoinService(token: token, currencyManager: App.shared.currencyManager, marketKit: App.shared.marketKit) let fiatService = FiatService(switchService: switchService, currencyManager: App.shared.currencyManager, marketKit: App.shared.marketKit, amount: mode.amount ?? 0) @@ -311,7 +311,7 @@ enum SendModule { feeService.feeValueService = service // ViewModels - let viewModel = SendViewModel(service: service) + let viewModel = SendViewModelOld(service: service) let availableBalanceViewModel = SendAvailableBalanceViewModel(service: service, coinService: coinService, switchService: switchService) let amountInputViewModel = AmountInputViewModel( service: amountInputService, @@ -357,7 +357,7 @@ enum SendModule { return viewController } - private static func viewController(token: Token, mode: SendBaseService.Mode, adapter: ISendTonAdapter) -> UIViewController? { + private static func viewController(token: Token, mode: PreSendViewModel.Mode, adapter: ISendTonAdapter) -> UIViewController? { let switchService = AmountTypeSwitchService(userDefaultsStorage: App.shared.userDefaultsStorage) let coinService = CoinService(token: token, currencyManager: App.shared.currencyManager, marketKit: App.shared.marketKit) let fiatService = FiatService(switchService: switchService, currencyManager: App.shared.currencyManager, marketKit: App.shared.marketKit) @@ -401,7 +401,7 @@ enum SendModule { feeService.feeValueService = service // ViewModels - let viewModel = SendViewModel(service: service) + let viewModel = SendViewModelOld(service: service) let availableBalanceViewModel = SendAvailableBalanceViewModel(service: service, coinService: coinService, switchService: switchService) let amountInputViewModel = AmountInputViewModel( service: amountInputService, diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/SendViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/SendViewModelOld.swift similarity index 88% rename from UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/SendViewModel.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/SendViewModelOld.swift index 667a6490b4..2a8b46c22c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/SendViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/SendViewModelOld.swift @@ -5,12 +5,12 @@ import RxSwift protocol ISendBaseService { var token: Token { get } - var mode: SendBaseService.Mode { get } + var mode: PreSendViewModel.Mode { get } var state: SendBaseService.State { get } var stateObservable: Observable { get } } -class SendViewModel { +class SendViewModelOld { private let service: ISendBaseService private let disposeBag = DisposeBag() @@ -42,7 +42,7 @@ class SendViewModel { } } -extension SendViewModel { +extension SendViewModelOld { var proceedEnableDriver: Driver { proceedEnabledRelay.asDriver() } @@ -57,14 +57,14 @@ extension SendViewModel { var title: String { switch service.mode { - case .send, .prefilled: return "send.title".localized(token.coin.code) + case .regular, .prefilled: return "send.title".localized(token.coin.code) case .predefined: return "donate.title".localized(token.coin.code) } } var showAddress: Bool { switch service.mode { - case .send, .prefilled: return true + case .regular, .prefilled: return true case .predefined: return false } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Ton/SendTonService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Ton/SendTonService.swift index 45421091d0..00c590dfbb 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Ton/SendTonService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Ton/SendTonService.swift @@ -10,7 +10,7 @@ class SendTonService { private let scheduler = SerialDispatchQueueScheduler(qos: .userInitiated, internalSerialQueueName: "\(AppConfig.label).send-bitcoin-service") let token: Token - let mode: SendBaseService.Mode + let mode: PreSendViewModel.Mode private let amountService: IAmountInputService private let amountCautionService: SendAmountCautionService @@ -44,7 +44,7 @@ class SendTonService { } } - init(amountService: IAmountInputService, amountCautionService: SendAmountCautionService, addressService: AddressService, memoService: SendMemoInputService, adapter: ISendTonAdapter, reachabilityManager: IReachabilityManager, token: Token, mode: SendBaseService.Mode) { + init(amountService: IAmountInputService, amountCautionService: SendAmountCautionService, addressService: AddressService, memoService: SendMemoInputService, adapter: ISendTonAdapter, reachabilityManager: IReachabilityManager, token: Token, mode: PreSendViewModel.Mode) { self.amountService = amountService self.amountCautionService = amountCautionService self.addressService = addressService @@ -58,7 +58,7 @@ class SendTonService { addressService.set(text: address) if let amount { addressService.publishAmountRelay.accept(amount) } case let .predefined(address): addressService.set(text: address) - case .send: () + case .regular: () } availableBalance = .completed(adapter.availableBalance) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Ton/SendTonViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Ton/SendTonViewController.swift index b0c13024cb..cc5ee21204 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Ton/SendTonViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Ton/SendTonViewController.swift @@ -7,7 +7,7 @@ class SendTonViewController: BaseSendViewController { private let feeCell: FeeCell init(confirmationFactory: ISendConfirmationFactory, - viewModel: SendViewModel, + viewModel: SendViewModelOld, availableBalanceViewModel: SendAvailableBalanceViewModel, amountInputViewModel: AmountInputViewModel, amountCautionViewModel: SendAmountCautionViewModel, diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Zcash/SendZcashService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Zcash/SendZcashService.swift index 56bc325902..a5f336c997 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Zcash/SendZcashService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Zcash/SendZcashService.swift @@ -10,7 +10,7 @@ class SendZcashService { private let scheduler = SerialDispatchQueueScheduler(qos: .userInitiated, internalSerialQueueName: "\(AppConfig.label).send-bitcoin-service") let token: Token - let mode: SendBaseService.Mode + let mode: PreSendViewModel.Mode private let amountService: IAmountInputService private let amountCautionService: SendAmountCautionService @@ -32,7 +32,7 @@ class SendZcashService { } } - init(amountService: IAmountInputService, amountCautionService: SendAmountCautionService, addressService: AddressService, memoService: SendMemoInputService, adapter: ISendZcashAdapter, reachabilityManager: IReachabilityManager, token: Token, mode: SendBaseService.Mode) { + init(amountService: IAmountInputService, amountCautionService: SendAmountCautionService, addressService: AddressService, memoService: SendMemoInputService, adapter: ISendZcashAdapter, reachabilityManager: IReachabilityManager, token: Token, mode: PreSendViewModel.Mode) { self.amountService = amountService self.amountCautionService = amountCautionService self.addressService = addressService @@ -46,7 +46,7 @@ class SendZcashService { addressService.set(text: address) if let amount { addressService.publishAmountRelay.accept(amount) } case let .predefined(address): addressService.set(text: address) - case .send: () + case .regular: () } subscribe(MainScheduler.instance, disposeBag, reachabilityManager.reachabilityObservable) { [weak self] isReachable in diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Zcash/SendZcashViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Zcash/SendZcashViewController.swift index de63365c97..0f687d87d7 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Zcash/SendZcashViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Send/Platforms/Zcash/SendZcashViewController.swift @@ -7,7 +7,7 @@ class SendZcashViewController: BaseSendViewController { private let feeCell: FeeCell init(confirmationFactory: ISendConfirmationFactory, - viewModel: SendViewModel, + viewModel: SendViewModelOld, availableBalanceViewModel: SendAvailableBalanceViewModel, amountInputViewModel: AmountInputViewModel, amountCautionViewModel: SendAmountCautionViewModel, diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Send/SwiftUI/SendAmountView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Send/SwiftUI/SendAmountView.swift deleted file mode 100644 index 4c031a2c1e..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Send/SwiftUI/SendAmountView.swift +++ /dev/null @@ -1,54 +0,0 @@ -import SwiftUI -import ThemeKit - -struct SendAmountView: View { - @ObservedObject var viewModel: SendAmountViewModel - - var body: some View { - VStack(spacing: 0) { - HStack { - switch viewModel.inputType { - case .coin: EmptyView() - case .currency: Text(viewModel.currency.symbol).textBody(color: .themeJacob) - } - - TextField( - "0", - text: $viewModel.text, - onEditingChanged: { print("changed: \($0)") }, - onCommit: { print("commit") } - ) - .keyboardType(.decimalPad) - .foregroundColor(viewModel.inputType == .coin ? .themeLeah : .themeJacob) - .accentColor(.themeYellow) - .padding(.vertical, .margin12) - - if viewModel.text.isEmpty { - Button(action: { print("tap Max") }) { - Text("send.max_button".localized) - } - .buttonStyle(SecondaryButtonStyle()) - } else { - Button(action: { print("tap Delete") }) { - Image("trash_20").renderingMode(.template) - } - .buttonStyle(SecondaryCircleButtonStyle(style: .default)) - } - } - .padding(.horizontal, .margin16) - - HorizontalDivider(color: .themeSteel20) - .padding(.horizontal, .margin8) - - Button(action: { viewModel.toggleInputType() }) { - switch viewModel.inputType { - case .coin: Text(ValueFormatter.instance.formatFull(currencyValue: CurrencyValue(currency: viewModel.currency, value: viewModel.currencyAmount)) ?? "").themeSubhead2(color: .themeJacob) - case .currency: Text(ValueFormatter.instance.formatFull(coinValue: CoinValue(kind: .token(token: viewModel.token), value: viewModel.coinAmount)) ?? "").themeSubhead2(color: .themeLeah) - } - } - .padding(.horizontal, .margin16) - .padding(.vertical, .margin12) - } - .modifier(InputRowModifier()) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Send/SwiftUI/SendAmountViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Send/SwiftUI/SendAmountViewModel.swift deleted file mode 100644 index 6126cb4a99..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Send/SwiftUI/SendAmountViewModel.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Combine -import Foundation -import MarketKit - -class SendAmountViewModel: ObservableObject { - let token: Token - - @Published var inputType: InputType = .coin - @Published var text: String = "" { - didSet {} - } - - @Published var coinAmount: Decimal = 0 - @Published var currencyAmount: Decimal = 0 - - let currency: Currency - - init(token: Token, currencyManager: CurrencyManager) { - self.token = token - currency = currencyManager.baseCurrency - } - - func toggleInputType() { - switch inputType { - case .coin: inputType = .currency - case .currency: inputType = .coin - } - } -} - -extension SendAmountViewModel { - enum InputType { - case coin - case currency - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Send/SwiftUI/SendModuleNew.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Send/SwiftUI/SendModuleNew.swift deleted file mode 100644 index a89101c840..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Send/SwiftUI/SendModuleNew.swift +++ /dev/null @@ -1,16 +0,0 @@ -import MarketKit -import SwiftUI - -enum SendModuleNew { - static func view(adapter _: ISendTonAdapter) -> some View { - let token = try? App.shared.marketKit.token(query: TokenQuery(blockchainType: .ton, tokenType: .native)) - - let viewModel = SendViewModelNew(token: token!) - return SendView(viewModel: viewModel) - } - - static func amountView(token: Token) -> some View { - let viewModel = SendAmountViewModel(token: token, currencyManager: App.shared.currencyManager) - return SendAmountView(viewModel: viewModel) - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Send/SwiftUI/SendView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Send/SwiftUI/SendView.swift deleted file mode 100644 index 8d1ac17aec..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Send/SwiftUI/SendView.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Kingfisher -import SwiftUI -import ThemeKit - -struct SendView: View { - @ObservedObject var viewModel: SendViewModelNew - - @Environment(\.presentationMode) private var presentationMode - - var body: some View { - ScrollableThemeView { - VStack(spacing: .margin16) { - HStack(spacing: .margin8) { - Text("send.available_balance".localized).textSubhead2() - Spacer() - Text("12345.678").textSubhead2(color: .themeLeah) - } - .padding(.horizontal, .margin16) - .padding(.vertical, .margin12) - .overlay(RoundedRectangle(cornerRadius: .cornerRadius12, style: .continuous).stroke(Color.themeSteel20, lineWidth: .heightOneDp)) - - SendModuleNew.amountView(token: viewModel.token) - - Button(action: {}) { - Text("button.next".localized) - } - .buttonStyle(PrimaryButtonStyle(style: .yellow)) - } - .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin16, trailing: .margin16)) - } - .navigationTitle("Send \(viewModel.token.coin.code)") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - KFImage.url(URL(string: viewModel.token.coin.imageUrl)) - .resizable() - .frame(width: .iconSize24, height: .iconSize24) - } - - ToolbarItem(placement: .navigationBarTrailing) { - Button("button.cancel".localized) { - presentationMode.wrappedValue.dismiss() - } - } - } - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Send/SwiftUI/SendViewModelNew.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Send/SwiftUI/SendViewModelNew.swift deleted file mode 100644 index 127d7dd08c..0000000000 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Send/SwiftUI/SendViewModelNew.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Combine -import Foundation -import MarketKit - -class SendViewModelNew: ObservableObject { - let token: Token - - @Published var availableBalance: Decimal = 123 - - init(token: Token) { - self.token = token - } -} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendEvm/SendEvmModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendEvm/SendEvmModule.swift index 0ffc1db6bd..ff0feb9c45 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SendEvm/SendEvmModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendEvm/SendEvmModule.swift @@ -3,7 +3,7 @@ import ThemeKit import UIKit enum SendEvmModule { - static func viewController(token: Token, mode: SendBaseService.Mode, adapter: ISendEthereumAdapter) -> UIViewController { + static func viewController(token: Token, mode: PreSendViewModel.Mode, adapter: ISendEthereumAdapter) -> UIViewController { let evmAddressParserItem = EvmAddressParser() let udnAddressParserItem = UdnAddressParserItem.item(rawAddressParserItem: evmAddressParserItem, coinCode: token.coin.code, token: token) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendEvm/SendEvmService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendEvm/SendEvmService.swift index 659d66db67..16b32e180f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SendEvm/SendEvmService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendEvm/SendEvmService.swift @@ -8,7 +8,7 @@ import RxSwift class SendEvmService { let sendToken: Token - let mode: SendBaseService.Mode + let mode: PreSendViewModel.Mode private let disposeBag = DisposeBag() private let adapter: ISendEthereumAdapter @@ -31,7 +31,7 @@ class SendEvmService { } } - init(token: Token, mode: SendBaseService.Mode, adapter: ISendEthereumAdapter, addressService: AddressService) { + init(token: Token, mode: PreSendViewModel.Mode, adapter: ISendEthereumAdapter, addressService: AddressService) { sendToken = token self.mode = mode self.adapter = adapter @@ -42,7 +42,7 @@ class SendEvmService { addressService.set(text: address) if let amount { addressService.publishAmountRelay.accept(amount) } case let .predefined(address): addressService.set(text: address) - case .send: () + case .regular: () } subscribe(disposeBag, addressService.stateObservable) { [weak self] in self?.sync(addressState: $0) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendEvm/SendEvmViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendEvm/SendEvmViewController.swift index fd8333976d..bf4bc623f5 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SendEvm/SendEvmViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendEvm/SendEvmViewController.swift @@ -60,7 +60,7 @@ class SendEvmViewController: ThemeViewController { iconImageView.snp.makeConstraints { make in make.size.equalTo(CGFloat.iconSize24) } - iconImageView.setImage(withUrlString: viewModel.token.coin.imageUrl, placeholder: UIImage(named: viewModel.token.placeholderImageName)) + iconImageView.setImage(coin: viewModel.token.coin, placeholder: viewModel.token.placeholderImageName) view.addSubview(tableView) tableView.snp.makeConstraints { maker in diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendEvm/SendEvmViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendEvm/SendEvmViewModel.swift index 266c67a4c5..123efceca8 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SendEvm/SendEvmViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendEvm/SendEvmViewModel.swift @@ -47,14 +47,14 @@ class SendEvmViewModel { extension SendEvmViewModel { var title: String { switch service.mode { - case .send, .prefilled: return "send.title".localized(token.coin.code) + case .regular, .prefilled: return "send.title".localized(token.coin.code) case .predefined: return "donate.title".localized(token.coin.code) } } var showAddress: Bool { switch service.mode { - case .send, .prefilled: return true + case .regular, .prefilled: return true case .predefined: return false } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/BaseSendBtcData.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BaseSendBtcData.swift similarity index 50% rename from UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/BaseSendBtcData.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BaseSendBtcData.swift index 445e649a67..36f64fe022 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/BaseSendBtcData.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BaseSendBtcData.swift @@ -1,29 +1,28 @@ +import BitcoinCore import Foundation import MarketKit class BaseSendBtcData { let satoshiPerByte: Int? - let bytes: Int? + let fee: Decimal? - init(satoshiPerByte: Int?, bytes: Int?) { + init(satoshiPerByte: Int?, fee: Decimal?) { self.satoshiPerByte = satoshiPerByte - self.bytes = bytes + self.fee = fee } func amountData(feeToken: Token, currency: Currency, feeTokenRate: Decimal?) -> AmountData? { - guard let satoshiPerByte, let bytes else { + guard let fee else { return nil } - let amount = Decimal(satoshiPerByte) * Decimal(bytes) / pow(10, feeToken.decimals) - return AmountData( - coinValue: CoinValue(kind: .token(token: feeToken), value: amount), - currencyValue: feeTokenRate.map { CurrencyValue(currency: currency, value: amount * $0) } + coinValue: CoinValue(kind: .token(token: feeToken), value: fee), + currencyValue: feeTokenRate.map { CurrencyValue(currency: currency, value: fee * $0) } ) } - func feeFields(feeToken: Token, currency: Currency, feeTokenRate: Decimal?) -> [SendConfirmField] { + func feeFields(feeToken: Token, currency: Currency, feeTokenRate: Decimal?) -> [SendField] { let amountData = amountData(feeToken: feeToken, currency: currency, feeTokenRate: feeTokenRate) return [ @@ -36,4 +35,26 @@ class BaseSendBtcData { ), ] } + + func caution(transactionError: Error, feeToken: Token) -> CautionNew { + let title: String + let text: String + + if let error = transactionError as? BitcoinCoreErrors.SendValueErrors { + switch error { + case .notEnough: + title = "fee_settings.errors.insufficient_balance".localized + text = "fee_settings.errors.insufficient_balance.info".localized(feeToken.coin.code) + + default: + title = "Send Info error" + text = "Send Info error description" + } + } else { + title = "alert.error".localized + text = transactionError.convertedError.smartDescription + } + + return CautionNew(title: title, text: text, type: .error) + } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/BaseSendEvmData.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BaseSendEvmData.swift similarity index 94% rename from UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/BaseSendEvmData.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BaseSendEvmData.swift index 4159e4c61a..8c54953c85 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/BaseSendEvmData.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BaseSendEvmData.swift @@ -13,7 +13,7 @@ class BaseSendEvmData { self.nonce = nonce } - func feeFields(feeToken: Token, currency: Currency, feeTokenRate: Decimal?) -> [SendConfirmField] { + func feeFields(feeToken: Token, currency: Currency, feeTokenRate: Decimal?) -> [SendField] { let amountData = evmFeeData.flatMap { $0.totalAmountData(gasPrice: gasPrice, feeToken: feeToken, currency: currency, feeTokenRate: feeTokenRate) } return [ @@ -27,7 +27,7 @@ class BaseSendEvmData { ] } - func caution(transactionError: Error, feeToken: Token?) -> CautionNew { + func caution(transactionError: Error, feeToken: Token) -> CautionNew { let title: String let text: String @@ -35,7 +35,7 @@ class BaseSendEvmData { switch reason { case .insufficientBalanceWithFee: title = "fee_settings.errors.insufficient_balance".localized - text = "ethereum_transaction.error.insufficient_balance_with_fee".localized(feeToken?.coin.code ?? "") + text = "ethereum_transaction.error.insufficient_balance_with_fee".localized(feeToken.coin.code) case let .executionReverted(message): title = "fee_settings.errors.unexpected_error".localized text = message diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BinancePreSendHandler.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BinancePreSendHandler.swift new file mode 100644 index 0000000000..de6dde15d9 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BinancePreSendHandler.swift @@ -0,0 +1,60 @@ +import Combine +import Foundation +import MarketKit +import RxSwift + +class BinancePreSendHandler { + private let token: Token + private let adapter: ISendBinanceAdapter & IBalanceAdapter + + private let stateSubject = PassthroughSubject() + private let balanceSubject = PassthroughSubject() + + private let disposeBag = DisposeBag() + + init(token: Token, adapter: ISendBinanceAdapter & IBalanceAdapter) { + self.token = token + self.adapter = adapter + + adapter.balanceStateUpdatedObservable + .observeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) + .subscribe { [weak self] state in + self?.stateSubject.send(state) + } + .disposed(by: disposeBag) + + adapter.balanceDataUpdatedObservable + .observeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) + .subscribe { [weak self, adapter] _ in + self?.balanceSubject.send(adapter.availableBalance) + } + .disposed(by: disposeBag) + } +} + +extension BinancePreSendHandler: IPreSendHandler { + var state: AdapterState { + adapter.balanceState + } + + var statePublisher: AnyPublisher { + stateSubject.eraseToAnyPublisher() + } + + var balance: Decimal { + adapter.availableBalance + } + + var balancePublisher: AnyPublisher { + balanceSubject.eraseToAnyPublisher() + } + + func hasMemo(address _: String?) -> Bool { + true + } + + func sendData(amount: Decimal, address: String, memo: String?) -> SendDataResult { + let sendData: SendData = .binance(token: token, amount: amount, address: address, memo: memo) + return .valid(sendData: sendData) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BinanceSendHandler.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BinanceSendHandler.swift new file mode 100644 index 0000000000..eae9cedbc3 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BinanceSendHandler.swift @@ -0,0 +1,135 @@ +import Foundation +import MarketKit + +class BinanceSendHandler { + let baseToken: Token + private let token: Token + private let amount: Decimal + private let address: String + private let memo: String? + private var adapter: ISendBinanceAdapter + + init(baseToken: Token, token: Token, amount: Decimal, address: String, memo: String?, adapter: ISendBinanceAdapter) { + self.baseToken = baseToken + self.token = token + self.amount = amount + self.address = address + self.memo = memo + self.adapter = adapter + } +} + +extension BinanceSendHandler: ISendHandler { + func sendData(transactionSettings _: TransactionSettings?) async throws -> ISendData { + SendData( + token: token, + amount: amount, + address: address, + memo: memo, + fee: adapter.fee + ) + } + + func send(data: ISendData) async throws { + guard let data = data as? SendData else { + throw SendError.invalidData + } + + _ = try await adapter.send(amount: data.amount, address: data.address, memo: data.memo) + } +} + +extension BinanceSendHandler { + class SendData: ISendData { + private let token: Token + let amount: Decimal + let address: String + let memo: String? + private let fee: Decimal + + init(token: Token, amount: Decimal, address: String, memo: String?, fee: Decimal) { + self.token = token + self.amount = amount + self.address = address + self.memo = memo + self.fee = fee + } + + var feeData: FeeData? { + nil + } + + var canSend: Bool { + true + } + + var rateCoins: [Coin] { + [token.coin] + } + + func cautions(baseToken _: Token) -> [CautionNew] { + [] + } + + func sections(baseToken: Token, currency: Currency, rates: [String: Decimal]) -> [[SendField]] { + var fields: [SendField] = [ + .amount( + title: "send.confirmation.you_send".localized, + token: token, + coinValueType: .regular(coinValue: CoinValue(kind: .token(token: token), value: amount)), + currencyValue: rates[token.coin.uid].map { CurrencyValue(currency: currency, value: $0 * amount) }, + type: .neutral + ), + .address( + title: "send.confirmation.to".localized, + value: address, + blockchainType: .binanceChain + ), + ] + + if let memo { + fields.append(.levelValue(title: "send.confirmation.memo".localized, value: memo, level: .regular)) + } + + return [ + fields, + [ + .value( + title: "fee_settings.network_fee".localized, + description: .init(title: "fee_settings.network_fee".localized, description: "fee_settings.network_fee.info".localized), + coinValue: CoinValue(kind: .token(token: baseToken), value: fee), + currencyValue: rates[baseToken.coin.uid].map { CurrencyValue(currency: currency, value: fee * $0) }, + formatFull: true + ), + ], + ] + } + } +} + +extension BinanceSendHandler { + enum SendError: Error { + case invalidData + } +} + +extension BinanceSendHandler { + static func instance(token: Token, amount: Decimal, address: String, memo: String?) -> BinanceSendHandler? { + guard let baseToken = try? App.shared.coinManager.token(query: .init(blockchainType: .binanceChain, tokenType: .native)) else { + return nil + } + + guard let adapter = App.shared.adapterManager.adapter(for: token) as? ISendBinanceAdapter else { + return nil + } + + return BinanceSendHandler( + baseToken: baseToken, + token: token, + amount: amount, + address: address, + memo: memo, + adapter: adapter + ) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BitcoinFeeData.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BitcoinFeeData.swift new file mode 100644 index 0000000000..b1f94135e5 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BitcoinFeeData.swift @@ -0,0 +1,19 @@ +import BigInt +import EvmKit +import Foundation +import MarketKit + +struct BitcoinFeeData { + let fee: Decimal + + init(fee: Decimal) { + self.fee = fee + } + + func amountData(feeToken: Token, currency: Currency, feeTokenRate: Decimal?) -> AmountData? { + let coinValue = CoinValue(kind: .token(token: feeToken), value: fee) + let currencyValue = feeTokenRate.map { CurrencyValue(currency: currency, value: fee * $0) } + + return AmountData(coinValue: coinValue, currencyValue: currencyValue) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BitcoinPreSendHandler.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BitcoinPreSendHandler.swift new file mode 100644 index 0000000000..b9d230fdf6 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BitcoinPreSendHandler.swift @@ -0,0 +1,165 @@ +import BigInt +import BitcoinCore +import Combine +import Foundation +import Hodler +import MarketKit +import RxSwift +import SwiftUI + +class BitcoinPreSendHandler { + let token: Token + let defaultSortMode: TransactionDataSortMode + let defaultRbfEnabled: Bool + + var customUtxos: [UnspentOutputInfo]? { + didSet { + balanceSubject.send(availableBalanceDecimal) + settingsModifiedSubject.send(settingsModified) + } + } + + var sortMode: TransactionDataSortMode { + didSet { + blockchainManager.save(transactionSortMode: sortMode, blockchainType: token.blockchainType) + settingsModifiedSubject.send(settingsModified) + } + } + + var rbfEnabled: Bool { + didSet { + blockchainManager.save(rbfEnabled: rbfEnabled, blockchainType: token.blockchainType) + settingsModifiedSubject.send(settingsModified) + } + } + + var lockTimeInterval: HodlerPlugin.LockTimeInterval? { + didSet { + settingsModifiedSubject.send(settingsModified) + } + } + + var allUtxos = [UnspentOutputInfo]() + var availableBalance: Int { + let utxos = customUtxos ?? allUtxos + return utxos.map(\.value).reduce(0, +) + } + + var availableBalanceDecimal: Decimal { + let coinRate = pow(10, token.decimals) + return Decimal(availableBalance) / coinRate + } + + private let adapter: BitcoinBaseAdapter + private let blockchainManager: BtcBlockchainManager + private let disposeBag = DisposeBag() + + private let stateSubject = PassthroughSubject() + private let balanceSubject = PassthroughSubject() + private let settingsModifiedSubject = PassthroughSubject() + + private var pluginData: [UInt8: IPluginData] { + guard let lockTimeInterval else { + return [:] + } + + return [HodlerPlugin.id: HodlerData(lockTimeInterval: lockTimeInterval)] + } + + init(token: Token, adapter: BitcoinBaseAdapter) { + self.token = token + self.adapter = adapter + + let blockchainType = token.blockchainType + blockchainManager = App.shared.btcBlockchainManager + + defaultSortMode = blockchainManager.transactionSortMode(blockchainType: blockchainType) + sortMode = defaultSortMode + + defaultRbfEnabled = blockchainManager.transactionRbfEnabled(blockchainType: blockchainType) + rbfEnabled = defaultRbfEnabled + + adapter.balanceStateUpdatedObservable + .observeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) + .subscribe { [weak self] state in + self?.stateSubject.send(state) + } + .disposed(by: disposeBag) + + adapter.balanceDataUpdatedObservable + .observeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) + .subscribe { [weak self] _ in + self?.syncBalance() + } + .disposed(by: disposeBag) + + syncBalance() + } + + private func syncBalance() { + allUtxos = adapter.unspentOutputs(filters: .init()) + balanceSubject.send(availableBalanceDecimal) + } +} + +extension BitcoinPreSendHandler: IPreSendHandler { + var hasSettings: Bool { + true + } + + var state: AdapterState { + adapter.balanceState + } + + var statePublisher: AnyPublisher { + stateSubject.eraseToAnyPublisher() + } + + var balance: Decimal { + availableBalanceDecimal + } + + var balancePublisher: AnyPublisher { + balanceSubject.eraseToAnyPublisher() + } + + var settingsModified: Bool { + sortMode != defaultSortMode || rbfEnabled != defaultRbfEnabled || customUtxos != nil || lockTimeInterval != nil + } + + var settingsModifiedPublisher: AnyPublisher { + settingsModifiedSubject.eraseToAnyPublisher() + } + + func hasMemo(address _: String?) -> Bool { + true + } + + func settingsView(onChangeSettings: @escaping () -> Void) -> AnyView { + let view = ThemeNavigationView { + BitcoinSendSettingsView(handler: self, onChangeSettings: onChangeSettings) + } + + return AnyView(view) + } + + func sendData(amount: Decimal, address: String, memo: String?) -> SendDataResult { + do { + try adapter.validate(address: address, pluginData: pluginData) + } catch { + return .invalid(cautions: [CautionNew(title: error.title, text: error.convertedError.localizedDescription, type: .error)]) + } + + let params = SendParameters( + address: address, + value: adapter.convertToSatoshi(value: amount), + sortType: adapter.convertToKitSortMode(sort: sortMode), + rbfEnabled: rbfEnabled, + memo: memo, + unspentOutputs: customUtxos, + pluginData: pluginData + ) + + return .valid(sendData: .bitcoin(token: token, params: params)) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BitcoinSendHandler.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BitcoinSendHandler.swift new file mode 100644 index 0000000000..d31f1a5324 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BitcoinSendHandler.swift @@ -0,0 +1,145 @@ +import BitcoinCore +import Foundation +import MarketKit + +class BitcoinSendHandler { + private let token: Token + private var params: SendParameters + private var adapter: BitcoinBaseAdapter + + init(token: Token, params: SendParameters, adapter: BitcoinBaseAdapter) { + self.token = token + self.params = params + self.adapter = adapter + } +} + +extension BitcoinSendHandler: ISendHandler { + var baseToken: MarketKit.Token { + token + } + + var expirationDuration: Int? { + 10 + } + + func sendData(transactionSettings: TransactionSettings?) async throws -> ISendData { + let satoshiPerByte = transactionSettings?.satoshiPerByte + var feeData: BitcoinFeeData? + var transactionError: Error? + let params = params.copy() + + if let satoshiPerByte { + params.feeRate = satoshiPerByte + + let balance = adapter.balanceData.available + let decimalValue = params.value.map { Decimal($0) / adapter.coinRate } + if decimalValue == balance { + params.value = adapter.convertToSatoshi(value: adapter.availableBalance(params: params)) + } + + do { + let sendInfo = try adapter.sendInfo(params: params) + feeData = .init(fee: sendInfo.fee) + } catch { + transactionError = error + } + } + + return SendData( + token: token, + params: params, + transactionError: transactionError, + satoshiPerByte: satoshiPerByte, + feeData: feeData + ) + } + + func send(data _: ISendData) async throws { + try adapter.send(params: params) + } +} + +extension BitcoinSendHandler { + class SendData: BaseSendBtcData, ISendData { + private let token: Token + private let params: SendParameters + private let transactionError: Error? + + init(token: Token, params: SendParameters, transactionError: Error?, satoshiPerByte: Int?, feeData: BitcoinFeeData?) { + self.token = token + self.params = params + self.transactionError = transactionError + + super.init(satoshiPerByte: satoshiPerByte, fee: feeData?.fee) + } + + var feeData: FeeData? { + fee.map { .bitcoin(bitcoinFeeData: BitcoinFeeData(fee: $0)) } + } + + var canSend: Bool { + fee != nil && transactionError == nil + } + + var customSendButtonTitle: String? { + nil + } + + var rateCoins: [MarketKit.Coin] { + [token.coin] + } + + func cautions(baseToken: Token) -> [CautionNew] { + var cautions = [CautionNew]() + + if let transactionError { + cautions.append(caution(transactionError: transactionError, feeToken: baseToken)) + } + + return cautions + } + + func sections(baseToken: Token, currency: Currency, rates: [String: Decimal]) -> [[SendField]] { + guard let toAddress = params.address, let value = params.value else { + return [] + } + + let decimalValue = baseToken.decimalValue(value: value) + let coinValue = CoinValue(kind: .token(token: baseToken), value: -decimalValue) + let rate = rates[baseToken.coin.uid] + + return [ + [ + .amount( + title: "send.confirmation.you_send".localized, + token: baseToken, + coinValueType: .regular(coinValue: coinValue), + currencyValue: rate.map { CurrencyValue(currency: currency, value: $0 * decimalValue) }, + type: .neutral + ), + .address( + title: "send.confirmation.to".localized, + value: toAddress, + blockchainType: baseToken.blockchainType + ), + ], + feeFields(feeToken: baseToken, currency: currency, feeTokenRate: rate), + ] + } + } +} + +extension BitcoinSendHandler { + static func instance(token: Token, params: SendParameters) -> BitcoinSendHandler? { + guard let adapter = App.shared.adapterManager.adapter(for: token) as? BitcoinBaseAdapter else { + return nil + } + + return BitcoinSendHandler( + token: token, + params: params, + adapter: adapter + ) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BitcoinSendSettingsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BitcoinSendSettingsView.swift new file mode 100644 index 0000000000..37ba8647b9 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BitcoinSendSettingsView.swift @@ -0,0 +1,208 @@ +import Foundation +import Hodler +import SwiftUI +import ThemeKit + +struct BitcoinSendSettingsView: View { + @StateObject var viewModel: BitcoinSendSettingsViewModel + var onChangeSettings: () -> Void + + @State private var chooseUtxos: Bool = false + @State private var chooseSortModePresented: Bool = false + @State private var chooseLockPeriodPresented: Bool = false + @State private var inputsOutputsDescriptionPresented: Bool = false + + @Environment(\.presentationMode) private var presentationMode + + init(handler: BitcoinPreSendHandler, onChangeSettings: @escaping () -> Void) { + _viewModel = .init(wrappedValue: BitcoinSendSettingsViewModel(handler: handler)) + self.onChangeSettings = onChangeSettings + } + + var body: some View { + ScrollableThemeView { + VStack(spacing: .margin32) { + VStack(spacing: 0) { + ListSection { + NavigationRow( + destination: { OutputSelectorView2(handler: viewModel.handler) } + ) { + HStack(spacing: .margin8) { + Text("send.unspent_outputs".localized).textBody() + + Spacer() + + HStack(spacing: .margin8) { + Text(viewModel.utxos).textSubhead1(color: .themeLeah) + Image("edit2_20").themeIcon(color: .gray) + } + } + } + } + ListSectionFooter(text: "send.unspent_outputs.description".localized) + } + VStack(spacing: 0) { + ListSection { + ListRow { + HStack(spacing: .margin8) { + Button(action: { + inputsOutputsDescriptionPresented = true + }, label: { + HStack(spacing: .margin8) { + Text("fee_settings.inputs_outputs".localized).textBody() + Image("circle_information_20").themeIcon() + } + }) + + Spacer() + + Button(action: { + chooseSortModePresented = true + }) { + Text(viewModel.sortMode.title) + } + .buttonStyle(SecondaryButtonStyle(rightAccessory: .dropDown)) + } + } + } + ListSectionFooter(text: "fee_settings.transaction_settings.description".localized) + } + VStack(spacing: 0) { + ListSection { + ListRow { + HStack(spacing: .margin8) { + Text("fee_settings.time_lock".localized).textBody() + + Spacer() + + Button(action: { + chooseLockPeriodPresented = true + }) { + HStack(spacing: .margin8) { + if let interval = viewModel.lockTimeInterval { + Text(HodlerPlugin.LockTimeInterval.title(lockTimeInterval: interval)).textCaption(color: .themeLeah) + } else { + Text("send.hodler_locktime_off".localized).textCaption(color: .themeLeah) + } + } + } + .buttonStyle(SecondaryButtonStyle(rightAccessory: .dropDown)) + } + .alert( + isPresented: $chooseLockPeriodPresented, + title: "fee_settings.time_lock".localized, + viewItems: [.init(text: "send.hodler_locktime_off".localized)] + + HodlerPlugin.LockTimeInterval.allCases.map { + AlertViewItem(text: HodlerPlugin.LockTimeInterval.title(lockTimeInterval: $0)) + }, + onTap: { index in + guard let index else { + return + } + switch index { + case 0: viewModel.lockTimeInterval = nil + case 1: viewModel.lockTimeInterval = .hour + case 2: viewModel.lockTimeInterval = .month + case 3: viewModel.lockTimeInterval = .halfYear + case 4: viewModel.lockTimeInterval = .year + default: () + } + } + ) + } + } + + ListSectionFooter(text: "fee_settings.time_lock.description".localized) + } + VStack(spacing: 0) { + ListSection { + ListRow { + Toggle(isOn: $viewModel.rbfEnabled) { + Text("fee_settings.replace_by_fee".localized).themeBody() + } + .toggleStyle(SwitchToggleStyle(tint: .themeYellow)) + } + } + + ListSectionFooter(text: "fee_settings.replace_by_fee.description".localized) + } + } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) + } + .sheet(isPresented: $inputsOutputsDescriptionPresented) { + ThemeNavigationView { + InfoNewView( + viewItems: [ + .header1(text: "send.transaction_inputs_outputs_info.title".localized), + .text(text: "send.transaction_inputs_outputs_info.description".localized(AppConfig.appName, AppConfig.appName)), + .header3(text: "send.transaction_inputs_outputs_info.shuffle.title".localized), + .text(text: "send.transaction_inputs_outputs_info.shuffle.description".localized), + .header3(text: "send.transaction_inputs_outputs_info.deterministic.title".localized), + .text(text: "send.transaction_inputs_outputs_info.deterministic.description".localized), + ], + isPresented: $inputsOutputsDescriptionPresented + ) + } + } + .bottomSheet(isPresented: $chooseSortModePresented) { + VStack(spacing: 0) { + HStack(spacing: .margin8) { + Image("arrow_medium_2_up_right_24").themeIcon(color: .gray) + + Text("fee_settings.transaction_settings".localized).themeHeadline2() + + Button(action: { + chooseSortModePresented = false + }) { + Image("close_3_24") + } + } + .padding(EdgeInsets(top: .margin24, leading: .margin32, bottom: .margin12, trailing: .margin32)) + + VStack(spacing: 0) { + ListSection { + ForEach(TransactionDataSortMode.allCases) { sortMode in + ClickableRow(action: { + if viewModel.sortMode != sortMode { + viewModel.sortMode = sortMode + } + + chooseSortModePresented = false + }) { + VStack(alignment: .leading, spacing: 1) { + Text(sortMode.title).textBody() + Text(sortMode.description).textSubhead2() + } + + Spacer() + + if viewModel.sortMode == sortMode { + Image.checkIcon + } + } + } + } + .overlay(RoundedRectangle(cornerRadius: .cornerRadius12, style: .continuous).stroke(Color.themeSteel20, lineWidth: .heightOneDp)) + } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin24, trailing: .margin16)) + } + } + .navigationTitle("fee_settings".localized) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("button.reset".localized) { + viewModel.reset() + } + .disabled(!viewModel.resetEnabled) + } + + ToolbarItem(placement: .confirmationAction) { + Button("button.done".localized) { + onChangeSettings() + presentationMode.wrappedValue.dismiss() + } + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BitcoinSendSettingsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BitcoinSendSettingsViewModel.swift new file mode 100644 index 0000000000..65fc623e8e --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BitcoinSendSettingsViewModel.swift @@ -0,0 +1,69 @@ +import Combine +import Foundation +import Hodler + +class BitcoinSendSettingsViewModel: ObservableObject { + private var cancellables = Set() + + let handler: BitcoinPreSendHandler + + @Published var sortMode: TransactionDataSortMode { + didSet { + handler.sortMode = sortMode + } + } + + @Published var rbfEnabled: Bool { + didSet { + handler.rbfEnabled = rbfEnabled + } + } + + @Published var lockTimeInterval: HodlerPlugin.LockTimeInterval? { + didSet { + handler.lockTimeInterval = lockTimeInterval + } + } + + @Published var resetEnabled: Bool + + @Published var utxos: String = "" + + init(handler: BitcoinPreSendHandler) { + self.handler = handler + + sortMode = handler.sortMode + rbfEnabled = handler.rbfEnabled + lockTimeInterval = handler.lockTimeInterval + resetEnabled = handler.settingsModified + + handler.settingsModifiedPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] in self?.resetEnabled = $0 } + .store(in: &cancellables) + + handler.balancePublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in self?.syncUtxos() } + .store(in: &cancellables) + + syncUtxos() + } + + private func syncUtxos() { + let totalUtxos = handler.allUtxos.count + let usedUtxos = handler.customUtxos?.count ?? totalUtxos + + utxos = [usedUtxos.description, totalUtxos.description].joined(separator: " / ") + } +} + +extension BitcoinSendSettingsViewModel { + func reset() { + sortMode = handler.defaultSortMode + rbfEnabled = handler.defaultRbfEnabled + lockTimeInterval = nil + handler.customUtxos = nil + resetEnabled = handler.settingsModified + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BitcoinTransactionService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BitcoinTransactionService.swift new file mode 100644 index 0000000000..b4fd13a9f9 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/BitcoinTransactionService.swift @@ -0,0 +1,91 @@ +import Combine +import MarketKit +import SwiftUI + +class BitcoinTransactionService: ITransactionService { + private let blockchainType: BlockchainType + private let feeRateProvider: IFeeRateProvider? + + private(set) var usingRecommended: Bool = true + private(set) var actualFeeRates: FeeRateProvider.FeeRates? + private(set) var cautions: [CautionNew] = [] + private(set) var satoshiPerByte: Int? { + didSet { + validate() + } + } + + private let updateSubject = PassthroughSubject() + + var transactionSettings: TransactionSettings? { + guard let satoshiPerByte else { + return nil + } + + return .bitcoin(satoshiPerByte: satoshiPerByte) + } + + var modified: Bool { + !usingRecommended + } + + var updatePublisher: AnyPublisher { + updateSubject.eraseToAnyPublisher() + } + + init(blockchainType: BlockchainType) { + self.blockchainType = blockchainType + feeRateProvider = App.shared.feeRateProviderFactory.provider(blockchainType: blockchainType) + } + + private func validate() { + guard let actualFeeRates, let satoshiPerByte else { + return + } + + if actualFeeRates.recommended > satoshiPerByte { + if actualFeeRates.minimum <= satoshiPerByte { + cautions = [.init(title: "send.fee_settings.stuck_warning.title".localized, text: "send.fee_settings.stuck_warning".localized, type: .warning)] + } else { + cautions = [.init(title: "send.fee_settings.fee_error.title".localized, text: "send.fee_settings.too_low".localized, type: .error)] + } + } else { + cautions = [] + } + } + + func sync() async throws { + actualFeeRates = try await feeRateProvider?.feeRates() + + if usingRecommended, let actualFeeRates { + satoshiPerByte = actualFeeRates.recommended + } + } + + func settingsView(feeData: Binding, loading: Binding, feeToken: MarketKit.Token, currency: Currency, feeTokenRate: Binding) -> AnyView? { + let view = BitcoinFeeSettingsView( + service: self, + blockchainType: blockchainType, + feeData: feeData, + loading: loading, + feeToken: feeToken, + currency: currency, + feeTokenRate: feeTokenRate + ) + + return AnyView(ThemeNavigationView { view }) + } + + func set(satoshiPerByte: Int) { + self.satoshiPerByte = satoshiPerByte + usingRecommended = (satoshiPerByte == actualFeeRates?.recommended) + + updateSubject.send() + } + + func useRecommended() { + satoshiPerByte = actualFeeRates?.recommended + usingRecommended = true + updateSubject.send() + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/EvmDecoration.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/EvmDecoration.swift new file mode 100644 index 0000000000..aead9eec8e --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/EvmDecoration.swift @@ -0,0 +1,131 @@ +import EvmKit +import Foundation +import MarketKit + +struct EvmDecoration { + let type: Type + let customSendButtonTitle: String? + + var rateCoins: [Coin] { + switch type { + case let .outgoingEip20(_, _, token): return [token.coin] + case let .approveEip20(_, _, token): return [token.coin] + default: return [] + } + } + + func sections(baseToken: Token, currency: Currency, rates: [String: Decimal]) -> [[SendField]] { + switch type { + case let .outgoingEvm(to, value): + return outgoingSections(token: baseToken, to: to, value: value, currency: currency, rates: rates) + case let .outgoingEip20(to, value, token): + return outgoingSections(token: token, to: to, value: value, currency: currency, rates: rates) + case let .approveEip20(spender, value, token): + return approveSections(token: token, spender: spender, value: value, currency: currency, rates: rates) + case let .unknown(to, value, input, method): + return unknownSections(baseToken: baseToken, to: to, value: value, input: input, method: method, currency: currency, rates: rates) + } + } + + private func outgoingSections(token: Token, to: EvmKit.Address, value: Decimal, currency: Currency, rates: [String: Decimal]) -> [[SendField]] { + [ + [ + amountField( + title: "send.confirmation.you_send".localized, + token: token, + value: value, + currency: currency, + rate: rates[token.coin.uid], + type: .neutral + ), + .address( + title: "send.confirmation.to".localized, + value: to.eip55, + blockchainType: token.blockchainType + ), + ], + ] + } + + private func approveSections(token: Token, spender: EvmKit.Address, value: Decimal, currency: Currency, rates: [String: Decimal]) -> [[SendField]] { + let isRevokeAllowance = value == 0 // Check approved new value or revoked last allowance + + let amountField: SendField + + if isRevokeAllowance { + amountField = .amount( + title: "approve.confirmation.you_revoke".localized, + token: token, + coinValueType: .withoutAmount(kind: .token(token: token)), + currencyValue: nil, + type: .neutral + ) + } else { + amountField = self.amountField( + title: "approve.confirmation.you_approve".localized, + token: token, + value: value, + currency: currency, + rate: rates[token.coin.uid], + type: .neutral + ) + } + + return [ + [ + amountField, + .address( + title: "approve.confirmation.spender".localized, + value: spender.eip55, + blockchainType: token.blockchainType + ), + ], + ] + } + + private func unknownSections(baseToken: Token, to: EvmKit.Address, value: Decimal, input: Data, method: String?, currency: Currency, rates: [String: Decimal]) -> [[SendField]] { + var fields: [SendField] = [ + amountField( + title: "send.confirmation.transfer".localized, + token: baseToken, + value: value, + currency: currency, + rate: rates[baseToken.coin.uid], + type: .neutral + ), + .address( + title: "send.confirmation.to".localized, + value: to.eip55, + blockchainType: baseToken.blockchainType + ), + .hex(title: "send.confirmation.input".localized, value: input.toHexString()), + ] + + if let method { + fields.append(.levelValue(title: "send.confirmation.method".localized, value: method, level: .regular)) + } + + return [fields] + } + + private func amountField(title: String, token: Token, value: Decimal, currency: Currency, rate: Decimal?, type: SendField.AmountType) -> SendField { + let coinValue = CoinValue(kind: .token(token: token), value: Decimal(sign: type.sign, exponent: value.exponent, significand: value.significand)) + + return .amount( + title: title, + token: token, + coinValueType: coinValue.isMaxValue ? .infinity(kind: coinValue.kind) : .regular(coinValue: coinValue), + currencyValue: coinValue.isMaxValue ? nil : rate.map { CurrencyValue(currency: currency, value: $0 * value) }, + type: type + ) + } +} + +extension EvmDecoration { + enum `Type` { + case outgoingEvm(to: EvmKit.Address, value: Decimal) + case outgoingEip20(to: EvmKit.Address, value: Decimal, token: Token) + case approveEip20(spender: EvmKit.Address, value: Decimal, token: Token) + case unknown(to: EvmKit.Address, value: Decimal, input: Data, method: String?) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/EvmDecorator.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/EvmDecorator.swift new file mode 100644 index 0000000000..d8edc557c9 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/EvmDecorator.swift @@ -0,0 +1,55 @@ +import Eip20Kit +import EvmKit +import MarketKit + +struct EvmDecorator { + private let coinManager = App.shared.coinManager + private let evmLabelManager = App.shared.evmLabelManager + + func decorate(baseToken: Token, transactionData: TransactionData, transactionDecoration: TransactionDecoration?) -> EvmDecoration { + var type: EvmDecoration.`Type`? + var customSendButtonTitle: String? + + switch transactionDecoration { + case let decoration as OutgoingDecoration: + type = .outgoingEvm( + to: decoration.to, + value: baseToken.decimalValue(value: decoration.value) + ) + + case let decoration as OutgoingEip20Decoration: + if let token = try? coinManager.token(query: .init(blockchainType: baseToken.blockchainType, tokenType: .eip20(address: decoration.contractAddress.hex))) { + type = .outgoingEip20( + to: decoration.to, + value: token.decimalValue(value: decoration.value), + token: token + ) + } + + case let decoration as ApproveEip20Decoration: + if let token = try? coinManager.token(query: .init(blockchainType: baseToken.blockchainType, tokenType: .eip20(address: decoration.contractAddress.hex))) { + type = .approveEip20( + spender: decoration.spender, + value: token.decimalValue(value: decoration.value), + token: token + ) + + let isRevoke = decoration.value == 0 + + customSendButtonTitle = isRevoke ? "send.confirmation.slide_to_revoke".localized : "send.confirmation.slide_to_approve".localized + } + default: + () + } + + return EvmDecoration( + type: type ?? .unknown( + to: transactionData.to, + value: baseToken.decimalValue(value: transactionData.value), + input: transactionData.input, + method: evmLabelManager.methodLabel(input: transactionData.input) + ), + customSendButtonTitle: customSendButtonTitle + ) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/EvmFeeData.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/EvmFeeData.swift similarity index 94% rename from UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/EvmFeeData.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/SendNew/EvmFeeData.swift index 7b84e0f47a..c4578733df 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/EvmFeeData.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/EvmFeeData.swift @@ -14,6 +14,10 @@ struct EvmFeeData { self.l1Fee = l1Fee } + func totalFee(gasPrice: GasPrice) -> BigUInt { + BigUInt(surchargedGasLimit * gasPrice.max) + (l1Fee ?? 0) + } + func totalAmountData(gasPrice: GasPrice?, feeToken: Token, currency: Currency, feeTokenRate: Decimal?) -> AmountData? { guard let gasPrice else { return nil diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/EvmFeeEstimator.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/EvmFeeEstimator.swift similarity index 66% rename from UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/EvmFeeEstimator.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/SendNew/EvmFeeEstimator.swift index e71563dd8b..264027410a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/EvmFeeEstimator.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/EvmFeeEstimator.swift @@ -5,7 +5,7 @@ import MarketKit struct EvmFeeEstimator { private static let surchargePercent: Double = 10 - func estimateFee(evmKitWrapper: EvmKitWrapper, transactionData: TransactionData, gasPrice: GasPrice, predefinedGasLimit: Int? = nil) async throws -> EvmFeeData { + private func _estimateFee(evmKitWrapper: EvmKitWrapper, transactionData: TransactionData, gasPrice: GasPrice, predefinedGasLimit: Int? = nil) async throws -> EvmFeeData { let evmKit = evmKitWrapper.evmKit let gasLimit: Int @@ -32,7 +32,7 @@ struct EvmFeeEstimator { let surchargedGasLimit: Int - if evmBalance > totalAmount { + if !transactionData.input.isEmpty, evmBalance > totalAmount { let remainingBalance = evmBalance - totalAmount var additionalGasLimit = Int(Double(gasLimit) / 100.0 * Self.surchargePercent) @@ -48,4 +48,16 @@ struct EvmFeeEstimator { return .init(gasLimit: gasLimit, surchargedGasLimit: surchargedGasLimit, l1Fee: l1Fee) } + + func estimateFee(evmKitWrapper: EvmKitWrapper, transactionData: TransactionData, gasPriceData: GasPriceData, predefinedGasLimit _: Int? = nil) async throws -> EvmFeeData { + do { + return try await _estimateFee(evmKitWrapper: evmKitWrapper, transactionData: transactionData, gasPrice: gasPriceData.userDefined) + } catch { + if case let AppError.ethereum(reason: ethereumError) = error.convertedError, case .lowerThanBaseGasLimit = ethereumError { + return try await _estimateFee(evmKitWrapper: evmKitWrapper, transactionData: transactionData, gasPrice: gasPriceData.recommended) + } else { + throw error + } + } + } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/EvmPreSendHandler.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/EvmPreSendHandler.swift new file mode 100644 index 0000000000..99f191d942 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/EvmPreSendHandler.swift @@ -0,0 +1,67 @@ +import BigInt +import Combine +import EvmKit +import Foundation +import MarketKit +import RxSwift + +class EvmPreSendHandler { + private let token: Token + private let adapter: ISendEthereumAdapter & IBalanceAdapter + + private let stateSubject = PassthroughSubject() + private let balanceSubject = PassthroughSubject() + + private let disposeBag = DisposeBag() + + init(token: Token, adapter: ISendEthereumAdapter & IBalanceAdapter) { + self.token = token + self.adapter = adapter + + adapter.balanceStateUpdatedObservable + .observeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) + .subscribe { [weak self] state in + self?.stateSubject.send(state) + } + .disposed(by: disposeBag) + + adapter.balanceDataUpdatedObservable + .observeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) + .subscribe { [weak self] balanceData in + self?.balanceSubject.send(balanceData.available) + } + .disposed(by: disposeBag) + } +} + +extension EvmPreSendHandler: IPreSendHandler { + var state: AdapterState { + adapter.balanceState + } + + var statePublisher: AnyPublisher { + stateSubject.eraseToAnyPublisher() + } + + var balance: Decimal { + adapter.balanceData.available + } + + var balancePublisher: AnyPublisher { + balanceSubject.eraseToAnyPublisher() + } + + func sendData(amount: Decimal, address: String, memo _: String?) -> SendDataResult { + guard let evmAmount = BigUInt(amount.hs.roundedString(decimal: token.decimals)) else { + return .invalid(cautions: []) + } + + guard let evmAddress = try? EvmKit.Address(hex: address) else { + return .invalid(cautions: []) + } + + let transactionData = adapter.transactionData(amount: evmAmount, address: evmAddress) + + return .valid(sendData: .evm(blockchainType: token.blockchainType, transactionData: transactionData)) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/EvmSendData.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/EvmSendData.swift new file mode 100644 index 0000000000..8314061af7 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/EvmSendData.swift @@ -0,0 +1,59 @@ +import EvmKit +import Foundation +import MarketKit + +class EvmSendData: BaseSendEvmData, ISendData { + let decoration: EvmDecoration + let transactionData: TransactionData? + let transactionError: Error? + + init(decoration: EvmDecoration, transactionData: TransactionData?, transactionError: Error?, gasPrice: GasPrice?, evmFeeData: EvmFeeData?, nonce: Int?) { + self.decoration = decoration + self.transactionData = transactionData + self.transactionError = transactionError + + super.init(gasPrice: gasPrice, evmFeeData: evmFeeData, nonce: nonce) + } + + var feeData: FeeData? { + evmFeeData.map { .evm(evmFeeData: $0) } + } + + var canSend: Bool { + evmFeeData != nil && transactionError == nil + } + + var rateCoins: [Coin] { + decoration.rateCoins + } + + var customSendButtonTitle: String? { + decoration.customSendButtonTitle + } + + func cautions(baseToken: Token) -> [CautionNew] { + var cautions = [CautionNew]() + + if let transactionError { + cautions.append(caution(transactionError: transactionError, feeToken: baseToken)) + } + + return cautions + } + + func sections(baseToken: Token, currency: Currency, rates: [String: Decimal]) -> [[SendField]] { + var sections = decoration.sections(baseToken: baseToken, currency: currency, rates: rates) + + if let nonce { + sections.append( + [ + .levelValue(title: "send.confirmation.nonce".localized, value: String(nonce), level: .regular), + ] + ) + } + + sections.append(feeFields(feeToken: baseToken, currency: currency, feeTokenRate: rates[baseToken.coin.uid])) + + return sections + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/EvmSendHandler.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/EvmSendHandler.swift new file mode 100644 index 0000000000..a129b15cda --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/EvmSendHandler.swift @@ -0,0 +1,113 @@ +import EvmKit +import Foundation +import MarketKit + +class EvmSendHandler { + let baseToken: Token + private let transactionData: TransactionData + private let evmKitWrapper: EvmKitWrapper + private let decorator = EvmDecorator() + private let evmFeeEstimator = EvmFeeEstimator() + + init(baseToken: Token, transactionData: TransactionData, evmKitWrapper: EvmKitWrapper) { + self.baseToken = baseToken + self.transactionData = transactionData + self.evmKitWrapper = evmKitWrapper + } +} + +extension EvmSendHandler: ISendHandler { + var expirationDuration: Int? { + 10 + } + + func sendData(transactionSettings: TransactionSettings?) async throws -> ISendData { + let gasPriceData = transactionSettings?.gasPriceData + var evmFeeData: EvmFeeData? + var transactionError: Error? + var transactionData = transactionData + + if let gasPriceData { + let evmBalance = evmKitWrapper.evmKit.accountState?.balance ?? 0 + + do { + if transactionData.input.isEmpty, transactionData.value == evmBalance { + let stubTransactionData = TransactionData(to: transactionData.to, value: 1, input: transactionData.input) + let stubFeeData = try await evmFeeEstimator.estimateFee(evmKitWrapper: evmKitWrapper, transactionData: stubTransactionData, gasPriceData: gasPriceData) + let totalFee = stubFeeData.totalFee(gasPrice: gasPriceData.userDefined) + + evmFeeData = stubFeeData + transactionData = TransactionData(to: transactionData.to, value: max(0, transactionData.value - totalFee), input: transactionData.input) + } else { + evmFeeData = try await evmFeeEstimator.estimateFee(evmKitWrapper: evmKitWrapper, transactionData: transactionData, gasPriceData: gasPriceData) + } + } catch { + transactionError = error + } + } + + let transactionDecoration = evmKitWrapper.evmKit.decorate(transactionData: transactionData) + let decoration = decorator.decorate(baseToken: baseToken, transactionData: transactionData, transactionDecoration: transactionDecoration) + + return EvmSendData( + decoration: decoration, + transactionData: transactionData, + transactionError: transactionError, + gasPrice: gasPriceData?.userDefined, + evmFeeData: evmFeeData, + nonce: transactionSettings?.nonce + ) + } + + func send(data: ISendData) async throws { + guard let data = data as? EvmSendData else { + throw SendError.invalidData + } + + guard let transactionData = data.transactionData else { + throw SendError.noTransactionData + } + + guard let gasPrice = data.gasPrice else { + throw SendError.noGasPrice + } + + guard let gasLimit = data.evmFeeData?.surchargedGasLimit else { + throw SendError.noGasLimit + } + + _ = try await evmKitWrapper.send( + transactionData: transactionData, + gasPrice: gasPrice, + gasLimit: gasLimit, + nonce: data.nonce + ) + } +} + +extension EvmSendHandler { + enum SendError: Error { + case invalidData + case noGasPrice + case noGasLimit + case noTransactionData + } +} + +extension EvmSendHandler { + static func instance(blockchainType: BlockchainType, transactionData: TransactionData) -> EvmSendHandler? { + guard let baseToken = try? App.shared.coinManager.token(query: .init(blockchainType: blockchainType, tokenType: .native)) else { + return nil + } + + guard let evmKitWrapper = App.shared.evmBlockchainManager.evmKitManager(blockchainType: blockchainType).evmKitWrapper else { + return nil + } + + return EvmSendHandler( + baseToken: baseToken, + transactionData: transactionData, + evmKitWrapper: evmKitWrapper + ) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/EvmTransactionService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/EvmTransactionService.swift similarity index 89% rename from UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/EvmTransactionService.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/SendNew/EvmTransactionService.swift index a5659d47cc..4b93009b8b 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/EvmTransactionService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/EvmTransactionService.swift @@ -62,14 +62,17 @@ class EvmTransactionService: ITransactionService { } var transactionSettings: TransactionSettings? { - guard let gasPrice else { + guard let gasPrice, let recommendedGasPrice else { return nil } - return .evm(gasPrice: gasPrice, nonce: usingRecommendedNonce ? nil : nonce) + return .evm( + gasPriceData: GasPriceData(recommended: recommendedGasPrice, userDefined: gasPrice), + nonce: usingRecommendedNonce ? nil : nonce + ) } - init?(blockchainType: BlockchainType, userAddress: EvmKit.Address) { + init?(blockchainType: BlockchainType, userAddress: EvmKit.Address, initialTransactionSettings: InitialTransactionSettings?) { guard let rpcSource = App.shared.evmSyncSourceManager.httpSyncSource(blockchainType: blockchainType)?.rpcSource else { return nil } @@ -78,6 +81,18 @@ class EvmTransactionService: ITransactionService { self.blockchainType = blockchainType self.userAddress = userAddress self.rpcSource = rpcSource + + if case let .evm(gasPrice, nonce) = initialTransactionSettings { + if let gasPrice { + usingRecommendedGasPrice = false + self.gasPrice = gasPrice + } + + if let nonce { + usingRecommendedNonce = false + self.nonce = nonce + } + } } private func validate() { @@ -129,9 +144,8 @@ class EvmTransactionService: ITransactionService { gasPrice = recommendedGasPrice } - nextNonce = try await EvmKit.Kit.nonceSingle(networkManager: networkManager, rpcSource: rpcSource, userAddress: userAddress, defaultBlockParameter: .latest) - minimumNonce = try await EvmKit.Kit.nonceSingle(networkManager: networkManager, rpcSource: rpcSource, userAddress: userAddress, defaultBlockParameter: .pending) - + minimumNonce = try await EvmKit.Kit.nonceSingle(networkManager: networkManager, rpcSource: rpcSource, userAddress: userAddress, defaultBlockParameter: .latest) + nextNonce = try await EvmKit.Kit.nonceSingle(networkManager: networkManager, rpcSource: rpcSource, userAddress: userAddress, defaultBlockParameter: .pending) if usingRecommendedNonce { nonce = nextNonce } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/FeeData.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeData.swift similarity index 80% rename from UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/FeeData.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeData.swift index 94e9f79f17..35c4e26d0d 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/FeeData.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeData.swift @@ -1,6 +1,6 @@ enum FeeData { case evm(evmFeeData: EvmFeeData) - case bitcoin(bytes: Int) + case bitcoin(bitcoinFeeData: BitcoinFeeData) var gasLimit: Int? { switch self { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeSettings/BitcoinFeeSettingsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeSettings/BitcoinFeeSettingsView.swift new file mode 100644 index 0000000000..5aa643d360 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeSettings/BitcoinFeeSettingsView.swift @@ -0,0 +1,96 @@ +import MarketKit +import SwiftUI + +struct BitcoinFeeSettingsView: View { + @StateObject private var viewModel: BitcoinFeeSettingsViewModel + @Binding private var feeData: FeeData? + @Binding private var loading: Bool + private var feeToken: Token + private var currency: Currency + @Binding private var feeTokenRate: Decimal? + @State private var feeRateInfoPresented: Bool = false + + private var helper = FeeSettingsViewHelper() + @Environment(\.presentationMode) private var presentationMode + + init(service: BitcoinTransactionService, blockchainType _: BlockchainType, feeData: Binding, loading: Binding, feeToken: Token, currency: Currency, feeTokenRate: Binding) { + _viewModel = .init(wrappedValue: BitcoinFeeSettingsViewModel(service: service)) + _feeData = feeData + _loading = loading + self.feeToken = feeToken + self.currency = currency + _feeTokenRate = feeTokenRate + } + + var body: some View { + ScrollableThemeView { + VStack(spacing: .margin24) { + ListSection { + helper.row( + title: "fee_settings.network_fee".localized, + feeValue: helper.feeAmount(feeToken: feeToken, currency: currency, feeTokenRate: feeTokenRate, loading: loading, feeData: feeData), + description: .init(title: "fee_settings.network_fee".localized, description: "fee_settings.network_fee.info".localized) + ) + } + + VStack(spacing: 0) { + Button(action: { + feeRateInfoPresented = true + }, label: { + HStack(spacing: .margin8) { + HStack(spacing: .margin8) { + Text("fee_settings.fee_rate".localized + " (Sat/Byte)".localized).textSubhead1() + .frame(maxWidth: .infinity, alignment: .leading) + Image("circle_information_20").themeIcon() + } + .padding(EdgeInsets(top: 5.5, leading: .margin16, bottom: 5.5, trailing: .margin16)) + } + }) + + helper.inputNumberWithSteps( + placeholder: "", + text: $viewModel.satoshiPerByte, + cautionState: $viewModel.satoshiPerByteCautionState, + onTap: viewModel.stepChangesatoshiPerByte + ) + } + + let cautions = viewModel.service.cautions + if !cautions.isEmpty { + VStack(spacing: .margin12) { + ForEach(cautions.indices, id: \.self) { index in + HighlightedTextView(caution: cautions[index]) + } + } + } + } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) + } + .animation(.default, value: viewModel.satoshiPerByte) + .navigationTitle("fee_settings".localized) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("button.reset".localized) { + viewModel.onReset() + }.disabled(!viewModel.resetEnabled) + } + ToolbarItem(placement: .confirmationAction) { + Button("button.done".localized) { + presentationMode.wrappedValue.dismiss() + } + } + } + .sheet(isPresented: $feeRateInfoPresented) { + ThemeNavigationView { + InfoNewView( + viewItems: [ + .header1(text: "send.fee_info.title".localized), + .text(text: "send.fee_info.description".localized), + ], + isPresented: $feeRateInfoPresented + ) + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeSettings/BitcoinFeeSettingsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeSettings/BitcoinFeeSettingsViewModel.swift new file mode 100644 index 0000000000..0b4e604586 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeSettings/BitcoinFeeSettingsViewModel.swift @@ -0,0 +1,80 @@ +import EvmKit +import Foundation +import MarketKit + +class BitcoinFeeSettingsViewModel: ObservableObject { + let service: BitcoinTransactionService + + init(service: BitcoinTransactionService) { + self.service = service + + if let satoshiPerByte = service.actualFeeRates?.recommended { + self.satoshiPerByte = satoshiPerByte.description + } + + syncFromService() + } + + @Published var satoshiPerByte: String = "" { + didSet { + DispatchQueue.main.async { [weak self] in + self?.handle() + } + } + } + + @Published var satoshiPerByteCautionState: FieldCautionState = .none + @Published var resetEnabled = false + + private func syncFromService() { + if let satoshiPerByte = service.satoshiPerByte { + self.satoshiPerByte = satoshiPerByte.description + } + + sync() + } + + private func sync() { + resetEnabled = service.modified + + if let caution = service.cautions.first { + satoshiPerByteCautionState = .caution(caution.type) + } else { + satoshiPerByteCautionState = .none + } + } + + private func handle() { + guard let satoshiPerByteInt = Int(satoshiPerByte) else { + satoshiPerByteCautionState = .caution(.error) + return + } + + service.set(satoshiPerByte: satoshiPerByteInt) + sync() + } + + private func updateByStep(value: String?, direction: StepChangeButtonsViewDirection) -> Int? { + guard let int = value.flatMap({ Int($0) }) else { + return nil + } + + switch direction { + case .down: return max(int - 1, 0) + case .up: return int + 1 + } + } +} + +extension BitcoinFeeSettingsViewModel { + func stepChangesatoshiPerByte(_ direction: StepChangeButtonsViewDirection) { + if let newValue = updateByStep(value: satoshiPerByte, direction: direction) { + satoshiPerByte = newValue.description + } + } + + func onReset() { + service.useRecommended() + syncFromService() + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/FeeSettings/Eip1559FeeSettingsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeSettings/Eip1559FeeSettingsView.swift similarity index 100% rename from UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/FeeSettings/Eip1559FeeSettingsView.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeSettings/Eip1559FeeSettingsView.swift diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/FeeSettings/Eip1559FeeSettingsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeSettings/Eip1559FeeSettingsViewModel.swift similarity index 85% rename from UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/FeeSettings/Eip1559FeeSettingsViewModel.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeSettings/Eip1559FeeSettingsViewModel.swift index 758d5e486c..395b01bd4c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/FeeSettings/Eip1559FeeSettingsViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeSettings/Eip1559FeeSettingsViewModel.swift @@ -16,25 +16,50 @@ class Eip1559FeeSettingsViewModel: ObservableObject { baseFee = feeViewItemFactory.description(value: recommendedMaxFeePerGas, step: baseStep) } - sync() + syncFromService() } @Published var baseFee: String = "" @Published var maxFeeCautionState: FieldCautionState = .none - @Published var maxFee: String = "" + @Published var maxFee: String = "" { + didSet { + DispatchQueue.main.async { [weak self] in + self?.handleGasPrice() + } + } + } + @Published var maxTipsCautionState: FieldCautionState = .none - @Published var maxTips: String = "" + @Published var maxTips: String = "" { + didSet { + DispatchQueue.main.async { [weak self] in + self?.handleGasPrice() + } + } + } + @Published var nonceCautionState: FieldCautionState = .none - @Published var nonce: String = "" + @Published var nonce: String = "" { + didSet { + DispatchQueue.main.async { [weak self] in + self?.handleNonce() + } + } + } + @Published var resetEnabled = false - private func sync() { + private func syncFromService() { if case let .eip1559(maxFee, maxTips) = service.gasPrice { self.maxFee = feeViewItemFactory.decimalValue(value: maxFee).description self.maxTips = feeViewItemFactory.decimalValue(value: maxTips).description } nonce = "\(service.nonce ?? 0)" + sync() + } + + private func sync() { resetEnabled = service.modified if service.warnings.contains(where: { $0 is EvmFeeModule.GasDataWarning }) { @@ -65,7 +90,6 @@ class Eip1559FeeSettingsViewModel: ObservableObject { maxFeePerGas: feeViewItemFactory.intValue(value: maxFeeDecimal), maxPriorityFeePerGas: feeViewItemFactory.intValue(value: maxTipsDecimal) )) - sync() } @@ -95,26 +119,23 @@ extension Eip1559FeeSettingsViewModel { func stepChangeMaxFee(_ direction: StepChangeButtonsViewDirection) { if let newValue = updateByStep(value: maxFee, direction: direction) { maxFee = newValue.description - handleGasPrice() } } func stepChangeMaxTips(_ direction: StepChangeButtonsViewDirection) { if let newValue = updateByStep(value: maxTips, direction: direction) { maxTips = newValue.description - handleGasPrice() } } func stepChangeNonce(_ direction: StepChangeButtonsViewDirection) { if let newValue = updateByStep(value: nonce, direction: direction) { nonce = newValue.description - handleNonce() } } func onReset() { service.useRecommended() - sync() + syncFromService() } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/FeeSettings/FeeSettings.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeSettings/FeeSettings.swift similarity index 100% rename from UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/FeeSettings/FeeSettings.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeSettings/FeeSettings.swift diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/FeeSettings/FeeSettingsViewHelper.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeSettings/FeeSettingsViewHelper.swift similarity index 82% rename from UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/FeeSettings/FeeSettingsViewHelper.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeSettings/FeeSettingsViewHelper.swift index 63464199b9..591e9581c3 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/FeeSettings/FeeSettingsViewHelper.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeSettings/FeeSettingsViewHelper.swift @@ -32,6 +32,23 @@ class FeeSettingsViewHelper { return (l2Value, l1Value, .value(primary: evmFeeData.gasLimit.description, secondary: nil)) } + func feeAmount(feeToken: Token, currency: Currency, feeTokenRate: Decimal?, loading: Bool, feeData: FeeData?) -> FeeSettings.FeeValue { + guard !loading else { + return .spinner + } + + guard case let .bitcoin(sendInfo) = feeData, + let amountData = sendInfo.amountData(feeToken: feeToken, currency: currency, feeTokenRate: feeTokenRate) + else { + return FeeSettings.FeeValue.none + } + + return .value( + primary: ValueFormatter.instance.formatShort(coinValue: amountData.coinValue) ?? "", + secondary: amountData.currencyValue.flatMap { ValueFormatter.instance.formatShort(currencyValue: $0) } ?? "n/a".localized + ) + } + @ViewBuilder func row(title: String, feeValue: FeeSettings.FeeValue, description: ActionSheetView.InfoDescription) -> some View { HStack(spacing: .margin8) { Text(title) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/FeeSettings/LegacyFeeSettingsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeSettings/LegacyFeeSettingsView.swift similarity index 100% rename from UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/FeeSettings/LegacyFeeSettingsView.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeSettings/LegacyFeeSettingsView.swift diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/FeeSettings/LegacyFeeSettingsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeSettings/LegacyFeeSettingsViewModel.swift similarity index 86% rename from UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/FeeSettings/LegacyFeeSettingsViewModel.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeSettings/LegacyFeeSettingsViewModel.swift index 25bb747785..5910e84bb4 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/FeeSettings/LegacyFeeSettingsViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/FeeSettings/LegacyFeeSettingsViewModel.swift @@ -11,21 +11,38 @@ class LegacyFeeSettingsViewModel: ObservableObject { self.service = service self.feeViewItemFactory = feeViewItemFactory - sync() + syncFromService() } @Published var gasPriceCautionState: FieldCautionState = .none - @Published var gasPrice: String = "" + @Published var gasPrice: String = "" { + didSet { + DispatchQueue.main.async { [weak self] in + self?.handleGasPrice() + } + } + } + @Published var nonceCautionState: FieldCautionState = .none - @Published var nonce: String = "" + @Published var nonce: String = "" { + didSet { + DispatchQueue.main.async { [weak self] in + self?.handleNonce() + } + } + } + @Published var resetEnabled = false - private func sync() { + private func syncFromService() { if case let .legacy(gasPrice) = service.gasPrice { self.gasPrice = feeViewItemFactory.decimalValue(value: gasPrice).description } - nonce = "\(service.nonce ?? 0)" + sync() + } + + private func sync() { resetEnabled = service.modified if service.warnings.contains(where: { $0 is EvmFeeModule.GasDataWarning }) { @@ -77,19 +94,17 @@ extension LegacyFeeSettingsViewModel { func stepChangeGasPrice(_ direction: StepChangeButtonsViewDirection) { if let newValue = updateByStep(value: gasPrice, direction: direction) { gasPrice = newValue.description - handleGasPrice() } } func stepChangeNonce(_ direction: StepChangeButtonsViewDirection) { if let newValue = updateByStep(value: nonce, direction: direction) { nonce = newValue.description - handleNonce() } } func onReset() { service.useRecommended() - sync() + syncFromService() } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/GasPriceData.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/GasPriceData.swift new file mode 100644 index 0000000000..86552f3e86 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/GasPriceData.swift @@ -0,0 +1,6 @@ +import EvmKit + +struct GasPriceData { + let recommended: GasPrice + let userDefined: GasPrice +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/IPreSendHandler.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/IPreSendHandler.swift new file mode 100644 index 0000000000..d93f0e66ce --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/IPreSendHandler.swift @@ -0,0 +1,43 @@ +import Combine +import Foundation +import SwiftUI + +protocol IPreSendHandler { + var hasSettings: Bool { get } + var state: AdapterState { get } + var statePublisher: AnyPublisher { get } + var balance: Decimal { get } + var balancePublisher: AnyPublisher { get } + var settingsModified: Bool { get } + var settingsModifiedPublisher: AnyPublisher { get } + func hasMemo(address: String?) -> Bool + func settingsView(onChangeSettings: @escaping () -> Void) -> AnyView + func sendData(amount: Decimal, address: String, memo: String?) -> SendDataResult +} + +extension IPreSendHandler { + var hasSettings: Bool { + false + } + + func hasMemo(address _: String?) -> Bool { + false + } + + func settingsView(onChangeSettings _: @escaping () -> Void) -> AnyView { + AnyView(EmptyView()) + } + + var settingsModified: Bool { + false + } + + var settingsModifiedPublisher: AnyPublisher { + Empty().eraseToAnyPublisher() + } +} + +enum SendDataResult { + case valid(sendData: SendData) + case invalid(cautions: [CautionNew]) +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/ISendData.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/ISendData.swift new file mode 100644 index 0000000000..a802b08c36 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/ISendData.swift @@ -0,0 +1,17 @@ +import Foundation +import MarketKit + +protocol ISendData { + var feeData: FeeData? { get } + var canSend: Bool { get } + var rateCoins: [Coin] { get } + var customSendButtonTitle: String? { get } + func cautions(baseToken: Token) -> [CautionNew] + func sections(baseToken: Token, currency: Currency, rates: [String: Decimal]) -> [[SendField]] +} + +extension ISendData { + var customSendButtonTitle: String? { + nil + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/ISendHandler.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/ISendHandler.swift new file mode 100644 index 0000000000..261fd968b7 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/ISendHandler.swift @@ -0,0 +1,24 @@ +import MarketKit + +protocol ISendHandler { + var baseToken: Token { get } + var syncingText: String? { get } + var expirationDuration: Int? { get } + var initialTransactionSettings: InitialTransactionSettings? { get } + func sendData(transactionSettings: TransactionSettings?) async throws -> ISendData + func send(data: ISendData) async throws +} + +extension ISendHandler { + var syncingText: String? { + nil + } + + var expirationDuration: Int? { + nil + } + + var initialTransactionSettings: InitialTransactionSettings? { + nil + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/ITransactionService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/ITransactionService.swift similarity index 100% rename from UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/ITransactionService.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/SendNew/ITransactionService.swift diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/InitialTransactionSettings.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/InitialTransactionSettings.swift new file mode 100644 index 0000000000..0cc7129608 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/InitialTransactionSettings.swift @@ -0,0 +1,5 @@ +import EvmKit + +enum InitialTransactionSettings { + case evm(gasPrice: GasPrice?, nonce: Int?) +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/OutputSelectView2.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/OutputSelectView2.swift new file mode 100644 index 0000000000..f3084329b4 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/OutputSelectView2.swift @@ -0,0 +1,91 @@ +import SwiftUI + +struct OutputSelectorView2: View { + @StateObject var viewModel: OutputSelectorViewModel2 + + @Environment(\.presentationMode) private var presentationMode + + init(handler: BitcoinPreSendHandler) { + _viewModel = StateObject(wrappedValue: OutputSelectorViewModel2(handler: handler)) + } + + var body: some View { + ThemeView { + VStack { + ListSection { + ListRow(minHeight: .heightDoubleLineCell) { + HStack(spacing: .margin8) { + VStack(spacing: 1) { + Text("send.available_balance".localized).themeSubhead2(color: .themeGray) + } + + Spacer() + + VStack(spacing: 1) { + Text(viewModel.availableBalanceCoinValue).themeSubhead1(color: .themeLeah, alignment: .trailing) + if let subtitle = viewModel.availableBalanceFiatValue { + Text(subtitle).themeSubhead2(alignment: .trailing) + } + } + } + } + } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin12, trailing: .margin16)) + + ScrollView { + ListSection { + ForEach(viewModel.outputsViewItems) { viewItem in + output(viewItem: viewItem) + } + } + .themeListStyle(.transparent) + } + + HorizontalDivider(color: .themeSteel10, height: .heightOneDp) + + HStack { + Button(action: { + viewModel.unselectAll() + }) { + Text("send.unselect_all".localized).themeBody(color: viewModel.selectedSet.isEmpty ? .themeGray50 : .themeJacob) + } + + Spacer() + + Button(action: { + viewModel.selectAll() + }) { + Text("send.select_all".localized).themeBody(color: viewModel.allSelected ? .themeGray50 : .themeJacob, alignment: .trailing) + } + } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: CGFloat(43), trailing: .margin16)) + } + } + .navigationTitle("send.unspent_outputs".localized) + .navigationBarTitleDisplayMode(.inline) + .interactiveDismissDisabled(viewModel.resetEnabled) + } + + @ViewBuilder func output(viewItem: OutputSelectorViewModel2.OutputViewItem) -> some View { + ClickableRow(action: { + viewModel.toggle(viewItem: viewItem) + }) { + HStack(spacing: .margin16) { + CheckBoxUiView(checked: .init(get: { viewModel.selectedSet.contains(viewItem.id) }, set: { _ in })) + + VStack(spacing: 1) { + Text(viewItem.date).themeBody() + Text(viewItem.address).themeSubhead2() + } + Spacer() + + VStack(spacing: 1) { + Text(viewItem.coinValue).themeBody(alignment: .trailing) + if let subtitle = viewItem.fiatValue { + Text(subtitle).themeSubhead2(alignment: .trailing) + } + } + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/OutputSelectorViewModel2.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/OutputSelectorViewModel2.swift new file mode 100644 index 0000000000..126f38509e --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/OutputSelectorViewModel2.swift @@ -0,0 +1,148 @@ +import BitcoinCore +import Combine +import Foundation +import MarketKit + +class OutputSelectorViewModel2: ObservableObject { + private var rateCancellable: AnyCancellable? + + @Published var outputsViewItems = [OutputViewItem]() + @Published var selectedSet = Set() + + @Published var availableBalanceCoinValue = "" + @Published var availableBalanceFiatValue: String? = "" + @Published var resetEnabled = true + @Published var allSelected: Bool = true + + private let handler: BitcoinPreSendHandler + private var rate: Decimal? + + init(handler: BitcoinPreSendHandler) { + self.handler = handler + + let currency = App.shared.currencyManager.baseCurrency + rate = App.shared.marketKit.coinPrice(coinUid: handler.token.coin.uid, currencyCode: currency.code)?.value + rateCancellable = App.shared.marketKit.coinPricePublisher(coinUid: handler.token.coin.uid, currencyCode: currency.code) + .receive(on: DispatchQueue.main) + .sink { [weak self] price in + self?.rate = price.value + self?.sync() + } + + sync() + } + + private func sync() { + // create outputs viewItems + let all = handler.allUtxos.sorted { output, output2 in + if output.timestamp > output2.timestamp { return true } + if output.timestamp == output2.timestamp, output.outputIndex < output2.outputIndex { return true } + return false + } + + let selectedIds = (handler.customUtxos ?? all).map { + OutputViewItem.id(hash: $0.transactionHash, index: $0.outputIndex) + } + selectedSet = Set(selectedIds) + + outputsViewItems = all.map { viewItem(unspentOutput: $0) } + allSelected = all.count == selectedSet.count + + let coinValue = coinValue(satoshiValue: handler.availableBalance) + let currencyValue = rate.flatMap { + CurrencyValue(currency: App.shared.currencyManager.baseCurrency, value: coinValue.value * $0) + } + + availableBalanceCoinValue = coinValue.formattedFull ?? "n/a".localized + availableBalanceFiatValue = currencyValue.flatMap(\.formattedFull) + } + + private func viewItem(unspentOutput: UnspentOutputInfo) -> OutputViewItem { + let coinValue = coinValue(satoshiValue: unspentOutput.value) + let currencyValue = rate.flatMap { + CurrencyValue(currency: App.shared.currencyManager.baseCurrency, value: coinValue.value * $0) + } + + return OutputViewItem( + outputIndex: unspentOutput.outputIndex, + transactionHash: unspentOutput.transactionHash, + date: DateHelper.instance.formatShortDateOnly(date: Date(timeIntervalSince1970: TimeInterval(unspentOutput.timestamp))), + address: unspentOutput.address?.shortened ?? "n/a".localized, + coinValue: coinValue.formattedFull ?? "n/a".localized, + fiatValue: currencyValue.flatMap(\.formattedFull) + ) + } + + private func coinValue(satoshiValue: Int) -> CoinValue { + let coinRate = pow(10, handler.token.decimals) + let decimalValue = Decimal(satoshiValue) / coinRate + return CoinValue(kind: .token(token: handler.token), value: decimalValue) + } +} + +extension OutputSelectorViewModel2 { + func toggle(viewItem: OutputViewItem) { + handler.customUtxos = handler.allUtxos.filter { + let id = OutputViewItem.id(hash: $0.transactionHash, index: $0.outputIndex) + + if viewItem.id == id { + return !selectedSet.contains(id) + } else { + return selectedSet.contains(id) + } + } + + sync() + } + + func unselectAll() { + handler.customUtxos = [] + sync() + } + + func selectAll() { + handler.customUtxos = handler.allUtxos + sync() + } + + func onTapDone() {} + + func reset() { + handler.customUtxos = nil + resetEnabled = false + } +} + +extension OutputSelectorViewModel2 { + struct ChangeViewItem: Equatable { + let address: String + let title: String + let subtitle: String? + + static func == (lhs: ChangeViewItem, rhs: ChangeViewItem) -> Bool { + lhs.address == rhs.address && + lhs.title == rhs.title && + lhs.subtitle == rhs.subtitle + } + } + + struct OutputViewItem: Hashable, Identifiable, Equatable { + let outputIndex: Int + let transactionHash: Data + let date: String + let address: String + let coinValue: String + let fiatValue: String? + + var id: String { + Self.id(hash: transactionHash, index: outputIndex) + } + + func hash(into hasher: inout Hasher) { + hasher.combine(outputIndex) + hasher.combine(transactionHash) + } + + static func id(hash: Data, index: Int) -> String { [hash.hs.hexString, index.description].joined(separator: "_") } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/PreSendView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/PreSendView.swift new file mode 100644 index 0000000000..5a02cf3eaf --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/PreSendView.swift @@ -0,0 +1,289 @@ +import ComponentKit +import Kingfisher +import SwiftUI +import ThemeKit + +struct PreSendView: View { + @StateObject var viewModel: PreSendViewModel + private let showIcon: Bool + private let onDismiss: (() -> Void)? + + @Environment(\.presentationMode) private var presentationMode + @FocusState private var focusField: FocusField? + + @State private var settingsPresented = false + @State private var confirmPresented = false + + init(wallet: Wallet, mode: PreSendViewModel.Mode = .regular, showIcon: Bool = false, onDismiss: (() -> Void)? = nil) { + _viewModel = StateObject(wrappedValue: PreSendViewModel(wallet: wallet, mode: mode)) + self.showIcon = showIcon + self.onDismiss = onDismiss + } + + var body: some View { + ThemeView { + ScrollView { + VStack(spacing: .margin16) { + VStack(spacing: .margin8) { + inputView() + availableBalanceView(value: balanceValue()) + } + + if viewModel.addressVisible { + addressView() + } + + if viewModel.hasMemo { + memoView() + } + + buttonView() + + if !viewModel.cautions.isEmpty { + cautionsView() + } + } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin16, trailing: .margin16)) + .animation(.linear, value: viewModel.hasMemo) + } + + NavigationLink( + isActive: $confirmPresented, + destination: { + if let sendData = viewModel.sendData { + RegularSendView(sendData: sendData) { + HudHelper.instance.show(banner: .sent) + + if let onDismiss { + onDismiss() + } else { + presentationMode.wrappedValue.dismiss() + } + } + } + } + ) { + EmptyView() + } + } + .navigationTitle("Send \(viewModel.token.coin.code)") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + if showIcon { + CoinIconView(coin: viewModel.token.coin) + } + } + + ToolbarItem(placement: .navigationBarTrailing) { + if let handler = viewModel.handler, handler.hasSettings { + Button(action: { + settingsPresented = true + }) { + Image("settings_24") + .renderingMode(.template) + .foregroundColor(.themeJacob) + } + } + } + } + .sheet(isPresented: $settingsPresented) { + if let handler = viewModel.handler { + handler.settingsView { + viewModel.syncSendData() + } + } + } + .accentColor(.themeJacob) + } + + @ViewBuilder private func availableBalanceView(value: String?) -> some View { + HStack(spacing: .margin8) { + Text("send.available_balance".localized).textCaption() + Spacer() + Text(value ?? "---") + .textCaption() + .multilineTextAlignment(.trailing) + } + .padding(.horizontal, .margin16) + } + + @ViewBuilder private func inputView() -> some View { + VStack(spacing: 3) { + TextField("", text: $viewModel.amountString, prompt: Text("0").foregroundColor(.themeGray)) + .foregroundColor(.themeLeah) + .font(.themeHeadline1) + .keyboardType(.decimalPad) + .focused($focusField, equals: .amount) + + if viewModel.rate != nil { + HStack(spacing: 0) { + Text(viewModel.currency.symbol).textBody(color: .themeGray) + + TextField("", text: $viewModel.fiatAmountString, prompt: Text("0").foregroundColor(.themeGray)) + .foregroundColor(.themeGray) + .font(.themeBody) + .keyboardType(.decimalPad) + .focused($focusField, equals: .fiatAmount) + .frame(height: 20) + } + } else { + Text("swap.rate_not_available".localized) + .themeSubhead2(color: .themeGray50, alignment: .leading) + .frame(height: 20) + } + } + .padding(.horizontal, .margin16) + .padding(.vertical, 20) + .modifier(ThemeListStyleModifier(cornerRadius: 18)) + .onFirstAppear { + focusField = .amount + } + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + if focusField != nil { + HStack(spacing: 0) { + if viewModel.availableBalance != nil { + ForEach(1 ... 4, id: \.self) { multiplier in + let percent = multiplier * 25 + + Button(action: { + viewModel.setAmountIn(percent: percent) + focusField = nil + }) { + Text("\(percent)%").textSubhead1(color: .themeLeah) + } + .frame(maxWidth: .infinity) + + RoundedRectangle(cornerRadius: 0.5, style: .continuous) + .fill(Color.themeSteel20) + .frame(width: 1) + .frame(maxHeight: .infinity) + } + } else { + Spacer() + } + + Button(action: { + viewModel.clearAmountIn() + }) { + Image(systemName: "trash") + .font(.themeSubhead1) + .foregroundColor(.themeLeah) + } + .frame(maxWidth: .infinity) + + RoundedRectangle(cornerRadius: 0.5, style: .continuous) + .fill(Color.themeSteel20) + .frame(width: 1) + .frame(maxHeight: .infinity) + + Button(action: { + focusField = nil + }) { + Image(systemName: "keyboard.chevron.compact.down") + .font(.themeSubhead1) + .foregroundColor(.themeLeah) + } + .frame(maxWidth: .infinity) + } + .padding(.horizontal, -16) + .frame(maxWidth: .infinity) + } + } + } + } + + @ViewBuilder private func addressView() -> some View { + AddressViewNew( + initial: .init( + blockchainType: viewModel.token.blockchainType, + showContacts: true + ), + text: $viewModel.address, + result: $viewModel.addressResult + ) + } + + @ViewBuilder private func memoView() -> some View { + InputTextRow { + InputTextView( + placeholder: "send.confirmation.memo_placeholder".localized, + multiline: true, + font: .themeBody.italic(), + text: $viewModel.memo + ) + } + } + + @ViewBuilder private func buttonView() -> some View { + let (title, disabled, showProgress) = buttonState() + + Button(action: { + confirmPresented = true + }) { + HStack(spacing: .margin8) { + if showProgress { + ProgressView() + } + + Text(title) + } + } + .disabled(disabled) + .buttonStyle(PrimaryButtonStyle(style: .yellow)) + } + + @ViewBuilder private func cautionsView() -> some View { + let cautions = viewModel.cautions + + VStack(spacing: .margin12) { + ForEach(cautions.indices, id: \.self) { index in + HighlightedTextView(caution: cautions[index]) + } + } + } + + private func balanceValue() -> String? { + guard let availableBalance = viewModel.availableBalance else { + return nil + } + + return ValueFormatter.instance.formatFull(coinValue: CoinValue(kind: .token(token: viewModel.token), value: availableBalance)) + } + + private func buttonState() -> (String, Bool, Bool) { + let title: String + var disabled = true + var showProgress = false + + if viewModel.adapterState == nil { + title = "send.token_not_enabled".localized + } else if let adapterState = viewModel.adapterState, adapterState.syncing { + title = "send.token_syncing".localized + showProgress = true + } else if let adapterState = viewModel.adapterState, !adapterState.isSynced { + title = "send.token_not_synced".localized + } else if viewModel.amount == nil { + title = "send.enter_amount".localized + } else if let availableBalance = viewModel.availableBalance, let amount = viewModel.amount, amount > availableBalance { + title = "send.insufficient_balance".localized + } else if case .empty = viewModel.addressState { + title = "send.enter_address".localized + } else if case .invalid = viewModel.addressState { + title = "send.invalid_address".localized + } else { + title = "send.next_button".localized + disabled = viewModel.sendData == nil + } + + return (title, disabled, showProgress) + } +} + +extension PreSendView { + private enum FocusField: Int, Hashable { + case amount + case fiatAmount + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/PreSendViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/PreSendViewModel.swift new file mode 100644 index 0000000000..651fbcc950 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/PreSendViewModel.swift @@ -0,0 +1,297 @@ +import Combine +import Foundation +import MarketKit + +class PreSendViewModel: ObservableObject { + private let wallet: Wallet + private let mode: Mode + private let currencyManager = App.shared.currencyManager + private let marketKit = App.shared.marketKit + private let walletManager = App.shared.walletManager + private let adapterManager = App.shared.adapterManager + + private var cancellables = Set() + + @Published var currency: Currency + + var amount: Decimal? { + didSet { + syncFiatAmount() + syncSendData() + + let amount = Decimal(string: amountString) + + if amount != self.amount { + amountString = self.amount?.description ?? "" + } + } + } + + @Published var amountString: String = "" { + didSet { + var amount = Decimal(string: amountString) + + if amount == 0 { + amount = nil + } + + guard amount != self.amount else { + return + } + + enteringFiat = false + + self.amount = amount + } + } + + @Published var fiatAmount: Decimal? { + didSet { + syncAmount() + + let amount = Decimal(string: fiatAmountString)?.rounded(decimal: 2) + + if amount != fiatAmount { + fiatAmountString = fiatAmount?.description ?? "" + } + } + } + + @Published var fiatAmountString: String = "" { + didSet { + let amount = Decimal(string: fiatAmountString)?.rounded(decimal: 2) + + guard amount != fiatAmount else { + return + } + + enteringFiat = true + + fiatAmount = amount + } + } + + @Published var rate: Decimal? { + didSet { + syncFiatAmount() + } + } + + @Published var adapterState: AdapterState? + @Published var availableBalance: Decimal? + @Published var hasMemo = false + + private var enteringFiat = false + + @Published var address: String = "" + @Published var addressResult: AddressInput.Result = .idle { + didSet { + syncAddressState() + } + } + + @Published var addressState: AddressState = .empty { + didSet { + syncHasMemo() + syncSendData() + } + } + + @Published var memo: String = "" { + didSet { + syncSendData() + } + } + + var handler: IPreSendHandler? + @Published var sendData: SendData? + @Published var cautions = [CautionNew]() + + let addressVisible: Bool + + init(wallet: Wallet, mode: Mode) { + self.wallet = wallet + self.mode = mode + + handler = SendHandlerFactory.preSendHandler(wallet: wallet) + currency = currencyManager.baseCurrency + + switch mode { + case let .predefined(address): + addressState = .valid(address: address) + addressVisible = false + default: + addressVisible = true + } + + defer { + switch mode { + case let .prefilled(address, amount): + self.address = address + self.amount = amount + default: () + } + } + + currencyManager.$baseCurrency + .receive(on: DispatchQueue.main) + .sink { [weak self] in self?.currency = $0 } + .store(in: &cancellables) + + rate = marketKit.coinPrice(coinUid: wallet.coin.uid, currencyCode: currency.code)?.value + marketKit.coinPricePublisher(coinUid: wallet.coin.uid, currencyCode: currency.code) + .receive(on: DispatchQueue.main) + .sink { [weak self] price in self?.rate = price.value } + .store(in: &cancellables) + + if let handler { + adapterState = handler.state + availableBalance = handler.balance + hasMemo = handler.hasMemo(address: nil) + + handler.statePublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] in self?.adapterState = $0 } + .store(in: &cancellables) + + handler.balancePublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] in self?.availableBalance = $0 } + .store(in: &cancellables) + + handler.settingsModifiedPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in self?.syncSendData() } + .store(in: &cancellables) + } + + syncFiatAmount() + } + + private func syncAmount() { + guard enteringFiat else { + return + } + + guard let rate, let fiatAmount else { + amount = nil + return + } + + amount = fiatAmount / rate + } + + private func syncFiatAmount() { + guard !enteringFiat else { + return + } + + guard let rate, let amount else { + fiatAmount = nil + return + } + + fiatAmount = (amount * rate).rounded(decimal: 2) + } + + private func syncAddressState() { + switch addressResult { + case .idle: + addressState = .empty + case .loading, .invalid: + addressState = .invalid + case let .valid(success): + let address = success.address.raw + addressState = .valid(address: address) + } + } + + private func syncHasMemo() { + guard let handler else { + hasMemo = false + return + } + + hasMemo = handler.hasMemo(address: addressState.address) + } +} + +extension PreSendViewModel { + var token: Token { + wallet.token + } + + func syncSendData() { + guard let amount else { + sendData = nil + return + } + + guard case let .valid(address) = addressState else { + sendData = nil + return + } + + guard let handler else { + sendData = nil + return + } + + let trimmedMemo = memo.trimmingCharacters(in: .whitespaces) + let memo = hasMemo && !trimmedMemo.isEmpty ? trimmedMemo : nil + + let result = handler.sendData(amount: amount, address: address, memo: memo) + + switch result { + case let .valid(sendData): + self.sendData = sendData + cautions = [] + case let .invalid(cautions): + sendData = nil + self.cautions = cautions + } + } + + func setAmountIn(percent: Int) { + guard let availableBalance else { + return + } + + enteringFiat = false + + amount = (availableBalance * Decimal(percent) / 100).rounded(decimal: token.decimals) + } + + func clearAmountIn() { + enteringFiat = false + amount = nil + } +} + +extension PreSendViewModel { + enum Mode { + case regular + case prefilled(address: String, amount: Decimal?) + case predefined(address: String) + + var amount: Decimal? { + switch self { + case let .prefilled(_, amount): return amount + default: return nil + } + } + } + + enum AddressState { + case empty + case invalid + case valid(address: String) + + var address: String? { + switch self { + case let .valid(address): return address + default: return nil + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/RegularSendView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/RegularSendView.swift new file mode 100644 index 0000000000..71ef94c632 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/RegularSendView.swift @@ -0,0 +1,54 @@ +import ComponentKit +import MarketKit +import SwiftUI + +struct RegularSendView: View { + @StateObject var sendViewModel: SendViewModel + + private let onSuccess: () -> Void + + init(sendData: SendData, onSuccess: @escaping () -> Void) { + _sendViewModel = .init(wrappedValue: SendViewModel(sendData: sendData)) + self.onSuccess = onSuccess + } + + var body: some View { + ThemeView { + BottomGradientWrapper { + SendView(viewModel: sendViewModel) + } bottomContent: { + switch sendViewModel.state { + case .syncing: + EmptyView() + case let .success(data): + if sendViewModel.canSend, sendViewModel.handler?.expirationDuration == nil || sendViewModel.timeLeft > 0 || sendViewModel.sending { + SlideButton( + styling: .text(start: data.customSendButtonTitle ?? "send.confirmation.slide_to_send".localized, end: "", success: ""), + action: { + try await sendViewModel.send() + }, completion: { + onSuccess() + } + ) + } else { + Button(action: { + sendViewModel.sync() + }) { + Text("send.confirmation.refresh".localized) + } + .buttonStyle(PrimaryButtonStyle(style: .gray)) + } + case .failed: + Button(action: { + sendViewModel.sync() + }) { + Text("send.confirmation.refresh".localized) + } + .buttonStyle(PrimaryButtonStyle(style: .gray)) + } + } + } + .navigationTitle("send.confirmation.title".localized) + .navigationBarTitleDisplayMode(.inline) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendData.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendData.swift new file mode 100644 index 0000000000..67b37a3792 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendData.swift @@ -0,0 +1,14 @@ +import BitcoinCore +import EvmKit +import Foundation +import MarketKit +import ZcashLightClientKit + +enum SendData { + case evm(blockchainType: BlockchainType, transactionData: TransactionData) + case bitcoin(token: Token, params: SendParameters) + case binance(token: Token, amount: Decimal, address: String, memo: String?) + case zcash(amount: Decimal, recipient: Recipient, memo: String?) + case swap(tokenIn: Token, tokenOut: Token, amountIn: Decimal, provider: IMultiSwapProvider) + case walletConnect(request: WalletConnectRequest) +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/SendConfirmField.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendField.swift similarity index 81% rename from UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/SendConfirmField.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendField.swift index 6397079424..c75e0e76b1 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/SendConfirmField.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendField.swift @@ -3,23 +3,19 @@ import Kingfisher import MarketKit import SwiftUI -enum SendConfirmField { +enum SendField { case amount(title: String, token: Token, coinValueType: CoinValueType, currencyValue: CurrencyValue?, type: AmountType) case value(title: String, description: ActionSheetView.InfoDescription?, coinValue: CoinValue?, currencyValue: CurrencyValue?, formatFull: Bool) case levelValue(title: String, value: String, level: ValueLevel) case address(title: String, value: String, blockchainType: BlockchainType) + case price(title: String, tokenA: Token, tokenB: Token, amountA: Decimal, amountB: Decimal) + case hex(title: String, value: String) @ViewBuilder var listRow: some View { switch self { case let .amount(title, token, coinValueType, currencyValue, type): ListRow { - KFImage.url(URL(string: token.coin.imageUrl)) - .resizable() - .placeholder { - Circle().fill(Color.themeSteel20) - } - .clipShape(Circle()) - .frame(width: .iconSize32, height: .iconSize32) + CoinIconView(coin: token.coin) HStack(spacing: .margin4) { VStack(alignment: .leading, spacing: 1) { @@ -83,10 +79,32 @@ enum SendConfirmField { ListRow { Text(title).textSubhead2() Spacer() - Text(value).textSubhead1(color: color(valueLevel: level)) + Text(value) + .textSubhead1(color: color(valueLevel: level)) + .multilineTextAlignment(.trailing) } case let .address(title, value, blockchainType): RecipientRowsView(title: title, value: value, blockchainType: blockchainType) + case let .price(title, tokenA, tokenB, amountA, amountB): + PriceRow(title: title, tokenA: tokenA, tokenB: tokenB, amountA: amountA, amountB: amountB) + case let .hex(title, value): + ListRow { + Text(title).textSubhead2() + + Spacer() + + Text(value) + .textSubhead1(color: .themeLeah) + .lineLimit(3) + .truncationMode(.middle) + + Button(action: { + CopyHelper.copyAndNotify(value: value) + }) { + Image("copy_20").renderingMode(.template) + } + .buttonStyle(SecondaryCircleButtonStyle(style: .default)) + } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendHandlerFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendHandlerFactory.swift new file mode 100644 index 0000000000..7489f45f6e --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendHandlerFactory.swift @@ -0,0 +1,42 @@ +import MarketKit + +enum SendHandlerFactory { + static func handler(sendData: SendData) -> ISendHandler? { + switch sendData { + case let .evm(blockchainType, transactionData): + return EvmSendHandler.instance(blockchainType: blockchainType, transactionData: transactionData) + case let .bitcoin(token, params): + return BitcoinSendHandler.instance(token: token, params: params) + case let .binance(token, amount, address, memo): + return BinanceSendHandler.instance(token: token, amount: amount, address: address, memo: memo) + case let .zcash(amount, recipient, memo): + return ZcashSendHandler.instance(amount: amount, recipient: recipient, memo: memo) + case let .swap(tokenIn, tokenOut, amountIn, provider): + return MultiSwapSendHandler.instance(tokenIn: tokenIn, tokenOut: tokenOut, amountIn: amountIn, provider: provider) + case let .walletConnect(request): + return WalletConnectSendHandler.instance(request: request) + } + } + + static func preSendHandler(wallet: Wallet) -> IPreSendHandler? { + let adapter = App.shared.adapterManager.adapter(for: wallet) + + if let adapter = adapter as? ISendEthereumAdapter & IBalanceAdapter { + return EvmPreSendHandler(token: wallet.token, adapter: adapter) + } + + if let adapter = adapter as? BitcoinBaseAdapter { + return BitcoinPreSendHandler(token: wallet.token, adapter: adapter) + } + + if let adapter = adapter as? ISendBinanceAdapter & IBalanceAdapter { + return BinancePreSendHandler(token: wallet.token, adapter: adapter) + } + + if let adapter = adapter as? ISendZcashAdapter & IBalanceAdapter { + return ZcashPreSendHandler(token: wallet.token, adapter: adapter) + } + + return nil + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendView.swift new file mode 100644 index 0000000000..d323e60a10 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendView.swift @@ -0,0 +1,102 @@ +import ComponentKit +import Kingfisher +import MarketKit +import SwiftUI + +struct SendView: View { + @ObservedObject var viewModel: SendViewModel + + @State private var feeSettingsPresented = false + + var body: some View { + ZStack { + if let handler = viewModel.handler { + switch viewModel.state { + case .syncing: + VStack(spacing: .margin12) { + ProgressView() + + if let syncingText = handler.syncingText { + Text(syncingText).textSubhead2() + } + } + case let .success(data): + dataView(data: data, handler: handler) + case let .failed(error): + errorView(error: error) + } + } else { + Text("No Handler") + } + } + .sheet(isPresented: $feeSettingsPresented) { + if let transactionService = viewModel.transactionService, let feeToken = viewModel.handler?.baseToken { + transactionService.settingsView( + feeData: Binding(get: { viewModel.state.data?.feeData }, set: { _ in }), + loading: Binding(get: { viewModel.state.isSyncing }, set: { _ in }), + feeToken: feeToken, + currency: viewModel.currency, + feeTokenRate: Binding(get: { viewModel.rates[feeToken.coin.uid] }, set: { _ in }) + ) + } + } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + if viewModel.transactionService != nil { + Button(action: { + feeSettingsPresented = true + }) { + Image("manage_2_20").renderingMode(.template) + } + .disabled(viewModel.state.isSyncing) + } + } + } + .onReceive(viewModel.errorSubject) { error in + HudHelper.instance.showError(subtitle: error) + } + .accentColor(.themeJacob) + } + + @ViewBuilder private func dataView(data: ISendData, handler: ISendHandler) -> some View { + ScrollView { + VStack(spacing: .margin16) { + let sections = data.sections(baseToken: handler.baseToken, currency: viewModel.currency, rates: viewModel.rates) + + if !sections.isEmpty { + ForEach(sections.indices, id: \.self) { sectionIndex in + let section = sections[sectionIndex] + + if !section.isEmpty { + ListSection { + ForEach(section.indices, id: \.self) { index in + section[index].listRow + } + } + } + } + } + + let cautions = viewModel.cautions + + if !cautions.isEmpty { + VStack(spacing: .margin12) { + ForEach(cautions.indices, id: \.self) { index in + HighlightedTextView(caution: cautions[index]) + } + } + } + } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) + } + } + + @ViewBuilder private func errorView(error: Error) -> some View { + ScrollView { + VStack(spacing: .margin16) { + HighlightedTextView(caution: CautionNew(text: error.smartDescription, type: .error)) + } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendViewModel.swift new file mode 100644 index 0000000000..878bb93f21 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendViewModel.swift @@ -0,0 +1,193 @@ +import Combine +import Foundation +import HsExtensions +import MarketKit + +class SendViewModel: ObservableObject { + private let currencyManager = App.shared.currencyManager + private let marketKit = App.shared.marketKit + + private var syncTask: AnyTask? + private var timer: AnyCancellable? + private var ratesCancellable: AnyCancellable? + private var cancellables = Set() + + let handler: ISendHandler? + let transactionService: ITransactionService? + let currency: Currency + + @Published var rates = [String: Decimal]() + + @Published var sending = false + @Published var transactionSettingsModified = false + @Published var timeLeft: Int = 0 + + let errorSubject = PassthroughSubject() + + @Published var state: State = .syncing { + didSet { + timer?.cancel() + + if let handler, let expirationDuration = handler.expirationDuration, let data = state.data, data.canSend { + timeLeft = expirationDuration + + timer = Timer.publish(every: 1, on: .main, in: .common) + .autoconnect() + .sink { [weak self] _ in + self?.handleTimerTick() + } + } + } + } + + var cautions: [CautionNew] { + var cautions = transactionService?.cautions ?? [] + + if let data = state.data, let baseToken = handler?.baseToken { + cautions.append(contentsOf: data.cautions(baseToken: baseToken)) + } + + return cautions + } + + var canSend: Bool { + guard let data = state.data, data.canSend else { + return false + } + + guard let service = transactionService, !service.cautions.contains(where: { $0.type == .error }) else { + return false + } + + return true + } + + init(sendData: SendData) { + handler = SendHandlerFactory.handler(sendData: sendData) + currency = currencyManager.baseCurrency + + if let handler { + transactionService = TransactionServiceFactory.transactionService(blockchainType: handler.baseToken.blockchainType, initialTransactionSettings: handler.initialTransactionSettings) + } else { + transactionService = nil + } + + transactionService?.updatePublisher + .sink { [weak self] in + self?.syncTransactionSettingsModified() + self?.sync() + } + .store(in: &cancellables) + + sync() + } + + private func handleTimerTick() { + timeLeft -= 1 + + if timeLeft == 0 { + timer?.cancel() + } + } + + private func syncTransactionSettingsModified() { + transactionSettingsModified = transactionService?.modified ?? false + } + + @MainActor private func syncRates(coins: [Coin]) { + let coinUids = Array(Set(coins)).map(\.uid) + + rates = marketKit.coinPriceMap(coinUids: coinUids, currencyCode: currency.code).mapValues { $0.value } + ratesCancellable = marketKit.coinPriceMapPublisher(coinUids: coinUids, currencyCode: currency.code) + .receive(on: DispatchQueue.main) + .sink { [weak self] rates in self?.rates = rates.mapValues { $0.value } } + } + + @MainActor private func set(sending: Bool) { + self.sending = sending + } +} + +extension SendViewModel { + func sync() { + guard let handler else { + return + } + + syncTask = nil + + if !state.isSyncing { + state = .syncing + } + + syncTask = Task { [weak self, handler, transactionService] in + var state: State + + do { + try await transactionService?.sync() + + let data = try await handler.sendData(transactionSettings: transactionService?.transactionSettings) + + await self?.syncRates(coins: [handler.baseToken.coin] + data.rateCoins) + + state = .success(data: data) + } catch { + state = .failed(error: error) + } + + if !Task.isCancelled { + await MainActor.run { [weak self, state] in + self?.state = state + } + } + } + .erased() + } + + func send() async throws { + do { + guard let handler else { + throw SendError.noHandler + } + + guard let data = state.data else { + throw SendError.noData + } + + await set(sending: true) + + _ = try await handler.send(data: data) + } catch { + await set(sending: false) + errorSubject.send(error.smartDescription) + throw error + } + } +} + +extension SendViewModel { + enum State { + case syncing + case success(data: ISendData) + case failed(error: Error) + + var data: ISendData? { + switch self { + case let .success(data): return data + default: return nil + } + } + + var isSyncing: Bool { + switch self { + case .syncing: return true + default: return false + } + } + } + + enum SendError: Error { + case noHandler + case noData + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/TransactionServiceFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/TransactionServiceFactory.swift new file mode 100644 index 0000000000..0908313a3f --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/TransactionServiceFactory.swift @@ -0,0 +1,18 @@ +import MarketKit + +enum TransactionServiceFactory { + static func transactionService(blockchainType: BlockchainType, initialTransactionSettings: InitialTransactionSettings?) -> ITransactionService? { + if EvmBlockchainManager.blockchainTypes.contains(blockchainType), + let evmKit = App.shared.evmBlockchainManager.evmKitManager(blockchainType: blockchainType).evmKitWrapper?.evmKit, + let transactionService = EvmTransactionService(blockchainType: blockchainType, userAddress: evmKit.receiveAddress, initialTransactionSettings: initialTransactionSettings) + { + return transactionService + } + + if BtcBlockchainManager.blockchainTypes.contains(blockchainType) { + return BitcoinTransactionService(blockchainType: blockchainType) + } + + return nil + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/TransactionSettings.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/TransactionSettings.swift similarity index 74% rename from UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/TransactionSettings.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/SendNew/TransactionSettings.swift index 7e4a8f4ffd..f6de7ddd20 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/TransactionSettings.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/TransactionSettings.swift @@ -1,12 +1,12 @@ import EvmKit enum TransactionSettings { - case evm(gasPrice: GasPrice, nonce: Int?) + case evm(gasPriceData: GasPriceData, nonce: Int?) case bitcoin(satoshiPerByte: Int) - var gasPrice: GasPrice? { + var gasPriceData: GasPriceData? { switch self { - case let .evm(gasPrice, _): return gasPrice + case let .evm(gasPriceData, _): return gasPriceData default: return nil } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/TronSendHandler.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/TronSendHandler.swift new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/TronSendHandler.swift @@ -0,0 +1 @@ + diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/ValueLevel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/ValueLevel.swift similarity index 100% rename from UnstoppableWallet/UnstoppableWallet/Modules/MultiSwap/SendConfirmation/ValueLevel.swift rename to UnstoppableWallet/UnstoppableWallet/Modules/SendNew/ValueLevel.swift diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/WalletConnectSendHandler.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/WalletConnectSendHandler.swift new file mode 100644 index 0000000000..5a32b22324 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/WalletConnectSendHandler.swift @@ -0,0 +1,143 @@ +import EvmKit +import Foundation +import MarketKit + +class WalletConnectSendHandler { + private let request: WalletConnectRequest + private let payload: WCEthereumTransactionPayload + private let signService: IWalletConnectSignService = App.shared.walletConnectSessionManager.service + + let baseToken: Token + private let transactionData: TransactionData + private let evmKitWrapper: EvmKitWrapper + private let decorator = EvmDecorator() + private let evmFeeEstimator = EvmFeeEstimator() + + init(request: WalletConnectRequest, payload: WCEthereumTransactionPayload, baseToken: Token, transactionData: TransactionData, evmKitWrapper: EvmKitWrapper) { + self.request = request + self.payload = payload + self.baseToken = baseToken + self.transactionData = transactionData + self.evmKitWrapper = evmKitWrapper + } +} + +extension WalletConnectSendHandler: ISendHandler { + var syncingText: String? { + nil + } + + var expirationDuration: Int? { + nil + } + + var initialTransactionSettings: InitialTransactionSettings? { + let transaction = payload.transaction + var gasPrice: GasPrice? + + if let maxFeePerGas = transaction.maxFeePerGas, + let maxPriorityFeePerGas = transaction.maxPriorityFeePerGas + { + gasPrice = .eip1559(maxFeePerGas: maxFeePerGas, maxPriorityFeePerGas: maxPriorityFeePerGas) + } else if let _gasPrice = transaction.gasPrice { + gasPrice = .legacy(gasPrice: _gasPrice) + } + + return .evm(gasPrice: gasPrice, nonce: transaction.nonce) + } + + func sendData(transactionSettings: TransactionSettings?) async throws -> ISendData { + let gasPriceData = transactionSettings?.gasPriceData + var evmFeeData: EvmFeeData? + var transactionError: Error? + + if let gasPriceData { + if let gasLimit = payload.transaction.gasLimit { + evmFeeData = EvmFeeData(gasLimit: gasLimit, surchargedGasLimit: gasLimit) + } else { + do { + evmFeeData = try await evmFeeEstimator.estimateFee(evmKitWrapper: evmKitWrapper, transactionData: transactionData, gasPriceData: gasPriceData) + } catch { + transactionError = error + } + } + } + + let transactionDecoration = evmKitWrapper.evmKit.decorate(transactionData: transactionData) + let decoration = decorator.decorate(baseToken: baseToken, transactionData: transactionData, transactionDecoration: transactionDecoration) + + return EvmSendData( + decoration: decoration, + transactionData: transactionData, + transactionError: transactionError, + gasPrice: gasPriceData?.userDefined, + evmFeeData: evmFeeData, + nonce: transactionSettings?.nonce + ) + } + + func send(data: ISendData) async throws { + guard let data = data as? EvmSendData else { + throw SendError.invalidData + } + + guard let transactionData = data.transactionData else { + throw SendError.noTransactionData + } + + guard let gasPrice = data.gasPrice else { + throw SendError.noGasPrice + } + + guard let gasLimit = data.evmFeeData?.surchargedGasLimit else { + throw SendError.noGasLimit + } + + let fullTransaction = try await evmKitWrapper.send( + transactionData: transactionData, + gasPrice: gasPrice, + gasLimit: gasLimit, + nonce: data.nonce + ) + + signService.approveRequest(id: request.id, result: fullTransaction.transaction.hash) + } +} + +extension WalletConnectSendHandler { + enum SendError: Error { + case invalidData + case noGasPrice + case noGasLimit + case noTransactionData + } +} + +extension WalletConnectSendHandler { + static func instance(request: WalletConnectRequest) -> WalletConnectSendHandler? { + guard let payload = request.payload as? WCEthereumTransactionPayload, + let account = App.shared.accountManager.activeAccount, + let evmKitWrapper = App.shared.walletConnectManager.evmKitWrapper(chainId: request.chain.id, account: account) + else { + return nil + } + + guard let baseToken = try? App.shared.coinManager.token(query: .init(blockchainType: evmKitWrapper.blockchainType, tokenType: .native)) else { + return nil + } + + let transactionData = TransactionData( + to: payload.transaction.to, + value: payload.transaction.value, + input: payload.transaction.data + ) + + return WalletConnectSendHandler( + request: request, + payload: payload, + baseToken: baseToken, + transactionData: transactionData, + evmKitWrapper: evmKitWrapper + ) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/WalletConnectSendView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/WalletConnectSendView.swift new file mode 100644 index 0000000000..c858ee7f9c --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/WalletConnectSendView.swift @@ -0,0 +1,62 @@ +import ComponentKit +import MarketKit +import SwiftUI + +struct WalletConnectSendView: View { + @StateObject var viewModel: WalletConnectSendViewModel + @StateObject var sendViewModel: SendViewModel + + @Environment(\.presentationMode) private var presentationMode + + init(request: WalletConnectRequest) { + _viewModel = .init(wrappedValue: WalletConnectSendViewModel(request: request)) + _sendViewModel = .init(wrappedValue: SendViewModel(sendData: .walletConnect(request: request))) + } + + var body: some View { + ThemeView { + BottomGradientWrapper { + SendView(viewModel: sendViewModel) + } bottomContent: { + VStack(spacing: .margin16) { + switch sendViewModel.state { + case .syncing: + EmptyView() + case .success: + Button(action: { + Task { + try await sendViewModel.send() + + await MainActor.run { + presentationMode.wrappedValue.dismiss() + } + } + }) { + Text("wallet_connect.button.confirm".localized) + } + .buttonStyle(PrimaryButtonStyle(style: .yellow)) + .disabled(sendViewModel.sending) + case .failed: + Button(action: { + sendViewModel.sync() + }) { + Text("send.confirmation.refresh".localized) + } + .buttonStyle(PrimaryButtonStyle(style: .gray)) + } + + Button(action: { + viewModel.reject() + presentationMode.wrappedValue.dismiss() + }) { + Text("button.reject".localized) + } + .buttonStyle(PrimaryButtonStyle(style: .gray)) + .disabled(sendViewModel.sending) + } + } + } + .navigationTitle(viewModel.dAppName) + .navigationBarTitleDisplayMode(.inline) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/WalletConnectSendViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/WalletConnectSendViewModel.swift new file mode 100644 index 0000000000..d5fd8ca1f9 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/WalletConnectSendViewModel.swift @@ -0,0 +1,18 @@ +import Combine + +class WalletConnectSendViewModel: ObservableObject { + private let request: WalletConnectRequest + private let signService: IWalletConnectSignService = App.shared.walletConnectSessionManager.service + + init(request: WalletConnectRequest) { + self.request = request + } + + var dAppName: String { + request.payload.dAppName + } + + func reject() { + signService.rejectRequest(id: request.id) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/ZcashPreSendHandler.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/ZcashPreSendHandler.swift new file mode 100644 index 0000000000..790c268e66 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/ZcashPreSendHandler.swift @@ -0,0 +1,76 @@ +import Combine +import Foundation +import MarketKit +import RxSwift + +class ZcashPreSendHandler { + private let token: Token + private let adapter: ISendZcashAdapter & IBalanceAdapter + + private let stateSubject = PassthroughSubject() + private let balanceSubject = PassthroughSubject() + + private let disposeBag = DisposeBag() + + init(token: Token, adapter: ISendZcashAdapter & IBalanceAdapter) { + self.token = token + self.adapter = adapter + + adapter.balanceStateUpdatedObservable + .observeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) + .subscribe { [weak self] state in + self?.stateSubject.send(state) + } + .disposed(by: disposeBag) + + adapter.balanceDataUpdatedObservable + .observeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) + .subscribe { [weak self, adapter] balanceData in + let balance = max(0, balanceData.available - adapter.fee) + self?.balanceSubject.send(balance) + } + .disposed(by: disposeBag) + } +} + +extension ZcashPreSendHandler: IPreSendHandler { + var state: AdapterState { + adapter.balanceState + } + + var statePublisher: AnyPublisher { + stateSubject.eraseToAnyPublisher() + } + + var balance: Decimal { + max(0, adapter.balanceData.available - adapter.fee) + } + + var balancePublisher: AnyPublisher { + balanceSubject.eraseToAnyPublisher() + } + + func hasMemo(address: String?) -> Bool { + guard let address, let addressType = try? adapter.validate(address: address, checkSendToSelf: true) else { + return false + } + + return addressType == .shielded + } + + func sendData(amount: Decimal, address: String, memo: String?) -> SendDataResult { + do { + _ = try adapter.validate(address: address, checkSendToSelf: true) + } catch { + return .invalid(cautions: [CautionNew(text: error.smartDescription, type: .error)]) + } + + guard let recipient = adapter.recipient(from: address) else { + return .invalid(cautions: []) + } + + let sendData: SendData = .zcash(amount: amount, recipient: recipient, memo: memo) + + return .valid(sendData: sendData) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/ZcashSendHandler.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/ZcashSendHandler.swift new file mode 100644 index 0000000000..c17819ee2b --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/ZcashSendHandler.swift @@ -0,0 +1,137 @@ +import Foundation +import MarketKit +import ZcashLightClientKit + +class ZcashSendHandler { + private let token: Token + private let amount: Decimal + private let recipient: Recipient + private let memo: String? + private var adapter: ISendZcashAdapter + + init(token: Token, amount: Decimal, recipient: Recipient, memo: String?, adapter: ISendZcashAdapter) { + self.token = token + self.amount = amount + self.recipient = recipient + self.memo = memo + self.adapter = adapter + } +} + +extension ZcashSendHandler: ISendHandler { + var baseToken: Token { + token + } + + func sendData(transactionSettings _: TransactionSettings?) async throws -> ISendData { + SendData( + token: token, + amount: amount, + recipient: recipient, + memo: memo, + fee: adapter.fee + ) + } + + func send(data: ISendData) async throws { + guard let data = data as? SendData else { + throw SendError.invalidData + } + + _ = try await adapter.send(amount: data.amount, address: data.recipient, memo: data.memo.flatMap { try? Memo(string: $0) }) + } +} + +extension ZcashSendHandler { + class SendData: ISendData { + private let token: Token + let amount: Decimal + let recipient: Recipient + let memo: String? + private let fee: Decimal + + init(token: Token, amount: Decimal, recipient: Recipient, memo: String?, fee: Decimal) { + self.token = token + self.amount = amount + self.recipient = recipient + self.memo = memo + self.fee = fee + } + + var feeData: FeeData? { + nil + } + + var canSend: Bool { + true + } + + var rateCoins: [Coin] { + [token.coin] + } + + func cautions(baseToken _: Token) -> [CautionNew] { + [] + } + + func sections(baseToken: Token, currency: Currency, rates: [String: Decimal]) -> [[SendField]] { + var fields: [SendField] = [ + .amount( + title: "send.confirmation.you_send".localized, + token: token, + coinValueType: .regular(coinValue: CoinValue(kind: .token(token: token), value: amount)), + currencyValue: rates[token.coin.uid].map { CurrencyValue(currency: currency, value: $0 * amount) }, + type: .neutral + ), + .address( + title: "send.confirmation.to".localized, + value: recipient.stringEncoded, + blockchainType: .binanceChain + ), + ] + + if let memo { + fields.append(.levelValue(title: "send.confirmation.memo".localized, value: memo, level: .regular)) + } + + return [ + fields, + [ + .value( + title: "fee_settings.network_fee".localized, + description: .init(title: "fee_settings.network_fee".localized, description: "fee_settings.network_fee.info".localized), + coinValue: CoinValue(kind: .token(token: baseToken), value: fee), + currencyValue: rates[baseToken.coin.uid].map { CurrencyValue(currency: currency, value: fee * $0) }, + formatFull: true + ), + ], + ] + } + } +} + +extension ZcashSendHandler { + enum SendError: Error { + case invalidData + } +} + +extension ZcashSendHandler { + static func instance(amount: Decimal, recipient: Recipient, memo: String?) -> ZcashSendHandler? { + guard let token = try? App.shared.coinManager.token(query: .init(blockchainType: .zcash, tokenType: .native)) else { + return nil + } + + guard let adapter = App.shared.adapterManager.adapter(for: token) as? ISendZcashAdapter else { + return nil + } + + return ZcashSendHandler( + token: token, + amount: amount, + recipient: recipient, + memo: memo, + adapter: adapter + ) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/Confirmation/SendTronConfirmationService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/Confirmation/SendTronConfirmationService.swift index fdfdc81c62..6854e01257 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/Confirmation/SendTronConfirmationService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/Confirmation/SendTronConfirmationService.swift @@ -13,7 +13,6 @@ class SendTronConfirmationService { private let feeService: SendFeeService private let tronKitWrapper: TronKitWrapper private let evmLabelManager: EvmLabelManager - private let sendAddress: TronKit.Address? private let stateRelay = PublishRelay() private(set) var state: State = .notReady(errors: []) { @@ -31,13 +30,6 @@ class SendTronConfirmationService { } } - private let sendAdressActiveRelay = PublishRelay() - private(set) var sendAdressActive: Bool = true { - didSet { - sendAdressActiveRelay.accept(sendAdressActive) - } - } - private(set) var contract: Contract private(set) var dataState: DataState @@ -59,23 +51,8 @@ class SendTronConfirmationService { decoration: tronKitWrapper.tronKit.decorate(contract: contract) ) - switch contract { - case let transfer as TransferContract: - sendAddress = transfer.toAddress - - case is TriggerSmartContract: - if let transfer = dataState.decoration as? OutgoingEip20Decoration { - sendAddress = transfer.to - } else { - sendAddress = nil - } - - default: sendAddress = nil - } - feeService.feeValueService = self syncFees() - syncAddress() } private var tronKit: TronKit.Kit { @@ -143,17 +120,6 @@ class SendTronConfirmationService { state = .ready(fees: fees) } - - private func syncAddress() { - guard let sendAddress else { - return - } - - Task { [weak self, tronKit] in - let active = try? await tronKit.accountActive(address: sendAddress) - self?.sendAdressActive = active ?? true - }.store(in: &tasks) - } } extension SendTronConfirmationService: ISendXFeeValueService { @@ -171,10 +137,6 @@ extension SendTronConfirmationService { sendStateRelay.asObservable() } - var sendAdressActiveObservable: Observable { - sendAdressActiveRelay.asObservable() - } - func send() { guard case .ready = state, case let .completed(fee) = feeState else { return diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/Confirmation/SendTronConfirmationViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/Confirmation/SendTronConfirmationViewModel.swift index fc89cdf593..3c3c804a09 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/Confirmation/SendTronConfirmationViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/Confirmation/SendTronConfirmationViewModel.swift @@ -33,7 +33,6 @@ class SendTronConfirmationViewModel { subscribe(disposeBag, service.stateObservable) { [weak self] in self?.sync(state: $0) } subscribe(disposeBag, service.sendStateObservable) { [weak self] in self?.sync(sendState: $0) } - subscribe(disposeBag, service.sendAdressActiveObservable) { [weak self] _ in self?.reSyncServiceState() } subscribe(disposeBag, contactLabelService.stateObservable) { [weak self] _ in self?.reSyncServiceState() } sync(state: service.state) @@ -183,16 +182,6 @@ class SendTronConfirmationViewModel { } } - private func addressActiveViewItems() -> [ViewItem] { - guard !service.sendAdressActive else { - return [] - } - - return [ - .warning(text: "tron.send.inactive_address".localized, title: "tron.send.activation_fee".localized, info: "tron.send.activation_fee.info".localized), - ] - } - private func amountViewItem(title: String, coinService: CoinService, value: BigUInt, type: AmountType) -> ViewItem { amountViewItem(title: title, coinService: coinService, amountData: coinService.amountData(value: value, sign: type.sign), type: type) } @@ -250,7 +239,7 @@ class SendTronConfirmationViewModel { viewItems.append(.value(title: "send.confirmation.contact_name".localized, value: contactName, type: .regular)) } - return [SectionViewItem(viewItems: viewItems + addressActiveViewItems())] + return [SectionViewItem(viewItems: viewItems)] } private func eip20TransferItems(to: TronKit.Address, value: BigUInt, contractAddress: TronKit.Address) -> [SectionViewItem]? { @@ -284,7 +273,7 @@ class SendTronConfirmationViewModel { viewItems.append(.value(title: "send.confirmation.contact_name".localized, value: contactName, type: .regular)) } - return [SectionViewItem(viewItems: viewItems + addressActiveViewItems())] + return [SectionViewItem(viewItems: viewItems)] } private func coinService(token: MarketKit.Token) -> CoinService { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/SendTronModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/SendTronModule.swift index 7d0f7bdf1b..fc7ff8c19f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/SendTronModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/SendTronModule.swift @@ -3,7 +3,7 @@ import ThemeKit import UIKit enum SendTronModule { - static func viewController(token: Token, mode: SendBaseService.Mode, adapter: ISendTronAdapter) -> UIViewController { + static func viewController(token: Token, mode: PreSendViewModel.Mode, adapter: ISendTronAdapter) -> UIViewController { let tronAddressParserItem = TronAddressParser() let addressParserChain = AddressParserChain().append(handler: tronAddressParserItem) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/SendTronService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/SendTronService.swift index 8a803c952d..995207c68e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/SendTronService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/SendTronService.swift @@ -8,7 +8,7 @@ import TronKit class SendTronService { let sendToken: Token - let mode: SendBaseService.Mode + let mode: PreSendViewModel.Mode private let disposeBag = DisposeBag() private let adapter: ISendTronAdapter @@ -39,9 +39,7 @@ class SendTronService { } } - private let activeAddressRelay = PublishRelay() - - init(token: Token, mode: SendBaseService.Mode, adapter: ISendTronAdapter, addressService: AddressService, memoService: SendMemoInputService) { + init(token: Token, mode: PreSendViewModel.Mode, adapter: ISendTronAdapter, addressService: AddressService, memoService: SendMemoInputService) { sendToken = token self.mode = mode self.adapter = adapter @@ -53,7 +51,7 @@ class SendTronService { addressService.set(text: address) if let amount { addressService.publishAmountRelay.accept(amount) } case let .predefined(address): addressService.set(text: address) - case .send: () + case .regular: () } subscribe(disposeBag, addressService.stateObservable) { [weak self] in self?.sync(addressState: $0) } @@ -108,10 +106,6 @@ extension SendTronService { var addressErrorObservable: Observable { addressErrorRelay.asObservable() } - - var activeAddressObservable: Observable { - activeAddressRelay.asObservable() - } } extension SendTronService: IAvailableBalanceService { @@ -185,24 +179,6 @@ extension SendTronService: IAmountInputService { addressError = AddressError.ownAddress return } - - Single - .create { [weak self] observer in - let task = Task { [weak self] in - let active = await self?.adapter.accountActive(address: tronAddress) ?? false - observer(.success(active)) - } - - return Disposables.create { - task.cancel() - } - } - .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) - .observeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) - .subscribe(onSuccess: { [weak self] active in - self?.activeAddressRelay.accept(active) - }) - .disposed(by: disposeBag) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/SendTronViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/SendTronViewController.swift index d251d0e2d4..9513bb602f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/SendTronViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/SendTronViewController.swift @@ -64,7 +64,7 @@ class SendTronViewController: ThemeViewController { iconImageView.snp.makeConstraints { make in make.size.equalTo(CGFloat.iconSize24) } - iconImageView.setImage(withUrlString: viewModel.token.coin.imageUrl, placeholder: UIImage(named: viewModel.token.placeholderImageName)) + iconImageView.setImage(coin: viewModel.token.coin, placeholder: viewModel.token.placeholderImageName) view.addSubview(tableView) tableView.snp.makeConstraints { maker in diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/SendTronViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/SendTronViewModel.swift index f007e15c0a..d5cbc6262a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/SendTronViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/SendTronViewModel.swift @@ -59,14 +59,14 @@ class SendTronViewModel { extension SendTronViewModel { var title: String { switch service.mode { - case .send, .prefilled: return "send.title".localized(token.coin.code) + case .regular, .prefilled: return "send.title".localized(token.coin.code) case .predefined: return "donate.title".localized(token.coin.code) } } var showAddress: Bool { switch service.mode { - case .send, .prefilled: return true + case .regular, .prefilled: return true case .predefined: return false } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/TronRecipientAddressViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/TronRecipientAddressViewModel.swift index 0ceeb1dea7..890ea61807 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/TronRecipientAddressViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendTron/TronRecipientAddressViewModel.swift @@ -1,22 +1,11 @@ import RxSwift class TronRecipientAddressViewModel: RecipientAddressViewModel { - private let disposeBag = DisposeBag() private let sendService: SendTronService init(service: AddressService, handlerDelegate: IRecipientAddressService?, sendService: SendTronService) { self.sendService = sendService super.init(service: service, handlerDelegate: handlerDelegate) - - subscribe(disposeBag, sendService.activeAddressObservable) { [weak self] in self?.handle(active: $0) } - } - - private func handle(active: Bool) { - if active { - cautionRelay.accept(nil) - } else { - cautionRelay.accept(Caution(text: "tron.send.inactive_address".localized, type: .warning)) - } } override func sync(state: AddressService.State? = nil, customError: Error? = nil) { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutView.swift index 28e875e515..656ff8982c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/About/AboutView.swift @@ -8,95 +8,82 @@ struct AboutView: View { var body: some View { ScrollableThemeView { - VStack(spacing: .margin24) { - HStack(spacing: .margin16) { - Image(uiImage: UIImage(named: AppIcon.main.imageName) ?? UIImage()) - .resizable() - .scaledToFit() - .clipShape(RoundedRectangle(cornerRadius: .cornerRadius16, style: .continuous)) - .frame(width: 72, height: 72) - - VStack(spacing: .margin8) { - Text("settings.about_app.app_name".localized(AppConfig.appName)).themeHeadline1() - Text("version".localized(viewModel.appVersion)).themeSubhead2() - } - } - .padding(.horizontal, .margin24) - - Text("settings.about_app.description".localized(AppConfig.appName, AppConfig.appName)) - .font(.themeBody) - .foregroundColor(.themeBran) - .padding(.horizontal, .margin32) - .padding(.vertical, .margin12) - - VStack(spacing: .margin32) { - if let releaseNotesUrl = viewModel.releaseNotesUrl { - ListSection { - NavigationRow(destination: { - MarkdownModule.gitReleaseNotesMarkdownView(url: releaseNotesUrl, presented: false) - .ignoresSafeArea() - }) { - Image("circle_information_24").themeIcon() - Text("settings.about_app.whats_new".localized).themeBody() - Image.disclosureIcon - } - } - } - + VStack(spacing: .margin32) { + if let releaseNotesUrl = viewModel.releaseNotesUrl { ListSection { - NavigationRow(destination: { - AppStatusModule.view() + NavigationRow(spacing: .margin8, destination: { + MarkdownModule.gitReleaseNotesMarkdownView(url: releaseNotesUrl, presented: false) + .onFirstAppear { stat(page: .aboutApp, event: .open(page: .whatsNews)) } + .ignoresSafeArea() }) { - Image("app_status_24").themeIcon() - Text("app_status.title".localized).themeBody() + HStack(spacing: .margin16) { + Image("circle_information_24").themeIcon() + Text("settings.about_app.app_version".localized).textBody() + } + Spacer() + Text(viewModel.appVersion).textSubhead1() Image.disclosureIcon } + } + } - ClickableRow(action: { - termsPresented = true - }) { - Image("unordered_24").themeIcon() - Text("terms.title".localized).themeBody() + ListSection { + NavigationRow(destination: { + AppStatusModule.view() + .onFirstAppear { stat(page: .aboutApp, event: .open(page: .appStatus)) } + }) { + Image("app_status_24").themeIcon() + Text("app_status.title".localized).themeBody() + Image.disclosureIcon + } - if viewModel.termsAlert { - Image("warning_2_20").themeIcon(color: .themeLucian).padding(.trailing, -.margin8) - } + ClickableRow(action: { + stat(page: .aboutApp, event: .open(page: .terms)) + termsPresented = true + }) { + Image("unordered_24").themeIcon() + Text("terms.title".localized).themeBody() - Image.disclosureIcon + if viewModel.termsAlert { + Image("warning_2_20").themeIcon(color: .themeLucian).padding(.trailing, -.margin8) } - NavigationRow(destination: { - PrivacyPolicyView(config: .privacy) - .navigationTitle(PrivacyPolicyViewController.Config.privacy.title) - .ignoresSafeArea() - }) { - Image("user_24").themeIcon() - Text("settings.privacy".localized).themeBody() - Image.disclosureIcon - } + Image.disclosureIcon } - ListSection { - ClickableRow(action: { - linkUrl = URL(string: "https://github.com/\(AppConfig.appGitHubAccount)/\(AppConfig.appGitHubRepository)") - }) { - Image("github_24").themeIcon() - Text("GitHub").themeBody() - Image.disclosureIcon - } + NavigationRow(destination: { + PrivacyPolicyView(config: .privacy) + .navigationTitle(PrivacyPolicyViewController.Config.privacy.title) + .onFirstAppear { stat(page: .aboutApp, event: .open(page: .privacy)) } + .ignoresSafeArea() + }) { + Image("user_24").themeIcon() + Text("settings.privacy".localized).themeBody() + Image.disclosureIcon + } + } - ClickableRow(action: { - linkUrl = URL(string: AppConfig.appWebPageLink) - }) { - Image("globe_24").themeIcon() - Text("settings.about_app.website".localized).themeBody() - Image.disclosureIcon - } + ListSection { + ClickableRow(action: { + stat(page: .aboutApp, event: .open(page: .externalGithub)) + linkUrl = URL(string: "https://github.com/\(AppConfig.appGitHubAccount)/\(AppConfig.appGitHubRepository)") + }) { + Image("github_24").themeIcon() + Text("GitHub").themeBody() + Image.disclosureIcon + } + + ClickableRow(action: { + stat(page: .aboutApp, event: .open(page: .externalWebsite)) + linkUrl = URL(string: AppConfig.appWebPageLink) + }) { + Image("globe_24").themeIcon() + Text("settings.about_app.website".localized).themeBody() + Image.disclosureIcon } } - .padding(.horizontal, .margin16) } - .padding(EdgeInsets(top: .margin24, leading: 0, bottom: .margin32, trailing: 0)) + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) .sheet(isPresented: $termsPresented) { TermsModule.view() .ignoresSafeArea() @@ -107,5 +94,6 @@ struct AboutView: View { } } .navigationTitle("settings.about_app.title".localized) + .navigationBarTitleDisplayMode(.inline) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Appearance/AppearanceView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Appearance/AppearanceView.swift index 26bb9629b7..1939287bf2 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Appearance/AppearanceView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Appearance/AppearanceView.swift @@ -5,98 +5,125 @@ import ThemeKit struct AppearanceView: View { @StateObject var viewModel = AppearanceViewModel() + @State private var themeSelectorPresented = false + @State private var priceChangeSelectorPresented = false + @State private var launchScreenSelectorPresented = false + @State private var balanceValueSelectorPresented = false + var body: some View { ScrollableThemeView { VStack(spacing: .margin24) { - VStack(spacing: 0) { - ListSectionHeader(text: "appearance.theme".localized) - ListSection { - ForEach(viewModel.themeModes, id: \.self) { themeMode in - ClickableRow(action: { - viewModel.themMode = themeMode - }) { - icon(themeMode: themeMode).themeIcon() - Text(title(themeMode: themeMode)).themeBody() - - if viewModel.themMode == themeMode { - Image.checkIcon - } + ListSection { + ClickableRow(spacing: .margin8) { + themeSelectorPresented = true + } content: { + Text("appearance.theme".localized).textBody() + Spacer() + Text(title(themeMode: viewModel.themeMode)).textSubhead1() + Image("arrow_small_down_20").themeIcon() + } + .alert( + isPresented: $themeSelectorPresented, + title: "appearance.theme".localized, + viewItems: viewModel.themeModes.map { .init(text: title(themeMode: $0), selected: viewModel.themeMode == $0) }, + onTap: { index in + guard let index else { + return } + + viewModel.themeMode = viewModel.themeModes[index] } - } + ) } VStack(spacing: 0) { - ListSectionHeader(text: "appearance.tab_settings".localized) + ListSectionHeader(text: "appearance.markets_tab".localized) ListSection { ListRow { - Image("markets_24").themeIcon() - Toggle(isOn: $viewModel.showMarketTab.animation()) { - Text("appearance.markets_tab".localized).themeBody() + Toggle(isOn: $viewModel.hideMarkets.animation()) { + Text("appearance.hide_markets".localized).themeBody() } .toggleStyle(SwitchToggleStyle(tint: .themeYellow)) } - } - } - - if viewModel.showMarketTab { - VStack(spacing: 0) { - ListSectionHeader(text: "appearance.launch_screen".localized) - ListSection { - ForEach(LaunchScreen.allCases, id: \.self) { launchScreen in - ClickableRow(action: { - viewModel.launchScreen = launchScreen - }) { - Image(launchScreen.iconName).themeIcon() - Text(launchScreen.title).themeBody() - if viewModel.launchScreen == launchScreen { - Image.checkIcon - } + ClickableRow(spacing: .margin8) { + priceChangeSelectorPresented = true + } content: { + Text("appearance.price_change".localized).textBody() + Spacer() + Text(title(priceChangeMode: viewModel.priceChangeMode)).textSubhead1() + Image("arrow_small_down_20").themeIcon() + } + .alert( + isPresented: $priceChangeSelectorPresented, + title: "appearance.price_change".localized, + viewItems: PriceChangeMode.allCases.map { .init(text: title(priceChangeMode: $0), selected: viewModel.priceChangeMode == $0) }, + onTap: { index in + guard let index else { + return } + + viewModel.priceChangeMode = PriceChangeMode.allCases[index] } - } + ) } } - VStack(spacing: 0) { - ListSectionHeader(text: "appearance.balance_conversion".localized) + if !viewModel.hideMarkets { ListSection { - ForEach(viewModel.conversionTokens, id: \.self) { token in - ClickableRow(action: { - viewModel.conversionToken = token - }) { - KFImage.url(URL(string: token.coin.imageUrl)) - .resizable() - .frame(width: .iconSize32, height: .iconSize32) - - Text(token.coin.code).themeBody() - - if viewModel.conversionToken == token { - Image.checkIcon + ClickableRow(spacing: .margin8) { + launchScreenSelectorPresented = true + } content: { + Text("appearance.launch_screen".localized).textBody() + Spacer() + Text(viewModel.launchScreen.title).textSubhead1() + Image("arrow_small_down_20").themeIcon() + } + .alert( + isPresented: $launchScreenSelectorPresented, + title: "appearance.launch_screen".localized, + viewItems: LaunchScreen.allCases.map { .init(text: $0.title, selected: viewModel.launchScreen == $0) }, + onTap: { index in + guard let index else { + return } + + viewModel.launchScreen = LaunchScreen.allCases[index] } - } + ) } } VStack(spacing: 0) { - ListSectionHeader(text: "appearance.balance_value".localized) + ListSectionHeader(text: "appearance.balance_tab".localized) ListSection { - ForEach(BalancePrimaryValue.allCases, id: \.self) { balancePrimaryValue in - ClickableRow(action: { - viewModel.balancePrimaryValue = balancePrimaryValue - }) { - VStack(spacing: 1) { - Text(balancePrimaryValue.title).themeBody() - Text(balancePrimaryValue.subtitle).themeSubhead2() - } + ListRow { + Toggle(isOn: $viewModel.hideBalanceButtons.animation()) { + Text("appearance.hide_buttons".localized).themeBody() + } + .toggleStyle(SwitchToggleStyle(tint: .themeYellow)) + } - if viewModel.balancePrimaryValue == balancePrimaryValue { - Image.checkIcon + ClickableRow(spacing: .margin8) { + balanceValueSelectorPresented = true + } content: { + Text("appearance.balance_value".localized).textBody() + Spacer() + Text(title(balancePrimaryValue: viewModel.balancePrimaryValue)).textSubhead1() + Image("arrow_small_down_20").themeIcon() + } + .alert( + isPresented: $balanceValueSelectorPresented, + title: "appearance.balance_value".localized, + viewItems: BalancePrimaryValue.allCases.map { .init(text: title(balancePrimaryValue: $0), selected: viewModel.balancePrimaryValue == $0) }, + onTap: { index in + guard let index else { + return } + + viewModel.balancePrimaryValue = BalancePrimaryValue.allCases[index] } - } + ) } } @@ -127,6 +154,7 @@ struct AppearanceView: View { .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) } .navigationTitle("appearance.title".localized) + .navigationBarTitleDisplayMode(.inline) } func title(themeMode: ThemeMode) -> String { @@ -137,11 +165,17 @@ struct AppearanceView: View { } } - func icon(themeMode: ThemeMode) -> Image { - switch themeMode { - case .system: return Image("settings_24") - case .dark: return Image("dark_24") - case .light: return Image("light_24") + func title(balancePrimaryValue: BalancePrimaryValue) -> String { + switch balancePrimaryValue { + case .coin: return "appearance.balance_value.coin_fiat".localized + case .currency: return "appearance.balance_value.fiat_coin".localized + } + } + + func title(priceChangeMode: PriceChangeMode) -> String { + switch priceChangeMode { + case .hour24: return "appearance.price_change.24h".localized + case .day1: return "appearance.price_change.1d".localized } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Appearance/AppearanceViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Appearance/AppearanceViewModel.swift index 72c56c4e0d..b048e1f493 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Appearance/AppearanceViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Appearance/AppearanceViewModel.swift @@ -1,3 +1,4 @@ +import Combine import MarketKit import SwiftUI import ThemeKit @@ -7,54 +8,87 @@ class AppearanceViewModel: ObservableObject { private let launchScreenManager = App.shared.launchScreenManager private let appIconManager = App.shared.appIconManager private let balancePrimaryValueManager = App.shared.balancePrimaryValueManager - private let balanceConversionManager = App.shared.balanceConversionManager + private let walletButtonHiddenManager = App.shared.walletButtonHiddenManager + private let priceChangeModeManager = App.shared.priceChangeModeManager let themeModes: [ThemeMode] = [.system, .dark, .light] - let conversionTokens: [Token] - @Published var themMode: ThemeMode { + @Published var themeMode: ThemeMode { didSet { - themeManager.themeMode = themMode + guard themeManager.themeMode != themeMode else { + return + } + stat(page: .appearance, event: .selectTheme(type: themeMode.rawValue)) + themeManager.themeMode = themeMode } } - @Published var showMarketTab: Bool { + @Published var hideMarkets: Bool { didSet { - launchScreenManager.showMarket = showMarketTab + guard launchScreenManager.showMarket == hideMarkets else { + return + } + stat(page: .appearance, event: .showMarketsTab(shown: !hideMarkets)) + launchScreenManager.showMarket = !hideMarkets + } + } + + @Published var priceChangeMode: PriceChangeMode { + didSet { + guard priceChangeModeManager.priceChangeMode != priceChangeMode else { + return + } + stat(page: .appearance, event: .showMarketsTab(shown: !hideMarkets)) + priceChangeModeManager.priceChangeMode = priceChangeMode } } @Published var launchScreen: LaunchScreen { didSet { + guard launchScreenManager.launchScreen != launchScreen else { + return + } + stat(page: .appearance, event: .selectLaunchScreen(type: launchScreen.statType)) launchScreenManager.launchScreen = launchScreen } } - @Published var conversionToken: Token? { + @Published var hideBalanceButtons: Bool { didSet { - balanceConversionManager.set(conversionToken: conversionToken) + guard walletButtonHiddenManager.buttonHidden != hideBalanceButtons else { + return + } + stat(page: .appearance, event: .hideBalanceButtons(hide: hideBalanceButtons)) + walletButtonHiddenManager.buttonHidden = hideBalanceButtons } } @Published var balancePrimaryValue: BalancePrimaryValue { didSet { + guard balancePrimaryValueManager.balancePrimaryValue != balancePrimaryValue else { + return + } + stat(page: .appearance, event: .selectBalanceValue(type: balancePrimaryValue.rawValue)) balancePrimaryValueManager.balancePrimaryValue = balancePrimaryValue } } @Published var appIcon: AppIcon { didSet { + guard appIconManager.appIcon != appIcon else { + return + } + stat(page: .appearance, event: .selectAppIcon(iconUid: appIcon.title.lowercased())) appIconManager.appIcon = appIcon } } init() { - conversionTokens = balanceConversionManager.conversionTokens - - themMode = themeManager.themeMode - showMarketTab = launchScreenManager.showMarket + themeMode = themeManager.themeMode + hideMarkets = !launchScreenManager.showMarket + priceChangeMode = priceChangeModeManager.priceChangeMode launchScreen = launchScreenManager.launchScreen - conversionToken = balanceConversionManager.conversionToken + hideBalanceButtons = walletButtonHiddenManager.buttonHidden balancePrimaryValue = balancePrimaryValueManager.balancePrimaryValue appIcon = appIconManager.appIcon } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupAppViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupAppViewModel.swift index 0d12ce176b..0c2b28e978 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupAppViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupAppViewModel.swift @@ -7,7 +7,7 @@ class BackupAppViewModel: ObservableObject { private let accountManager = App.shared.accountManager private let contactManager = App.shared.contactManager private let cloudBackupManager = App.shared.cloudBackupManager - private let favoritesManager = App.shared.favoritesManager + private let watchlistManager = App.shared.watchlistManager private let evmSyncSourceManager = App.shared.evmSyncSourceManager private var cancellables = Set() @@ -149,7 +149,7 @@ extension BackupAppViewModel { return BackupAppModule.items( watchAccountCount: accounts(watch: true).count, - watchlistCount: favoritesManager.allCoinUids.count, + watchlistCount: watchlistManager.coinUids.count, contactAddressCount: contacts.count, blockchainSourcesCount: evmSyncSourceManager.customSyncSources(blockchainType: nil).count ) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupDisclaimer/BackupDisclaimerView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupDisclaimer/BackupDisclaimerView.swift index 3374133537..e307af1755 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupDisclaimer/BackupDisclaimerView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupDisclaimer/BackupDisclaimerView.swift @@ -12,21 +12,23 @@ struct BackupDisclaimerView: View { ThemeView { BottomGradientWrapper { - VStack(spacing: .margin32) { - HighlightedTextView(text: backupDisclaimer.highlightedDescription, style: .warning) - ListSection { - ClickableRow(action: { - isOn.toggle() - }) { - Toggle(isOn: $isOn) {} - .labelsHidden() - .toggleStyle(CheckboxStyle()) + ScrollView { + VStack(spacing: .margin32) { + HighlightedTextView(text: backupDisclaimer.highlightedDescription, style: .warning) + ListSection { + ClickableRow(action: { + isOn.toggle() + }) { + Toggle(isOn: $isOn) {} + .labelsHidden() + .toggleStyle(CheckboxStyle()) - Text(backupDisclaimer.selectedCheckboxText).themeSubhead2(color: .themeLeah) + Text(backupDisclaimer.selectedCheckboxText).themeSubhead2(color: .themeLeah) + } } } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) } - .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) } bottomContent: { NavigationLink( destination: BackupNameView(viewModel: viewModel, onDismiss: onDismiss), diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupList/BackupListView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupList/BackupListView.swift index 6c954877fa..0e472194cd 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupList/BackupListView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupList/BackupListView.swift @@ -8,62 +8,64 @@ struct BackupListView: View { var body: some View { ThemeView { BottomGradientWrapper { - VStack(spacing: .margin24) { - if !viewModel.accountItems.isEmpty { - VStack(spacing: 0) { - ListSectionHeader(text: "backup_app.backup_list.header.wallets".localized) + ScrollView { + VStack(spacing: .margin24) { + if !viewModel.accountItems.isEmpty { + VStack(spacing: 0) { + ListSectionHeader(text: "backup_app.backup_list.header.wallets".localized) - ListSection { - ForEach(viewModel.accountItems, id: \.accountId) { (item: BackupAppModule.AccountItem) in - if viewModel.selected[item.id] != nil { - let selected = binding(for: item.accountId) + ListSection { + ForEach(viewModel.accountItems, id: \.accountId) { (item: BackupAppModule.AccountItem) in + if viewModel.selected[item.id] != nil { + let selected = binding(for: item.accountId) - ClickableRow(action: { - viewModel.toggle(item: item) - }) { - HStack { - AccountView(item: item) + ClickableRow(action: { + viewModel.toggle(item: item) + }) { + HStack { + AccountView(item: item) - Toggle(isOn: selected) {} - .labelsHidden() - .toggleStyle(CheckboxStyle()) + Toggle(isOn: selected) {} + .labelsHidden() + .toggleStyle(CheckboxStyle()) + } + } + } else { + ListRow { + AccountView(item: item) } - } - } else { - ListRow { - AccountView(item: item) } } } } } - } - VStack(spacing: 0) { - ListSectionHeader(text: "backup_app.backup_list.header.other".localized) + VStack(spacing: 0) { + ListSectionHeader(text: "backup_app.backup_list.header.other".localized) - ListSection { - ForEach(viewModel.otherItems) { (item: BackupAppModule.Item) in - ListRow { - VStack(spacing: 1) { - HStack { - Text(item.title).themeBody() + ListSection { + ForEach(viewModel.otherItems) { (item: BackupAppModule.Item) in + ListRow { + VStack(spacing: 1) { + HStack { + Text(item.title).themeBody() - if let value = item.value { - Text(value).themeSubhead1(alignment: .trailing) + if let value = item.value { + Text(value).themeSubhead1(alignment: .trailing) + } } - } - if let description = item.description { - Text(description).themeSubhead2() + if let description = item.description { + Text(description).themeSubhead2() + } } } } } } } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) } - .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) } bottomContent: { NavigationLink( destination: BackupDisclaimerView(viewModel: viewModel, onDismiss: onDismiss), diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupName/BackupNameView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupName/BackupNameView.swift index e46de5d6f6..f64e38ef12 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupName/BackupNameView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupName/BackupNameView.swift @@ -8,24 +8,26 @@ struct BackupNameView: View { var body: some View { ThemeView { BottomGradientWrapper { - VStack(spacing: .margin24) { - Text("backup_app.backup.name.description".localized) - .themeSubhead2() - .padding(EdgeInsets(top: 0, leading: .margin16, bottom: .margin12, trailing: .margin16)) + ScrollView { + VStack(spacing: .margin24) { + Text("backup_app.backup.name.description".localized) + .themeSubhead2() + .padding(EdgeInsets(top: 0, leading: .margin16, bottom: .margin12, trailing: .margin16)) - InputTextRow { - InputTextView( - placeholder: "backup.cloud.name.placeholder".localized, - text: $viewModel.name - ) - .autocapitalization(.words) - .autocorrectionDisabled() + InputTextRow { + InputTextView( + placeholder: "backup.cloud.name.placeholder".localized, + text: $viewModel.name + ) + .autocapitalization(.words) + .autocorrectionDisabled() + } + .modifier(CautionBorder(cautionState: $viewModel.nameCautionState)) + .modifier(CautionPrompt(cautionState: $viewModel.nameCautionState)) } - .modifier(CautionBorder(cautionState: $viewModel.nameCautionState)) - .modifier(CautionPrompt(cautionState: $viewModel.nameCautionState)) + .animation(.default, value: viewModel.nameCautionState) + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) } - .animation(.default, value: viewModel.nameCautionState) - .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) } bottomContent: { NavigationLink( destination: BackupPasswordView(viewModel: viewModel, onDismiss: onDismiss), diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupPassword/BackupPasswordView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupPassword/BackupPasswordView.swift index f3734d8f69..79a6929c75 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupPassword/BackupPasswordView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupPassword/BackupPasswordView.swift @@ -11,45 +11,47 @@ struct BackupPasswordView: View { var body: some View { ThemeView { BottomGradientWrapper { - VStack(spacing: .margin32) { - Text("backup_app.backup.password.description".localized) - .themeSubhead2() - .padding(EdgeInsets(top: 0, leading: .margin16, bottom: 0, trailing: .margin16)) + ScrollView { + VStack(spacing: .margin32) { + Text("backup_app.backup.password.description".localized) + .themeSubhead2() + .padding(EdgeInsets(top: 0, leading: .margin16, bottom: 0, trailing: .margin16)) - VStack(spacing: .margin16) { - InputTextRow { - InputTextView( - placeholder: "backup.cloud.password.placeholder".localized, - text: $viewModel.password, - isValidText: { text in PassphraseValidator.validate(text: text) } - ) - .secure($secureLock) - .autocapitalization(.none) - .autocorrectionDisabled() - } - .modifier(CautionBorder(cautionState: $viewModel.passwordCautionState)) - .modifier(CautionPrompt(cautionState: $viewModel.passwordCautionState)) + VStack(spacing: .margin16) { + InputTextRow { + InputTextView( + placeholder: "backup.cloud.password.placeholder".localized, + text: $viewModel.password, + isValidText: { text in PassphraseValidator.validate(text: text) } + ) + .secure($secureLock) + .autocapitalization(.none) + .autocorrectionDisabled() + } + .modifier(CautionBorder(cautionState: $viewModel.passwordCautionState)) + .modifier(CautionPrompt(cautionState: $viewModel.passwordCautionState)) - InputTextRow { - InputTextView( - placeholder: "backup.cloud.password.confirm.placeholder".localized, - text: $viewModel.confirm, - isValidText: { text in PassphraseValidator.validate(text: text) } - ) - .secure($secureLock) - .autocapitalization(.none) - .autocorrectionDisabled() + InputTextRow { + InputTextView( + placeholder: "backup.cloud.password.confirm.placeholder".localized, + text: $viewModel.confirm, + isValidText: { text in PassphraseValidator.validate(text: text) } + ) + .secure($secureLock) + .autocapitalization(.none) + .autocorrectionDisabled() + } + .modifier(CautionBorder(cautionState: $viewModel.confirmCautionState)) + .modifier(CautionPrompt(cautionState: $viewModel.confirmCautionState)) } - .modifier(CautionBorder(cautionState: $viewModel.confirmCautionState)) - .modifier(CautionPrompt(cautionState: $viewModel.confirmCautionState)) - } - .animation(.default, value: secureLock) + .animation(.default, value: secureLock) - HighlightedTextView(text: "backup_app.backup.password.highlighted_description".localized, style: .warning) + HighlightedTextView(text: "backup_app.backup.password.highlighted_description".localized, style: .warning) + } + .animation(.default, value: viewModel.passwordCautionState) + .animation(.default, value: viewModel.confirmCautionState) + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) } - .animation(.default, value: viewModel.passwordCautionState) - .animation(.default, value: viewModel.confirmCautionState) - .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) } bottomContent: { Button(action: { viewModel.onTapSave() diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupType/BackupTypeView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupType/BackupTypeView.swift index e4d85938cd..bffbd1f633 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupType/BackupTypeView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BackupApp/Backup/BackupType/BackupTypeView.swift @@ -81,7 +81,7 @@ struct BackupTypeView: View { NavigationRow( destination: { BackupListView(viewModel: viewModel, onDismiss: onDismiss) - .modifier(FirstAppear { stat(page: .exportFull, event: .open(page: statPage)) }) + .onFirstAppear { stat(page: .exportFull, event: .open(page: statPage)) } }, isActive: isActive ) { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BaseCurrency/BaseCurrencySettingsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BaseCurrency/BaseCurrencySettingsViewModel.swift index 19e260bf3d..77170b1a5c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BaseCurrency/BaseCurrencySettingsViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BaseCurrency/BaseCurrencySettingsViewModel.swift @@ -8,6 +8,7 @@ class BaseCurrencySettingsViewModel: ObservableObject { var baseCurrency: Currency { didSet { + stat(page: .baseCurrency, event: .switchBaseCurrency(code: baseCurrency.code)) currencyManager.baseCurrency = baseCurrency } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BlockchainSettings/BlockchainSettingsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BlockchainSettings/BlockchainSettingsView.swift index 4f1ce48adc..bd9c51dc85 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BlockchainSettings/BlockchainSettingsView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BlockchainSettings/BlockchainSettingsView.swift @@ -41,7 +41,7 @@ struct BlockchainSettingsView: View { } } .sheet(item: $evmSheetBlockchain) { blockchain in - EvmNetworkView(blockchain: blockchain) + EvmNetworkView(blockchain: blockchain).ignoresSafeArea() } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift index 1f5ef2a973..10c6dde717 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift @@ -50,8 +50,10 @@ class MainSettingsViewController: ThemeViewController { super.viewDidLoad() title = "settings.title".localized + navigationItem.largeTitleDisplayMode = .never navigationItem.backBarButtonItem = UIBarButtonItem(title: title, style: .plain, target: nil, action: nil) + tableView.registerHeaderFooter(forClass: HighlightedSubtitleHeaderFooterView.self) tableView.sectionDataSource = self tableView.separatorStyle = .none @@ -65,7 +67,7 @@ class MainSettingsViewController: ThemeViewController { manageAccountsCell.set(backgroundStyle: .lawrence, isFirst: true) syncManageAccountCell() - walletConnectCell.set(backgroundStyle: .lawrence, isFirst: true, isLast: true) + walletConnectCell.set(backgroundStyle: .lawrence) syncWalletConnectCell() securityCell.set(backgroundStyle: .lawrence, isFirst: true) @@ -226,7 +228,6 @@ class MainSettingsViewController: ThemeViewController { image: .local(UIImage(named: "blocks_24")), title: .body("settings.blockchain_settings".localized), accessoryType: .disclosure, - isLast: false, action: { [weak self] in let viewController = BlockchainSettingsModule.view().toViewController(title: "blockchain_settings.title".localized) self?.navigationController?.pushViewController(viewController, animated: true) @@ -234,11 +235,21 @@ class MainSettingsViewController: ThemeViewController { stat(page: .settings, event: .open(page: .blockchainSettings)) } ), + StaticRow( + cell: walletConnectCell, + id: "wallet-connect", + height: .heightCell48, + autoDeselect: true, + action: { [weak self] in + self?.viewModel.onTapWalletConnect() + } + ), tableView.universalRow48( id: "backup-manager", image: .local(UIImage(named: "icloud_24")), title: .body("settings.backup_manager".localized), accessoryType: .disclosure, + isFirst: false, isLast: true, action: { [weak self] in let viewController = BackupManagerModule.viewController() @@ -250,20 +261,6 @@ class MainSettingsViewController: ThemeViewController { ] } - private var walletConnectRows: [RowProtocol] { - [ - StaticRow( - cell: walletConnectCell, - id: "wallet-connect", - height: .heightCell48, - autoDeselect: true, - action: { [weak self] in - self?.viewModel.onTapWalletConnect() - } - ), - ] - } - private var appearanceRows: [RowProtocol] { [ StaticRow( @@ -358,7 +355,7 @@ class MainSettingsViewController: ThemeViewController { [ tableView.universalRow48( id: "telegram", - image: .local(UIImage(named: "telegram_24")), + image: .local(UIImage(named: "filled_telegram_24")?.withTintColor(.themeJacob)), title: .body("Telegram"), accessoryType: .disclosure, autoDeselect: true, @@ -371,7 +368,7 @@ class MainSettingsViewController: ThemeViewController { ), tableView.universalRow48( id: "twitter", - image: .local(UIImage(named: "twitter_24")), + image: .local(UIImage(named: "filled_twitter_24")?.withTintColor(.themeJacob)), title: .body("Twitter"), accessoryType: .disclosure, autoDeselect: true, @@ -526,10 +523,20 @@ extension MainSettingsViewController: SectionsDataSource { func buildSections() -> [SectionProtocol] { var sections: [SectionProtocol] = [ Section(id: "account", headerState: .margin(height: AppConfig.donateEnabled ? .margin32 : .margin12), rows: accountRows), - Section(id: "wallet_connect", headerState: .margin(height: .margin32), rows: walletConnectRows), - Section(id: "appearance_settings", headerState: .margin(height: .margin32), rows: appearanceRows), + Section(id: "appearance_settings", headerState: .margin(height: .margin32), footerState: .margin(height: .margin24), rows: appearanceRows), + Section( + id: "social", + headerState: .cellType( + hash: "settings.social_networks.label".localized, + binder: { (view: HighlightedSubtitleHeaderFooterView) in + view.bind(text: "settings.social_networks.label".localized, color: .themeJacob, backgroundColor: UIColor.clear) + }, + dynamicHeight: { _ in .margin32 } + ), + footerState: tableView.sectionFooter(text: "settings.social_networks.footer".localized, topMargin: .margin12, bottomMargin: .zero), + rows: socialRows + ), Section(id: "knowledge", headerState: .margin(height: .margin32), rows: knowledgeRows), - Section(id: "social", headerState: .margin(height: .margin32), rows: socialRows), Section(id: "about", headerState: .margin(height: .margin32), rows: aboutRows), Section(id: "footer", headerState: .margin(height: .margin32), footerState: .margin(height: .margin32), rows: footerRows), ] @@ -544,6 +551,17 @@ extension MainSettingsViewController: SectionsDataSource { id: "test-net-switcher", footerState: .margin(height: .margin32), rows: [ + tableView.universalRow48( + id: "new-send-switcher", + title: .body("New Send"), + accessoryType: .switch( + isOn: App.shared.localStorage.newSendEnabled, + onSwitch: { enabled in + App.shared.localStorage.newSendEnabled = enabled + } + ), + isFirst: true + ), tableView.universalRow48( id: "test-net-switcher", title: .body("TestNet Enabled"), @@ -553,7 +571,6 @@ extension MainSettingsViewController: SectionsDataSource { App.shared.testNetManager.set(testNetEnabled: enabled) } ), - isFirst: true, isLast: true ), ] @@ -570,3 +587,33 @@ extension MainSettingsViewController: MFMailComposeViewControllerDelegate { controller.dismiss(animated: true) } } + +class HighlightedSubtitleHeaderFooterView: UITableViewHeaderFooterView { + private let label = UILabel() + + override public init(reuseIdentifier: String?) { + super.init(reuseIdentifier: reuseIdentifier) + + backgroundView = UIView() + + addSubview(label) + label.snp.makeConstraints { maker in + maker.leading.trailing.equalToSuperview().inset(CGFloat.margin32) + maker.centerY.equalToSuperview() + } + + label.font = .subhead1 + label.textColor = .themeGray + } + + @available(*, unavailable) + public required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func bind(text: String?, color: UIColor = .clear, backgroundColor: UIColor = .clear) { + label.text = text?.uppercased() + label.textColor = color + backgroundView?.backgroundColor = backgroundColor + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Privacy/PrivacyPolicyViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Privacy/PrivacyPolicyViewController.swift index af7b57ee0b..8b707f0488 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Privacy/PrivacyPolicyViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Privacy/PrivacyPolicyViewController.swift @@ -55,16 +55,40 @@ class PrivacyPolicyViewController: ThemeViewController { return [ Section( id: "privacy-section", - footerState: .margin(height: .margin32), + footerState: .margin(height: .margin16), rows: infoRows ), ] } + + private var statAllowedSections: [SectionProtocol] { + [ + Section( + id: "stat-allowed-section", + footerState: .margin(height: .margin32), + rows: [ + tableView.universalRow48( + id: "stat-allowed-cell", + image: .local(UIImage(named: "share_1_24")), + title: .body("settings.privacy.allow".localized), + accessoryType: .switch( + isOn: App.shared.statManager.allowed, + onSwitch: { enabled in + App.shared.statManager.allowed = enabled + } + ), + isFirst: true, + isLast: true + ), + ] + ), + ] + } } extension PrivacyPolicyViewController: SectionsDataSource { func buildSections() -> [SectionProtocol] { - privacySections + privacySections + statAllowedSections } } @@ -79,9 +103,8 @@ extension PrivacyPolicyViewController { title: "settings.privacy".localized, description: "settings.privacy.description".localized(AppConfig.appName), viewItems: [ - "settings.privacy.statement.user_data_storage".localized, "settings.privacy.statement.data_usage".localized, - "settings.privacy.statement.data_privacy".localized, + "settings.privacy.statement.data_storage".localized, "settings.privacy.statement.user_account".localized, ] ) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Swap/CoinCard/SwapCoinCardCell.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Swap/CoinCard/SwapCoinCardCell.swift index 18c17dc9a3..252529948e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Swap/CoinCard/SwapCoinCardCell.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Swap/CoinCard/SwapCoinCardCell.swift @@ -96,7 +96,7 @@ class SwapCoinCardCell: UITableViewCell { maker.height.equalTo(formAmountInput.viewHeight) } - amountInputWrapper.layer.cornerRadius = .cornerRadius8 + amountInputWrapper.layer.cornerRadius = AmountInputCell.cornerRadius amountInputWrapper.layer.cornerCurve = .continuous amountInputWrapper.layer.borderWidth = .heightOnePixel amountInputWrapper.layer.borderColor = UIColor.themeSteel20.cgColor @@ -145,11 +145,7 @@ extension SwapCoinCardCell { } private func set(tokenViewItem: SwapCoinCardViewModel.TokenViewItem?) { - if let urlString = tokenViewItem?.iconUrlString { - tokenIconImageView.setImage(withUrlString: urlString, placeholder: tokenViewItem.flatMap { UIImage(named: $0.placeholderIconName) }) - } else { - tokenIconImageView.image = tokenViewItem.flatMap { UIImage(named: $0.placeholderIconName) } - } + tokenIconImageView.setImage(coin: tokenViewItem?.coin, placeholder: tokenViewItem?.placeholderIconName) if let tokenViewItem { tokenSelectButton.setTitle(tokenViewItem.title, for: .normal) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Swap/CoinCard/SwapCoinCardViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Swap/CoinCard/SwapCoinCardViewModel.swift index 704ed64a9d..10709dde3f 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Swap/CoinCard/SwapCoinCardViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Swap/CoinCard/SwapCoinCardViewModel.swift @@ -53,7 +53,7 @@ class SwapCoinCardViewModel { } private func sync(token: MarketKit.Token?) { - tokenViewItemRelay.accept(token.map { TokenViewItem(title: $0.coin.code, iconUrlString: $0.coin.imageUrl, placeholderIconName: $0.placeholderImageName) }) + tokenViewItemRelay.accept(token.map { TokenViewItem(title: $0.coin.code, coin: $0.coin, placeholderIconName: $0.placeholderImageName) }) } private func sync(balance: Decimal?) { @@ -119,7 +119,7 @@ extension SwapCoinCardViewModel { extension SwapCoinCardViewModel { struct TokenViewItem { let title: String - let iconUrlString: String + let coin: Coin let placeholderIconName: String } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SwapNew/InputCard/SwapInputCardView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SwapNew/InputCard/SwapInputCardView.swift index ecfe0d35db..f877361b27 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SwapNew/InputCard/SwapInputCardView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SwapNew/InputCard/SwapInputCardView.swift @@ -138,11 +138,7 @@ extension SwapInputCardView { } private func set(tokenViewItem: SwapCoinCardViewModel.TokenViewItem?) { - if let urlString = tokenViewItem?.iconUrlString { - tokenSelectView.tokenImage.setImage(urlString: urlString, placeholder: tokenViewItem.flatMap { UIImage(named: $0.placeholderIconName) }) - } else { - tokenSelectView.tokenImage.imageView.image = tokenViewItem.flatMap { UIImage(named: $0.placeholderIconName) } ?? UIImage(named: "placeholder_circle_32") - } + tokenSelectView.tokenImage.imageView.setImage(coin: tokenViewItem?.coin, placeholder: tokenViewItem?.placeholderIconName) if let tokenViewItem { tokenSelectView.tokenButton.setTitle(tokenViewItem.title, for: .normal) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SwitchAccount/SwitchAccountViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SwitchAccount/SwitchAccountViewController.swift index 5c9c538aef..7a37e104ea 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SwitchAccount/SwitchAccountViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SwitchAccount/SwitchAccountViewController.swift @@ -58,7 +58,7 @@ extension SwitchAccountViewController: SectionsDataSource { CellBuilderNew.row( rootElement: .hStack([ .image24 { component in - component.imageView.image = viewItem.selected ? UIImage(named: "circle_radioon_24")?.withTintColor(.themeJacob) : UIImage(named: "circle_radiooff_24")?.withTintColor(.themeGray) + component.imageView.image = viewItem.selected ? UIImage(named: "circle_radioon_24") : UIImage(named: "circle_radiooff_24") }, .vStackCentered([ .text { component in diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TransactionInfo/TransactionInfoViewItemFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TransactionInfo/TransactionInfoViewItemFactory.swift index 96b9f27247..8f9fc89e22 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/TransactionInfo/TransactionInfoViewItemFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TransactionInfo/TransactionInfoViewItemFactory.swift @@ -44,7 +44,7 @@ class TransactionInfoViewItemFactory { subtitle: subtitle, iconUrl: iconUrl, iconPlaceholderImageName: iconPlaceholderImageName, - coinAmount: balanceHidden ? BalanceHiddenManager.placeholder : transactionValue.formattedFull(showSign: type.showSign) ?? "n/a".localized, + coinAmount: balanceHidden ? BalanceHiddenManager.placeholder : transactionValue.formattedFull(signType: type.signType) ?? "n/a".localized, currencyAmount: balanceHidden ? BalanceHiddenManager.placeholder : currencyValue.flatMap { ValueFormatter.instance.formatFull(currencyValue: $0) }, type: type, coinUid: transactionValue.coin?.uid @@ -56,7 +56,7 @@ class TransactionInfoViewItemFactory { .nftAmount( iconUrl: metadata?.previewImageUrl, iconPlaceholderImageName: "placeholder_nft_32", - nftAmount: balanceHidden ? BalanceHiddenManager.placeholder : transactionValue.formattedFull(showSign: type.showSign) ?? "n/a".localized, + nftAmount: balanceHidden ? BalanceHiddenManager.placeholder : transactionValue.formattedFull(signType: type.signType) ?? "n/a".localized, type: type, providerCollectionUid: metadata?.providerCollectionUid, nftUid: metadata?.nftUid diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Transactions/TransactionFilter/TransactionTokenSelectView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Transactions/TransactionFilter/TransactionTokenSelectView.swift index 17addfbb82..aaf4883c77 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Transactions/TransactionFilter/TransactionTokenSelectView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Transactions/TransactionFilter/TransactionTokenSelectView.swift @@ -36,10 +36,7 @@ struct TransactionTokenSelectView: View { viewModel.set(currentToken: token) presentationMode.wrappedValue.dismiss() }) { - KFImage.url(URL(string: token.coin.imageUrl)) - .resizable() - .placeholder { Image(token.placeholderImageName) } - .frame(width: .iconSize32, height: .iconSize32) + CoinIconView(coin: token.coin, placeholderImage: Image(token.placeholderImageName)) VStack(spacing: 1) { HStack(spacing: .margin8) { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Transactions/TransactionsViewItemFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Transactions/TransactionsViewItemFactory.swift index 26b861a159..b67448d626 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Transactions/TransactionsViewItemFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Transactions/TransactionsViewItemFactory.swift @@ -25,8 +25,8 @@ class TransactionsViewItemFactory { contactLabelService.contactData(for: address, blockchainType: blockchainType).name ?? evmLabelManager.mapped(address: address) } - private func coinString(from transactionValue: TransactionValue, showSign: Bool = true) -> String { - guard let value = transactionValue.formattedShort(showSign: showSign) else { + private func coinString(from transactionValue: TransactionValue, signType: ValueFormatter.SignType = .always) -> String { + guard let value = transactionValue.formattedShort(signType: signType) else { return "n/a".localized } @@ -192,7 +192,7 @@ class TransactionsViewItemFactory { title = "transactions.send".localized subTitle = "transactions.to".localized(mapped(address: record.to, blockchainType: item.record.source.blockchainType)) - primaryValue = BaseTransactionsViewModel.Value(text: coinString(from: record.value, showSign: !record.sentToSelf), type: type(value: record.value, condition: record.sentToSelf, .neutral, .outgoing)) + primaryValue = BaseTransactionsViewModel.Value(text: coinString(from: record.value, signType: record.sentToSelf ? .never : .always), type: type(value: record.value, condition: record.sentToSelf, .neutral, .outgoing)) secondaryValue = singleValueSecondaryValue(value: record.value, currencyValue: item.currencyValue, nftMetadata: item.nftMetadata) sentToSelf = record.sentToSelf @@ -229,7 +229,7 @@ class TransactionsViewItemFactory { primaryValue = BaseTransactionsViewModel.Value(text: "∞ \(record.value.coinCode)", type: .neutral) secondaryValue = BaseTransactionsViewModel.Value(text: "transactions.value.unlimited".localized, type: .secondary) } else { - primaryValue = BaseTransactionsViewModel.Value(text: coinString(from: record.value, showSign: false), type: .neutral) + primaryValue = BaseTransactionsViewModel.Value(text: coinString(from: record.value, signType: .never), type: .neutral) if let currencyValue = item.currencyValue { secondaryValue = BaseTransactionsViewModel.Value(text: currencyString(from: currencyValue), type: .secondary) @@ -290,7 +290,7 @@ class TransactionsViewItemFactory { title = "transactions.send".localized subTitle = record.to.flatMap { "transactions.to".localized(mapped(address: $0, blockchainType: item.record.source.blockchainType)) } ?? "---" - primaryValue = BaseTransactionsViewModel.Value(text: coinString(from: record.value, showSign: !record.sentToSelf), type: type(value: record.value, condition: record.sentToSelf, .neutral, .outgoing)) + primaryValue = BaseTransactionsViewModel.Value(text: coinString(from: record.value, signType: record.sentToSelf ? .never : .always), type: type(value: record.value, condition: record.sentToSelf, .neutral, .outgoing)) if let currencyValue = item.currencyValue { secondaryValue = BaseTransactionsViewModel.Value(text: currencyString(from: currencyValue), type: .secondary) @@ -317,7 +317,7 @@ class TransactionsViewItemFactory { title = "transactions.send".localized subTitle = "transactions.to".localized(mapped(address: record.to, blockchainType: item.record.source.blockchainType)) - primaryValue = BaseTransactionsViewModel.Value(text: coinString(from: record.value, showSign: !record.sentToSelf), type: type(value: record.value, condition: record.sentToSelf, .neutral, .outgoing)) + primaryValue = BaseTransactionsViewModel.Value(text: coinString(from: record.value, signType: record.sentToSelf ? .never : .always), type: type(value: record.value, condition: record.sentToSelf, .neutral, .outgoing)) if let currencyValue = item.currencyValue { secondaryValue = BaseTransactionsViewModel.Value(text: currencyString(from: currencyValue), type: .secondary) @@ -341,7 +341,7 @@ class TransactionsViewItemFactory { title = "transactions.send".localized subTitle = "transactions.to".localized(mapped(address: record.to, blockchainType: item.record.source.blockchainType)) - primaryValue = BaseTransactionsViewModel.Value(text: coinString(from: record.value, showSign: !record.sentToSelf), type: type(value: record.value, condition: record.sentToSelf, .neutral, .outgoing)) + primaryValue = BaseTransactionsViewModel.Value(text: coinString(from: record.value, signType: record.sentToSelf ? .never : .always), type: type(value: record.value, condition: record.sentToSelf, .neutral, .outgoing)) secondaryValue = singleValueSecondaryValue(value: record.value, currencyValue: item.currencyValue, nftMetadata: item.nftMetadata) sentToSelf = record.sentToSelf @@ -355,7 +355,7 @@ class TransactionsViewItemFactory { primaryValue = BaseTransactionsViewModel.Value(text: "∞ \(record.value.coinCode)", type: .neutral) secondaryValue = BaseTransactionsViewModel.Value(text: "transactions.value.unlimited".localized, type: .secondary) } else { - primaryValue = BaseTransactionsViewModel.Value(text: coinString(from: record.value, showSign: false), type: .neutral) + primaryValue = BaseTransactionsViewModel.Value(text: coinString(from: record.value, signType: .never), type: .neutral) if let currencyValue = item.currencyValue { secondaryValue = BaseTransactionsViewModel.Value(text: currencyString(from: currencyValue), type: .secondary) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/BalanceViewItem.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/BalanceViewItem.swift index b30b263dfe..0a38c662ac 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/BalanceViewItem.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/BalanceViewItem.swift @@ -9,15 +9,14 @@ struct BalanceViewItem { struct BalanceTopViewItem { let isMainNet: Bool - let iconUrlString: String? - let placeholderIconName: String + let coin: Coin? + let placeholderIconName: String? let name: String let blockchainBadge: String? let syncSpinnerProgress: Int? let indefiniteSearchCircle: Bool let failedImageViewVisible: Bool - let sendEnabled: Bool let primaryValue: (text: String?, dimmed: Bool)? let secondaryInfo: BalanceSecondaryInfoViewItem @@ -44,7 +43,7 @@ enum BalanceDiffType { extension BalanceTopViewItem: Equatable { static func == (lhs: BalanceTopViewItem, rhs: BalanceTopViewItem) -> Bool { lhs.isMainNet == rhs.isMainNet && - lhs.iconUrlString == rhs.iconUrlString && + lhs.coin == rhs.coin && lhs.name == rhs.name && lhs.blockchainBadge == rhs.blockchainBadge && lhs.syncSpinnerProgress == rhs.syncSpinnerProgress && @@ -101,7 +100,7 @@ extension BalanceViewItem: CustomStringConvertible { extension BalanceTopViewItem: CustomStringConvertible { var description: String { - "[iconUrlString: \(iconUrlString ?? "nil"); name: \(name); blockchainBadge: \(blockchainBadge ?? "nil"); syncSpinnerProgress: \(syncSpinnerProgress.map { "\($0)" } ?? "nil"); indefiniteSearchCircle: \(indefiniteSearchCircle); failedImageViewVisible: \(failedImageViewVisible); primaryValue: \(primaryValue.map { "[text: \($0.text ?? "nil"); dimmed: \($0.dimmed)]" } ?? "nil"); secondaryInfo: \(secondaryInfo)]" + "[coin: \(coin?.name ?? "nil"); name: \(name); blockchainBadge: \(blockchainBadge ?? "nil"); syncSpinnerProgress: \(syncSpinnerProgress.map { "\($0)" } ?? "nil"); indefiniteSearchCircle: \(indefiniteSearchCircle); failedImageViewVisible: \(failedImageViewVisible); primaryValue: \(primaryValue.map { "[text: \($0.text ?? "nil"); dimmed: \($0.dimmed)]" } ?? "nil"); secondaryInfo: \(secondaryInfo)]" } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Receive/Address/ReceiveAddressView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Receive/Address/ReceiveAddressView.swift index 9e04c6a6c5..e05589779b 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Receive/Address/ReceiveAddressView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Receive/Address/ReceiveAddressView.swift @@ -1,3 +1,4 @@ +import Combine import SwiftUI private let qrSize: CGFloat = 203 @@ -13,8 +14,6 @@ struct ReceiveAddressView: View { @State private var shareText: String? @State private var inputAmountPresented: Bool = false - @State private var inputText: String = "" - @Environment(\.presentationMode) private var presentationMode init(cexAsset: CexAsset, network: CexDepositNetwork?, provider: ICexDepositProvider) { @@ -90,16 +89,17 @@ struct ReceiveAddressView: View { .sheet(item: $shareText) { shareText in ActivityView.view(activityItems: [shareText]) } - .alert("deposit.enter_amount".localized, isPresented: $inputAmountPresented, actions: { - TextField("deposit.enter_amount".localized, text: $inputText) // TODO: Can't check valid numbers in default alertview - .keyboardType(.decimalPad) - Button("button.cancel".localized) { - updateAmount(success: false) - } - Button("button.confirm".localized) { - updateAmount(success: true) + .textFieldAlert( + isPresented: $inputAmountPresented, + amountChanged: viewModel.onAmountChanged(_:), + content: { + TextFieldAlert( + title: "deposit.enter_amount".localized, + message: nil, + initial: viewModel.initialText + ) } - }) + ) .alertButtonTint(color: .themeJacob) .bottomSheet(item: $warningAlertPopup) { popup in ActionSheetView( @@ -204,22 +204,12 @@ struct ReceiveAddressView: View { } } - private func updateAmount(success: Bool) { - if success { - viewModel.set(amount: inputText) - stat(page: .receive, event: .setAmount) - } else { - inputText = viewModel.amount == 0 ? "" : viewModel.amount.description - } - } - @ViewBuilder func view(amount: String) -> some View { ListRow { Text("deposit.amount".localized).textSubhead2() Spacer() Text(amount).textSubhead1(color: .themeLeah) Button(action: { - inputText = "" viewModel.set(amount: "") stat(page: .receive, event: .removeAmount) }, label: { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Receive/Address/ReceiveAddressViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Receive/Address/ReceiveAddressViewModel.swift index e5368357ec..3ed9db2a3a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Receive/Address/ReceiveAddressViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Receive/Address/ReceiveAddressViewModel.swift @@ -26,6 +26,11 @@ class ReceiveAddressViewModel: ObservableObject { @Published private(set) var actions: [ReceiveAddressModule.ActionType] = [] @Published private(set) var amount: Decimal = 0 + private var amountFieldChangedSuccessSubject = PassthroughSubject() + var amountFieldChangedSuccessPublisher: AnyPublisher { + amountFieldChangedSuccessSubject.eraseToAnyPublisher() + } + init(service: IReceiveAddressService, viewItemFactory: IReceiveAddressViewItemFactory, decimalParser: AmountDecimalParser) { self.service = service self.viewItemFactory = viewItemFactory @@ -77,6 +82,15 @@ extension ReceiveAddressViewModel { hasAppeared = true syncPopup(state: service.state) } + + func onAmountChanged(_ text: String) { + set(amount: text) + stat(page: .receive, event: .setAmount) + } + + var initialText: String { + amount == 0 ? "" : amount.description + } } extension ReceiveAddressViewModel { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Receive/SelectCoin/ReceiveSelectCoinViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Receive/SelectCoin/ReceiveSelectCoinViewController.swift index 29545ae399..0a42aec6bc 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Receive/SelectCoin/ReceiveSelectCoinViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Receive/SelectCoin/ReceiveSelectCoinViewController.swift @@ -88,7 +88,7 @@ extension ReceiveSelectCoinViewController: SectionsDataSource { return tableView.universalRow62( id: viewItem.uid, - image: .url(viewItem.imageUrl, placeholder: "placeholder_circle_32"), + image: .url(viewItem.coin), title: .body(viewItem.title), description: .subhead2(viewItem.description), backgroundStyle: .transparent, diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Receive/SelectCoin/ReceiveSelectCoinViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Receive/SelectCoin/ReceiveSelectCoinViewModel.swift index eaece4363d..fc60dabb4e 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Receive/SelectCoin/ReceiveSelectCoinViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Receive/SelectCoin/ReceiveSelectCoinViewModel.swift @@ -22,7 +22,7 @@ class ReceiveSelectCoinViewModel { private func sync(coins: [FullCoin]) { viewItems = coins.map { fullCoin -> ViewItem in - ViewItem(uid: fullCoin.coin.uid, imageUrl: fullCoin.coin.imageUrl, title: fullCoin.coin.code, description: fullCoin.coin.name) + ViewItem(uid: fullCoin.coin.uid, coin: fullCoin.coin, title: fullCoin.coin.code, description: fullCoin.coin.name) } } } @@ -42,7 +42,7 @@ extension ReceiveSelectCoinViewModel { extension ReceiveSelectCoinViewModel { struct ViewItem { let uid: String - let imageUrl: String? + let coin: Coin? let title: String let description: String } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/CautionDataSource.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/CautionDataSource.swift new file mode 100644 index 0000000000..09652b9d3c --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/CautionDataSource.swift @@ -0,0 +1,122 @@ +import Combine +import ComponentKit +import Foundation +import HUD +import MarketKit +import SectionsTableView +import ThemeKit +import UIKit + +class CautionDataSource: NSObject { + private let viewModel: ICautionDataSourceViewModel + private var cancellables: [AnyCancellable] = [] + + private var caution: TitledCaution? + private var tableView: UITableView? + + weak var parentViewController: UIViewController? + weak var delegate: ISectionDataSourceDelegate? + + init(viewModel: ICautionDataSourceViewModel) { + self.viewModel = viewModel + + super.init() + + viewModel.cautionPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.sync(caution: $0) + } + .store(in: &cancellables) + + sync(caution: viewModel.caution) + } + + private func sync(caution: TitledCaution?) { + let oldCautionExists = self.caution != nil + let newCautionExists = caution != nil + self.caution = caution + + guard oldCautionExists == newCautionExists else { + tableView?.reloadData() + return + } + + if let tableView { + if newCautionExists { + let indexPath = IndexPath(row: 0, section: 0) + let originalIndexPath = delegate? + .originalIndexPath(tableView: tableView, dataSource: self, indexPath: indexPath) ?? indexPath + + if let cell = tableView.cellForRow(at: originalIndexPath) as? TitledHighlightedDescriptionCell { + bind(cell: cell, row: 0) + } + } + } + } + + private func bind(cell: TitledHighlightedDescriptionCell, row _: Int) { + guard let caution else { + return + } + cell.set(backgroundStyle: .externalBorderOnly, cornerRadius: .margin12, isFirst: true, isLast: true) + cell.bind(caution: caution) + } +} + +extension CautionDataSource: ISectionDataSource { + func prepare(tableView: UITableView) { + tableView.registerCell(forClass: TitledHighlightedDescriptionCell.self) + tableView.registerHeaderFooter(forClass: SectionColorHeader.self) + self.tableView = tableView + } +} + +extension CautionDataSource: UITableViewDataSource { + func numberOfSections(in _: UITableView) -> Int { + 1 + } + + func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { + caution == nil ? 0 : 1 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let originalIndexPath = delegate?.originalIndexPath(tableView: tableView, dataSource: self, indexPath: indexPath) ?? indexPath + return tableView.dequeueReusableCell(withIdentifier: String(describing: TitledHighlightedDescriptionCell.self), for: originalIndexPath) + } +} + +extension CautionDataSource: UITableViewDelegate { + func tableView(_: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + if let cell = cell as? TitledHighlightedDescriptionCell { + bind(cell: cell, row: indexPath.row) + } + } + + func tableView(_ tableView: UITableView, heightForRowAt _: IndexPath) -> CGFloat { + guard let caution else { + return 0 + } + + return TitledHighlightedDescriptionCell.height(containerWidth: tableView.width, text: caution.text) + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection _: Int) -> UIView? { + guard caution != nil else { + return nil + } + + let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: String(describing: SectionColorHeader.self)) as? SectionColorHeader + view?.backgroundView?.backgroundColor = .clear + return view + } + + func tableView(_: UITableView, heightForHeaderInSection _: Int) -> CGFloat { + caution == nil ? .zero : .margin8 + } + + func tableView(_: UITableView, heightForFooterInSection _: Int) -> CGFloat { + caution == nil ? .zero : .margin16 + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/CautionDataSourceViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/CautionDataSourceViewModel.swift new file mode 100644 index 0000000000..eda92870a9 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/CautionDataSourceViewModel.swift @@ -0,0 +1,29 @@ +import Combine +import HsExtensions +import TronKit + +protocol ICautionDataSourceViewModel { + var caution: TitledCaution? { get } + var cautionPublisher: AnyPublisher { get } +} + +class TronAccountInactiveViewModel { + private let cautionSubject = PassthroughSubject() + private(set) var caution: TitledCaution? { + didSet { + cautionSubject.send(caution) + } + } + + init(adapter: BaseTronAdapter) { + caution = (adapter.receiveAddress as? ActivatedDepositAddress)?.isActive == true + ? nil + : TitledCaution(title: "balance.token.account.inactive.title".localized, text: "balance.token.account.inactive.description".localized, type: .warning) + } +} + +extension TronAccountInactiveViewModel: ICautionDataSourceViewModel { + var cautionPublisher: AnyPublisher { + cautionSubject.eraseToAnyPublisher() + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/Views/WalletTokenBalanceCell.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/Views/WalletTokenBalanceCell.swift index e7ec18935f..5f18edc284 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/Views/WalletTokenBalanceCell.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/Views/WalletTokenBalanceCell.swift @@ -71,7 +71,7 @@ class WalletTokenBalanceCell: UITableViewCell { func bind(viewItem: WalletTokenBalanceViewModel.ViewItem, onTapError: (() -> Void)?) { testnetImageView.isHidden = viewItem.isMainNet coinIconView.bind( - iconUrlString: viewItem.iconUrlString, + coin: viewItem.coin, placeholderIconName: viewItem.placeholderIconName, spinnerProgress: viewItem.syncSpinnerProgress, indefiniteSearchCircle: viewItem.indefiniteSearchCircle, diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceDataSource.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceDataSource.swift index 313155685d..d9eb8deff4 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceDataSource.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceDataSource.swift @@ -172,8 +172,10 @@ class WalletTokenBalanceDataSource: NSObject { } case let .wallet(wallet): cell.actions[.send] = { [weak self] in - if let viewController = SendModule.controller(wallet: wallet) { - self?.parentViewController?.present(ThemeNavigationController(rootViewController: viewController), animated: true) + let module = App.shared.localStorage.newSendEnabled ? PreSendView(wallet: wallet, showIcon: true).toNavigationViewController() : SendModule.controller(wallet: wallet).map { ThemeNavigationController(rootViewController: $0) } + + if let module { + self?.parentViewController?.present(module, animated: true) stat(page: .tokenPage, event: .openSend(token: wallet.token)) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceModule.swift index 7c55331e43..42ffec455c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceModule.swift @@ -7,8 +7,8 @@ enum WalletTokenBalanceModule { } let coinPriceService = WalletCoinPriceService( - tag: "wallet-token-balance", currencyManager: App.shared.currencyManager, + priceChangeModeManager: App.shared.priceChangeModeManager, marketKit: App.shared.marketKit ) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceService.swift index d68de528c0..14928e4350 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceService.swift @@ -181,6 +181,5 @@ extension WalletTokenBalanceService: IWalletCoinPriceServiceDelegate { } } - func didUpdateBaseCurrency() {} - func didUpdate(itemsMap _: [String: WalletCoinPriceService.Item]) {} + func didUpdate(itemsMap _: [String: WalletCoinPriceService.Item]?) {} } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift index 9d8e41552a..5e09fef204 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewItemFactory.swift @@ -17,15 +17,11 @@ class WalletTokenBalanceViewItemFactory { if item.watchAccount { buttons[.address] = .enabled } else { - let sendButtonState: ButtonState = item - .state - .spendAllowed(beforeSync: item.balanceData.sendBeforeSync) ? .enabled : .disabled - - buttons[.send] = sendButtonState + buttons[.send] = .enabled buttons[.receive] = .enabled if AppConfig.swapEnabled, wallet.token.swappable { - buttons[.swap] = sendButtonState + buttons[.swap] = .enabled } } case let .cexAsset(cexAsset): @@ -43,8 +39,8 @@ class WalletTokenBalanceViewItemFactory { return WalletTokenBalanceViewModel.ViewItem( isMainNet: item.isMainNet, - iconUrlString: iconUrlString(coin: item.element.coin, state: state), - placeholderIconName: item.element.wallet?.token.placeholderImageName ?? "placeholder_circle_32", + coin: stateAwareCoin(coin: item.element.coin, state: state), + placeholderIconName: item.element.wallet?.token.placeholderImageName, syncSpinnerProgress: syncSpinnerProgress(state: state), indefiniteSearchCircle: indefiniteSearchCircle(state: state), failedImageViewVisible: failedImageViewVisible(state: state), @@ -78,10 +74,10 @@ class WalletTokenBalanceViewItemFactory { } } - private func iconUrlString(coin: Coin?, state: AdapterState) -> String? { + private func stateAwareCoin(coin: Coin?, state: AdapterState) -> Coin? { switch state { case .notSynced: return nil - default: return coin?.imageUrl + default: return coin } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewModel.swift index c1001c6ad5..4e5d100192 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/DataSources/WalletTokenBalance/WalletTokenBalanceViewModel.swift @@ -138,8 +138,8 @@ extension WalletTokenBalanceViewModel { struct ViewItem { let isMainNet: Bool - let iconUrlString: String? - let placeholderIconName: String + let coin: Coin? + let placeholderIconName: String? let syncSpinnerProgress: Int? let indefiniteSearchCircle: Bool diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/WalletTokenModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/WalletTokenModule.swift index b2d5d0ea96..cabdd90dde 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/WalletTokenModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Token/WalletTokenModule.swift @@ -21,6 +21,10 @@ enum WalletTokenModule { dataSourceChain.append(source: tokenBalanceDataSource) if let wallet = element.wallet { + if let cautionDataSource = cautionDataSource(wallet: wallet) { + dataSourceChain.append(source: cautionDataSource) + } + let transactionsDataSource = TransactionsModule.dataSource(token: wallet.token, statPage: .tokenPage) transactionsDataSource.viewController = viewController dataSourceChain.append(source: transactionsDataSource) @@ -28,4 +32,14 @@ enum WalletTokenModule { return viewController } + + static func cautionDataSource(wallet: Wallet) -> ISectionDataSource? { + guard wallet.token.blockchainType == .tron, + let adapter = App.shared.adapterManager.adapter(for: wallet) as? BaseTronAdapter + else { + return nil + } + + return CautionDataSource(viewModel: TronAccountInactiveViewModel(adapter: adapter)) + } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListDataSource.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListDataSource.swift index 093b92e731..ba393b21f7 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListDataSource.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListDataSource.swift @@ -165,7 +165,6 @@ extension WalletTokenListDataSource: ISectionDataSource { } subscribe(disposeBag, viewModel.noConnectionErrorSignal) { HudHelper.instance.show(banner: .noInternet) } - subscribe(disposeBag, viewModel.showSyncingSignal) { HudHelper.instance.show(banner: .attention(string: "wait_for_synchronization".localized)) } subscribe(disposeBag, viewModel.selectWalletSignal) { [weak self] in self?.onSelect(wallet: $0) } subscribe(disposeBag, viewModel.openSyncErrorSignal) { [weak self] in self?.openSyncError(wallet: $0, error: $1) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListService.swift index f2efc74edd..f6821eb8c0 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListService.swift @@ -252,24 +252,21 @@ extension WalletTokenListService: IWalletCoinPriceServiceDelegate { internalState = .loaded(items: _sorted(items: items)) } - func didUpdateBaseCurrency() { + func didUpdate(itemsMap: [String: WalletCoinPriceService.Item]?) { queue.async { guard case let .loaded(items) = self.internalState else { return } - let coinUids = Array(Set(items.compactMap(\.element.priceCoinUid))) - self._handleUpdated(priceItemMap: self.coinPriceService.itemMap(coinUids: coinUids), items: items) - } - } - - func didUpdate(itemsMap: [String: WalletCoinPriceService.Item]) { - queue.async { - guard case let .loaded(items) = self.internalState else { - return + let _itemsMap: [String: WalletCoinPriceService.Item] + if let itemsMap { + _itemsMap = itemsMap + } else { + let coinUids = Array(Set(items.compactMap(\.element.priceCoinUid))) + _itemsMap = self.coinPriceService.itemMap(coinUids: coinUids) } - self._handleUpdated(priceItemMap: itemsMap, items: items) + self._handleUpdated(priceItemMap: _itemsMap, items: items) } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListViewItemFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListViewItemFactory.swift index 0daf132b3b..5428f4a194 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListViewItemFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListViewItemFactory.swift @@ -8,18 +8,16 @@ class WalletTokenListViewItemFactory { private func topViewItem(item: WalletTokenListService.Item, balancePrimaryValue: BalancePrimaryValue, balanceHidden: Bool) -> BalanceTopViewItem { let state = item.state - let sendEnabled = state.spendAllowed(beforeSync: item.balanceData.sendBeforeSync) return BalanceTopViewItem( isMainNet: item.isMainNet, - iconUrlString: iconUrlString(coin: item.element.coin, state: state), - placeholderIconName: item.element.wallet?.token.placeholderImageName ?? "placeholder_circle_32", + coin: stateAwareCoin(coin: item.element.coin, state: state), + placeholderIconName: item.element.wallet?.token.placeholderImageName, name: item.element.name, blockchainBadge: item.element.wallet?.badge, syncSpinnerProgress: syncSpinnerProgress(state: state), indefiniteSearchCircle: indefiniteSearchCircle(state: state), failedImageViewVisible: failedImageViewVisible(state: state), - sendEnabled: sendEnabled, primaryValue: primaryValue(item: item, balancePrimaryValue: balancePrimaryValue, balanceHidden: balanceHidden), secondaryInfo: secondaryInfo(item: item, balancePrimaryValue: balancePrimaryValue, balanceHidden: balanceHidden) ) @@ -45,10 +43,10 @@ class WalletTokenListViewItemFactory { } } - private func iconUrlString(coin: Coin?, state: AdapterState) -> String? { + private func stateAwareCoin(coin: Coin?, state: AdapterState) -> Coin? { switch state { case .notSynced: return nil - default: return coin?.imageUrl + default: return coin } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListViewModel.swift index fc7b8d06c3..57fdcf892c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/TokenList/WalletTokenListViewModel.swift @@ -31,7 +31,6 @@ class WalletTokenListViewModel { private let showWarningRelay = BehaviorRelay(value: nil) private let noConnectionErrorRelay = PublishRelay() - private let showSyncingRelay = PublishRelay() private let selectWalletRelay = PublishRelay() private let openSyncErrorRelay = PublishRelay<(Wallet, Error)>() @@ -132,10 +131,6 @@ extension WalletTokenListViewModel { noConnectionErrorRelay.asSignal() } - var showSyncingSignal: Signal { - showSyncingRelay.asSignal() - } - var selectWalletSignal: Signal { selectWalletRelay.asSignal() } @@ -170,10 +165,6 @@ extension WalletTokenListViewModel { onTapFailedIcon(element: item.element) return } - if !item.topViewItem.sendEnabled { - showSyncingRelay.accept(()) - return - } if let wallet = item.element.wallet { selectWalletRelay.accept(wallet) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Views/BalanceCoinIconHolder.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Views/BalanceCoinIconHolder.swift index 59b19aefbd..8e5270134b 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Views/BalanceCoinIconHolder.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Views/BalanceCoinIconHolder.swift @@ -1,5 +1,6 @@ import ComponentKit import HUD +import MarketKit import ThemeKit import UIKit @@ -49,12 +50,12 @@ class BalanceCoinIconHolder: UIView { onTapError?() } - func bind(iconUrlString: String?, placeholderIconName: String, spinnerProgress: Int?, indefiniteSearchCircle: Bool, failViewVisible: Bool, onTapError: (() -> Void)?) { + func bind(coin: Coin?, placeholderIconName: String?, spinnerProgress: Int?, indefiniteSearchCircle: Bool, failViewVisible: Bool, onTapError: (() -> Void)?) { self.onTapError = onTapError - coinIconImageView.isHidden = iconUrlString == nil - if let iconUrlString { - coinIconImageView.setImage(withUrlString: iconUrlString, placeholder: UIImage(named: placeholderIconName)) + coinIconImageView.isHidden = coin == nil + if let coin { + coinIconImageView.setImage(coin: coin, placeholder: placeholderIconName) } else { coinIconImageView.image = nil } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Views/BalanceTopView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Views/BalanceTopView.swift index 9fcdee0c32..53df65cf97 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Views/BalanceTopView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/Views/BalanceTopView.swift @@ -102,7 +102,7 @@ class BalanceTopView: UIView { func bind(viewItem: BalanceTopViewItem, onTapError: (() -> Void)?) { coinIconView.bind( - iconUrlString: viewItem.iconUrlString, + coin: viewItem.coin, placeholderIconName: viewItem.placeholderIconName, spinnerProgress: viewItem.syncSpinnerProgress, indefiniteSearchCircle: viewItem.indefiniteSearchCircle, diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletCoinPriceService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletCoinPriceService.swift index 7f84c3377a..92e3318b94 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletCoinPriceService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletCoinPriceService.swift @@ -3,15 +3,14 @@ import Foundation import MarketKit protocol IWalletCoinPriceServiceDelegate: AnyObject { - func didUpdateBaseCurrency() - func didUpdate(itemsMap: [String: WalletCoinPriceService.Item]) + func didUpdate(itemsMap: [String: WalletCoinPriceService.Item]?) } class WalletCoinPriceService { weak var delegate: IWalletCoinPriceServiceDelegate? - private let tag: String private let currencyManager: CurrencyManager + private let priceChangeModeManager: PriceChangeModeManager private let marketKit: MarketKit.Kit private var cancellables = Set() private var coinPriceCancellables = Set() @@ -21,10 +20,10 @@ class WalletCoinPriceService { private var feeCoinUids = Set() private var conversionCoinUids = Set() - init(tag: String, currencyManager: CurrencyManager, marketKit: MarketKit.Kit) { - self.tag = tag + init(currencyManager: CurrencyManager, priceChangeModeManager: PriceChangeModeManager, marketKit: MarketKit.Kit) { self.currencyManager = currencyManager self.marketKit = marketKit + self.priceChangeModeManager = priceChangeModeManager currency = currencyManager.baseCurrency @@ -33,19 +32,25 @@ class WalletCoinPriceService { self?.onUpdate(baseCurrency: currency) } .store(in: &cancellables) + + priceChangeModeManager.$priceChangeMode + .sink { [weak self] _ in + self?.delegate?.didUpdate(itemsMap: nil) + } + .store(in: &cancellables) } private func onUpdate(baseCurrency: Currency) { currency = baseCurrency subscribeToCoinPrices() - delegate?.didUpdateBaseCurrency() + delegate?.didUpdate(itemsMap: nil) } private func subscribeToCoinPrices() { coinPriceCancellables = Set() if !coinUids.isEmpty { - marketKit.coinPriceMapPublisher(tag: tag, coinUids: Array(coinUids), currencyCode: currencyManager.baseCurrency.code) + marketKit.coinPriceMapPublisher(coinUids: Array(coinUids), currencyCode: currencyManager.baseCurrency.code) .sink { [weak self] in self?.onUpdate(coinPriceMap: $0) } @@ -53,13 +58,13 @@ class WalletCoinPriceService { } if !feeCoinUids.isEmpty { - marketKit.coinPriceMapPublisher(tag: "fee:\(tag)", coinUids: Array(feeCoinUids), currencyCode: currencyManager.baseCurrency.code) + marketKit.coinPriceMapPublisher(coinUids: Array(feeCoinUids), currencyCode: currencyManager.baseCurrency.code) .sink { _ in } .store(in: &coinPriceCancellables) } if !conversionCoinUids.isEmpty { - marketKit.coinPriceMapPublisher(tag: "conversion:\(tag)", coinUids: Array(conversionCoinUids), currencyCode: currencyManager.baseCurrency.code) + marketKit.coinPriceMapPublisher(coinUids: Array(conversionCoinUids), currencyCode: currencyManager.baseCurrency.code) .sink { _ in } .store(in: &coinPriceCancellables) } @@ -73,9 +78,17 @@ class WalletCoinPriceService { private func item(coinPrice: CoinPrice) -> Item { let currency = currencyManager.baseCurrency + let diff: Decimal? + switch priceChangeModeManager.priceChangeMode { + case .hour24: + diff = coinPrice.diff24h + case .day1: + diff = coinPrice.diff1d + } + return Item( price: CurrencyValue(currency: currency, value: coinPrice.value), - diff: coinPrice.diff, + diff: diff, expired: coinPrice.expired ) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletModule.swift index 79ed810e4d..a3b29a668c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletModule.swift @@ -6,8 +6,8 @@ import UIKit enum WalletModule { static func viewController() -> UIViewController { let coinPriceService = WalletCoinPriceService( - tag: "wallet", currencyManager: App.shared.currencyManager, + priceChangeModeManager: App.shared.priceChangeModeManager, marketKit: App.shared.marketKit ) @@ -26,6 +26,7 @@ enum WalletModule { reachabilityManager: App.shared.reachabilityManager, balancePrimaryValueManager: App.shared.balancePrimaryValueManager, balanceHiddenManager: App.shared.balanceHiddenManager, + buttonHiddenManager: App.shared.walletButtonHiddenManager, balanceConversionManager: App.shared.balanceConversionManager, cloudAccountBackupManager: App.shared.cloudBackupManager, rateAppManager: App.shared.rateAppManager, @@ -51,14 +52,14 @@ enum WalletModule { return WalletViewController(viewModel: viewModel) } - static func sendTokenListViewController(allowedBlockchainTypes: [BlockchainType]? = nil, allowedTokenTypes: [TokenType]? = nil, mode: SendBaseService.Mode = .send) -> UIViewController? { + static func sendTokenListViewController(allowedBlockchainTypes: [BlockchainType]? = nil, allowedTokenTypes: [TokenType]? = nil, mode: PreSendViewModel.Mode = .regular) -> UIViewController? { guard let account = App.shared.accountManager.activeAccount else { return nil } let coinPriceService = WalletCoinPriceService( - tag: "send-token-list", currencyManager: App.shared.currencyManager, + priceChangeModeManager: App.shared.priceChangeModeManager, marketKit: App.shared.marketKit ) @@ -101,7 +102,11 @@ enum WalletModule { let viewController = WalletTokenListViewController(viewModel: viewModel, dataSource: dataSourceChain) dataSource.viewController = viewController dataSource.onSelectWallet = { [weak viewController] wallet in - if let module = SendModule.controller(wallet: wallet, mode: mode) { + let module = App.shared.localStorage.newSendEnabled ? + PreSendView(wallet: wallet, mode: mode, onDismiss: { viewController?.dismiss(animated: true) }).toViewController() : + SendModule.controller(wallet: wallet, mode: mode) + + if let module { viewController?.navigationController?.pushViewController(module, animated: true) } } @@ -115,8 +120,8 @@ enum WalletModule { } let coinPriceService = WalletCoinPriceService( - tag: "swap-token-list", currencyManager: App.shared.currencyManager, + priceChangeModeManager: App.shared.priceChangeModeManager, marketKit: App.shared.marketKit ) @@ -171,8 +176,8 @@ enum WalletModule { let service: IWalletTokenListService if let account = App.shared.accountManager.activeAccount, !account.watchAccount, !account.cexAccount { let coinPriceService = WalletCoinPriceService( - tag: "send-token-list", currencyManager: App.shared.currencyManager, + priceChangeModeManager: App.shared.priceChangeModeManager, marketKit: App.shared.marketKit ) @@ -232,9 +237,13 @@ enum WalletModule { return } - if let module = SendModule.controller(wallet: wallet, mode: .predefined(address: address)) { - stat(page: .donate, event: .openSend(token: wallet.token)) + let module = App.shared.localStorage.newSendEnabled ? + PreSendView(wallet: wallet, mode: .predefined(address: address), onDismiss: { viewController?.dismiss(animated: true) }).toViewController() : + SendModule.controller(wallet: wallet, mode: .predefined(address: address)) + + if let module { viewController?.navigationController?.pushViewController(module, animated: true) + stat(page: .donate, event: .openSend(token: wallet.token)) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletService.swift index ac3eb51d9a..0e4311ba87 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletService.swift @@ -34,6 +34,7 @@ class WalletService { private let reachabilityManager: IReachabilityManager private let balancePrimaryValueManager: BalancePrimaryValueManager private let balanceHiddenManager: BalanceHiddenManager + private let buttonHiddenManager: WalletButtonHiddenManager private let balanceConversionManager: BalanceConversionManager private let cloudAccountBackupManager: CloudBackupManager private let rateAppManager: RateAppManager @@ -41,6 +42,7 @@ class WalletService { private let userDefaultsStorage: UserDefaultsStorage private let sorter = WalletSorter() private let disposeBag = DisposeBag() + private var cancellables = Set() private var internalState: State = .loading { didSet { @@ -75,7 +77,8 @@ class WalletService { init(elementServiceFactory: WalletElementServiceFactory, coinPriceService: WalletCoinPriceService, accountManager: AccountManager, cacheManager: EnabledWalletCacheManager, accountRestoreWarningManager: AccountRestoreWarningManager, reachabilityManager: IReachabilityManager, - balancePrimaryValueManager: BalancePrimaryValueManager, balanceHiddenManager: BalanceHiddenManager, balanceConversionManager: BalanceConversionManager, + balancePrimaryValueManager: BalancePrimaryValueManager, balanceHiddenManager: BalanceHiddenManager, + buttonHiddenManager: WalletButtonHiddenManager, balanceConversionManager: BalanceConversionManager, cloudAccountBackupManager: CloudBackupManager, rateAppManager: RateAppManager, appManager: IAppManager, feeCoinProvider: FeeCoinProvider, userDefaultsStorage: UserDefaultsStorage) { @@ -87,6 +90,7 @@ class WalletService { self.reachabilityManager = reachabilityManager self.balancePrimaryValueManager = balancePrimaryValueManager self.balanceHiddenManager = balanceHiddenManager + self.buttonHiddenManager = buttonHiddenManager self.balanceConversionManager = balanceConversionManager self.cloudAccountBackupManager = cloudAccountBackupManager self.rateAppManager = rateAppManager @@ -119,9 +123,8 @@ class WalletService { subscribe(disposeBag, appManager.willEnterForegroundObservable) { [weak self] in self?.coinPriceService.refresh() } - subscribe(disposeBag, balanceConversionManager.conversionTokenObservable) { [weak self] _ in - self?.syncTotalItem() - } + + balanceConversionManager.$conversionToken.sink { [weak self] _ in self?.syncTotalItem() }.store(in: &cancellables) sync(activeAccount: accountManager.activeAccount) } @@ -386,24 +389,21 @@ extension WalletService: IWalletCoinPriceServiceDelegate { _syncTotalItem() } - func didUpdateBaseCurrency() { + func didUpdate(itemsMap: [String: WalletCoinPriceService.Item]?) { queue.async { guard case let .loaded(items) = self.internalState else { return } - let coinUids = Array(Set(items.compactMap(\.element.priceCoinUid))) - self._handleUpdated(priceItemMap: self.coinPriceService.itemMap(coinUids: coinUids), items: items) - } - } - - func didUpdate(itemsMap: [String: WalletCoinPriceService.Item]) { - queue.async { - guard case let .loaded(items) = self.internalState else { - return + let _itemsMap: [String: WalletCoinPriceService.Item] + if let itemsMap { + _itemsMap = itemsMap + } else { + let coinUids = Array(Set(items.compactMap(\.element.priceCoinUid))) + _itemsMap = self.coinPriceService.itemMap(coinUids: coinUids) } - self._handleUpdated(priceItemMap: itemsMap, items: items) + self._handleUpdated(priceItemMap: _itemsMap, items: items) } } } @@ -437,6 +437,10 @@ extension WalletService { balanceHiddenManager.balanceHiddenObservable } + var buttonHiddenObservable: Observable { + buttonHiddenManager.buttonHiddenObservable + } + var activeAccount: Account? { accountManager.activeAccount } @@ -461,6 +465,10 @@ extension WalletService { balanceHiddenManager.balanceHidden } + var buttonHidden: Bool { + buttonHiddenManager.buttonHidden + } + var isReachable: Bool { reachabilityManager.isReachable } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewItemFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewItemFactory.swift index b611c0139f..e8292c11a5 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewItemFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewItemFactory.swift @@ -8,18 +8,16 @@ class WalletViewItemFactory { private func topViewItem(item: WalletService.Item, balancePrimaryValue: BalancePrimaryValue, balanceHidden: Bool) -> BalanceTopViewItem { let state = item.state - let sendEnabled = state.spendAllowed(beforeSync: item.balanceData.sendBeforeSync) return BalanceTopViewItem( isMainNet: item.isMainNet, - iconUrlString: iconUrlString(coin: item.element.coin, state: state), - placeholderIconName: item.element.wallet?.token.placeholderImageName ?? "placeholder_circle_32", + coin: stateAwareCoin(coin: item.element.coin, state: state), + placeholderIconName: item.element.wallet?.token.placeholderImageName, name: item.element.name, blockchainBadge: item.element.wallet?.badge, syncSpinnerProgress: syncSpinnerProgress(state: state), indefiniteSearchCircle: indefiniteSearchCircle(state: state), failedImageViewVisible: failedImageViewVisible(state: state), - sendEnabled: sendEnabled, primaryValue: balanceHidden ? nil : primaryValue(item: item, balancePrimaryValue: balancePrimaryValue), secondaryInfo: secondaryInfo(item: item, balancePrimaryValue: balancePrimaryValue, balanceHidden: balanceHidden) ) @@ -45,10 +43,10 @@ class WalletViewItemFactory { } } - private func iconUrlString(coin: Coin?, state: AdapterState) -> String? { + private func stateAwareCoin(coin: Coin?, state: AdapterState) -> Coin? { switch state { case .notSynced: return nil - default: return coin?.imageUrl + default: return coin } } @@ -94,7 +92,7 @@ class WalletViewItemFactory { return nil } - guard let formattedValue = ValueFormatter.instance.format(percentValue: value, showSign: true) else { + guard let formattedValue = ValueFormatter.instance.format(percentValue: value, signType: .always) else { return nil } @@ -137,7 +135,7 @@ class WalletViewItemFactory { } private func headerButtons(account: Account?) -> [WalletModule.Button: ButtonState] { - guard let account else { + guard let account, !account.watchAccount else { return [:] } switch account.type { @@ -166,7 +164,7 @@ extension WalletViewItemFactory { ) } - func headerViewItem(totalItem: WalletService.TotalItem, balanceHidden: Bool, account: Account?) -> WalletModule.HeaderViewItem { + func headerViewItem(totalItem: WalletService.TotalItem, balanceHidden: Bool, buttonHidden: Bool, account: Account?) -> WalletModule.HeaderViewItem { let amount = balanceHidden ? BalanceHiddenManager.placeholder : ValueFormatter.instance.formatShort(currencyValue: totalItem.currencyValue) let convertedValue: String @@ -183,7 +181,7 @@ extension WalletViewItemFactory { amountExpired: balanceHidden ? false : totalItem.expired, convertedValue: convertedValue, convertedValueExpired: balanceHidden ? false : totalItem.convertedValueExpired, - buttons: headerButtons(account: account) + buttons: buttonHidden ? [:] : headerButtons(account: account) ) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewModel.swift index e327c2f731..e9ad9b44f5 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Wallet/WalletViewModel.swift @@ -42,6 +42,7 @@ class WalletViewModel { subscribe(disposeBag, service.activeAccountObservable) { [weak self] in self?.sync(activeAccount: $0) } subscribe(disposeBag, service.balanceHiddenObservable) { [weak self] _ in self?.onUpdateBalanceHidden() } + subscribe(disposeBag, service.buttonHiddenObservable) { [weak self] _ in self?.onUpdateButtonHidden() } subscribe(disposeBag, service.itemUpdatedObservable) { [weak self] in self?.syncUpdated(item: $0) } subscribe(disposeBag, service.sortTypeObservable) { [weak self] in self?.sync(sortType: $0, scrollToTop: true) } subscribe(disposeBag, service.balancePrimaryValueObservable) { [weak self] _ in self?.onUpdateBalancePrimaryValue() } @@ -103,12 +104,16 @@ class WalletViewModel { sync(totalItem: service.totalItem) } + private func onUpdateButtonHidden() { + sync(totalItem: service.totalItem) + } + private func onUpdateBalancePrimaryValue() { sync(serviceState: service.state) } private func sync(totalItem: WalletService.TotalItem?) { - headerViewItem = totalItem.map { factory.headerViewItem(totalItem: $0, balanceHidden: service.balanceHidden, account: service.activeAccount) } + headerViewItem = totalItem.map { factory.headerViewItem(totalItem: $0, balanceHidden: service.balanceHidden, buttonHidden: service.buttonHidden, account: service.activeAccount) } } private func sync(sortType: WalletModule.SortType, scrollToTop: Bool) { @@ -298,7 +303,7 @@ extension WalletViewModel { do { self?.qrScanningRelay.accept(true) - try await eventHandler.handle(event: scanned.trimmingCharacters(in: .whitespacesAndNewlines), eventType: [.walletConnectUri, .address]) + try await eventHandler.handle(source: StatPage.balance, event: scanned.trimmingCharacters(in: .whitespacesAndNewlines), eventType: [.walletConnectUri, .address]) } catch {} } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/List/WalletConnectListViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/List/WalletConnectListViewController.swift index b90ddc2e52..9a5c880e69 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/List/WalletConnectListViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/List/WalletConnectListViewController.swift @@ -97,6 +97,7 @@ class WalletConnectListViewController: ThemeViewController { let scanQrViewController = ScanQrViewController(reportAfterDismiss: true, pasteEnabled: true) scanQrViewController.didFetch = { [weak self] in self?.viewModel.didScan(string: $0) } + stat(page: .walletConnect, event: .open(page: .scanQrCode)) present(scanQrViewController, animated: true) } @@ -105,12 +106,14 @@ class WalletConnectListViewController: ThemeViewController { return } + stat(page: .walletConnect, event: .open(page: .walletConnectSession)) navigationController?.present(viewController, animated: true) } private func showPairings() { let viewController = WalletConnectPairingModule.viewController() + stat(page: .walletConnect, event: .open(page: .walletConnectPairings)) navigationController?.pushViewController(viewController, animated: true) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/List/WalletConnectListViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/List/WalletConnectListViewModel.swift index 621ac9b7fb..5843e3f354 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/List/WalletConnectListViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/List/WalletConnectListViewModel.swift @@ -118,7 +118,7 @@ extension WalletConnectListViewModel { do { self?.disableNewConnectionRelay.accept(true) - try await eventHandler.handle(event: string, eventType: .walletConnectUri) + try await eventHandler.handle(source: .walletConnect, event: string, eventType: .walletConnectUri) } catch {} } } @@ -129,6 +129,7 @@ extension WalletConnectListViewModel { } func kill(id: Int) { + stat(page: .walletConnect, event: .delete(entity: .session)) service.kill(id: id) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Main/Proposal/ProposalChain.swift b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Main/Proposal/ProposalChain.swift index d171158542..7433d25426 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Main/Proposal/ProposalChain.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Main/Proposal/ProposalChain.swift @@ -19,9 +19,9 @@ extension Session: INamespaceProvider { var proposalNamespaces: [String: ProposalNamespace] { namespaces.reduce(into: [:]) { $0[$1.key] = ProposalNamespace( - chains: Set($1.value.accounts.compactMap { account in + chains: $1.value.accounts.compactMap { account in Blockchain(namespace: account.namespace, reference: account.reference) - }), + }, methods: $1.value.methods, events: $1.value.events ) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Main/WalletConnectMainService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Main/WalletConnectMainService.swift index f61d0f2f47..8aeb73729a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Main/WalletConnectMainService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Main/WalletConnectMainService.swift @@ -241,7 +241,7 @@ extension WalletConnectMainService { Task { [weak self, service, blockchains] in do { - try await service.approve(proposal: proposal, accounts: Set(accounts), methods: blockchains.methods, events: blockchains.events) + try await service.approve(proposal: proposal, accounts: accounts, methods: blockchains.methods, events: blockchains.events) } catch { self?.errorRelay.accept(error) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Main/WalletConnectMainViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Main/WalletConnectMainViewController.swift index ec6f290cce..866fba87ca 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Main/WalletConnectMainViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Main/WalletConnectMainViewController.swift @@ -165,22 +165,27 @@ class WalletConnectMainViewController: ThemeViewController { } @objc private func onTapCancel() { + stat(page: .walletConnectSession, event: .cancel) viewModel.cancel() } @objc private func onTapConnect() { + stat(page: .walletConnectSession, event: .connect) viewModel.connect() } @objc private func onTapReject() { + stat(page: .walletConnectSession, event: .reject) viewModel.reject() } @objc private func onTapDisconnect() { + stat(page: .walletConnectSession, event: .disconnect) viewModel.disconnect() } @objc private func onTapReconnect() { + stat(page: .walletConnectSession, event: .reconnect) viewModel.reconnect() } @@ -224,7 +229,8 @@ class WalletConnectMainViewController: ThemeViewController { return case let .controller(controller): guard let controller else { return } - present(ThemeNavigationController(rootViewController: controller), animated: true) + stat(page: .walletConnectSession, event: .open(page: .walletConnectRequest)) + present(controller, animated: true) } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Pairings/WalletConnectPairingViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Pairings/WalletConnectPairingViewModel.swift index 04790eff8d..2edc41a7ec 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Pairings/WalletConnectPairingViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Pairings/WalletConnectPairingViewModel.swift @@ -50,10 +50,12 @@ extension WalletConnectPairingViewModel { } func onDisconnect(topic: String) { + stat(page: .walletConnectPairings, event: .delete(entity: .walletConnectPair)) service.disconnect(topic: topic) } func onDisconnectAll() { + stat(page: .walletConnectPairings, event: .delete(entity: .all)) service.disconnectAll() } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Request/Handlers/EthereumTransaction/WCEthereumTransactionPayload.swift b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Request/Handlers/EthereumTransaction/WCEthereumTransactionPayload.swift index 08d308ad4d..6760fd098c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Request/Handlers/EthereumTransaction/WCEthereumTransactionPayload.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Request/Handlers/EthereumTransaction/WCEthereumTransactionPayload.swift @@ -1,4 +1,5 @@ import Foundation +import ThemeKit import UIKit import WalletConnectSign @@ -35,7 +36,7 @@ class WCSendEthereumTransactionPayload: WCEthereumTransactionPayload { override class var method: String { "eth_sendTransaction" } override class var name: String { "Approve Transaction" } override class func module(request: WalletConnectRequest) -> UIViewController? { - WCSendEthereumTransactionRequestModule.viewController(request: request) + WalletConnectSendView(request: request).toNavigationViewController() } } @@ -43,6 +44,6 @@ class WCSignEthereumTransactionPayload: WCEthereumTransactionPayload { override class var method: String { "eth_signTransaction" } override class var name: String { "Sign Transaction" } override class func module(request: WalletConnectRequest) -> UIViewController? { - WCSignEthereumTransactionRequestModule.viewController(request: request) + WCSignEthereumTransactionRequestModule.viewController(request: request).map { ThemeNavigationController(rootViewController: $0) } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Request/Handlers/Sign/WCSignMessagePayload.swift b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Request/Handlers/Sign/WCSignMessagePayload.swift index 12a5058424..7549900db1 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Request/Handlers/Sign/WCSignMessagePayload.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/Request/Handlers/Sign/WCSignMessagePayload.swift @@ -1,4 +1,5 @@ import Foundation +import ThemeKit import UIKit import WalletConnectSign @@ -20,7 +21,7 @@ class WCSignMessagePayload: WCRequestPayload { } class func module(request: WalletConnectRequest) -> UIViewController? { - WCSignMessageRequestModule.viewController(request: request) + WCSignMessageRequestModule.viewController(request: request).map { ThemeNavigationController(rootViewController: $0) } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/WalletConnectRequest.swift b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/WalletConnectRequest.swift index 8cd5a67cf0..4edbc9fdbe 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/WalletConnectRequest.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/WalletConnectRequest.swift @@ -28,6 +28,10 @@ class WalletConnectRequest { self.chainName = chainName self.address = address } + + var description: String { + chainName ?? id.description + } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/WalletConnectService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/WalletConnectService.swift index a4dee9ece7..0483ce02f1 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/WalletConnectService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/WalletConnectService.swift @@ -1,6 +1,7 @@ import Combine import CryptoSwift import Foundation +import HsCryptoKit import HsToolKit import RxRelay import RxSwift @@ -11,6 +12,7 @@ import WalletConnectPairing import WalletConnectRelay import WalletConnectSign import WalletConnectUtils +import Web3Wallet extension Starscream.WebSocket: WebSocketConnecting {} @@ -43,39 +45,53 @@ class WalletConnectService { private var publishers = [AnyCancellable]() init(connectionService: WalletConnectSocketConnectionService, info: WalletConnectClientInfo, logger: Logger? = nil) { + let bundleIdentifier: String = Bundle.main.bundleIdentifier ?? "" + + Networking.configure( + groupIdentifier: "group.\(bundleIdentifier)", + projectId: info.projectId, + socketFactory: SocketFactory(), + socketConnectionType: .manual + ) + self.connectionService = connectionService + self.logger = logger let metadata = WalletConnectSign.AppMetadata( name: info.name, description: info.description, url: info.url, - icons: info.icons + icons: info.icons, + redirect: .init(native: DeepLinkManager.deepLinkScheme + "://", universal: nil) + ) + + Web3Wallet.configure( + metadata: metadata, + crypto: DefaultCryptoProvider() ) - Networking.configure(projectId: info.projectId, socketFactory: SocketFactory(), socketConnectionType: .manual) - Pair.configure(metadata: metadata) setUpAuthSubscribing() - connectionService.relayClient = Relay.instance + connectionService.relayClient = Networking.instance updateSessions() updatePairings() } func setUpAuthSubscribing() { - Sign.instance.socketConnectionStatusPublisher + Web3Wallet.instance.socketConnectionStatusPublisher .receive(on: DispatchQueue.main) .sink { [weak self] status in self?.didChangeSocketConnectionStatus(status) }.store(in: &publishers) - Sign.instance.sessionProposalPublisher + Web3Wallet.instance.sessionProposalPublisher .receive(on: DispatchQueue.main) .sink { [weak self] sessionProposal in self?.didReceive(sessionProposal: sessionProposal.proposal) }.store(in: &publishers) - Sign.instance.sessionSettlePublisher + Web3Wallet.instance.sessionSettlePublisher .receive(on: DispatchQueue.main) .sink { [weak self] session in self?.didSettle(session: session) @@ -87,13 +103,13 @@ class WalletConnectService { self?.didUpdate(sessionTopic: pair.sessionTopic, namespaces: pair.namespaces) }.store(in: &publishers) - Sign.instance.sessionRequestPublisher + Web3Wallet.instance.sessionRequestPublisher .receive(on: DispatchQueue.main) - .sink { [weak self] sessionRequest in - self?.didReceive(sessionRequest: sessionRequest.request) + .sink { [weak self] pair in + self?.didReceive(sessionRequest: pair.request) }.store(in: &publishers) - Sign.instance.sessionDeletePublisher + Web3Wallet.instance.sessionDeletePublisher .receive(on: DispatchQueue.main) .sink { [weak self] tuple in self?.didDelete(sessionTopic: tuple.0, reason: tuple.1) @@ -176,7 +192,7 @@ extension WalletConnectService { // works with pending requests public var pendingRequests: [WalletConnectSign.Request] { - Sign.instance.getPendingRequests() + Web3Wallet.instance.getPendingRequests().map(\.request) } public var pendingRequestsUpdatedObservable: Observable { @@ -185,7 +201,7 @@ extension WalletConnectService { // works with pairings public var pairings: [WalletConnectPairing.Pairing] { - Pair.instance.getPairings() + Web3Wallet.instance.getPairings() } public var pairingUpdatedObservable: Observable { @@ -196,7 +212,7 @@ extension WalletConnectService { Single.create { observer in Task { [weak self] in do { - try await Pair.instance.disconnect(topic: topic) + try await Web3Wallet.instance.disconnectPairing(topic: topic) self?.updatePairings() observer(.success(())) } catch { @@ -227,7 +243,7 @@ extension WalletConnectService { // works with dApp public func validate(uri: String) throws -> WalletConnectUtils.WalletConnectURI { - guard let uri = WalletConnectUtils.WalletConnectURI(string: uri) else { + guard let uri = try? WalletConnectUtils.WalletConnectURI(uriString: uri) else { throw WalletConnectUriHandler.ConnectionError.wrongUri } return uri @@ -236,7 +252,7 @@ extension WalletConnectService { public func pair(uri: WalletConnectUtils.WalletConnectURI) async throws { Task.init { [weak self] in do { - try await Pair.instance.pair(uri: uri) + try await Web3Wallet.instance.pair(uri: uri) self?.updatePairings() } catch { // can't pair with dApp, duplicate pairing or can't parse uri @@ -245,7 +261,7 @@ extension WalletConnectService { } } - public func approve(proposal: WalletConnectSign.Session.Proposal, accounts: Set, methods: Set, events: Set) async throws { + public func approve(proposal: WalletConnectSign.Session.Proposal, accounts: [WalletConnectUtils.Account], methods: Set, events: Set) async throws { logger?.debug("[WALLET] Approve Session: \(proposal.id)") Task { [logger] in do { @@ -254,7 +270,7 @@ extension WalletConnectService { methods: methods, events: events ) - try await Sign.instance.approve(proposalId: proposal.id, namespaces: ["eip155": eip155]) + _ = try await Web3Wallet.instance.approve(proposalId: proposal.id, namespaces: ["eip155": eip155]) } catch { logger?.error("WC v2 can't approve proposal, cause: \(error.localizedDescription)") throw error @@ -265,7 +281,7 @@ extension WalletConnectService { public func reject(proposal: WalletConnectSign.Session.Proposal) async throws { logger?.debug("[WALLET] Reject Session: \(proposal.id)") do { - try await Sign.instance.reject(proposalId: proposal.id, reason: .userRejected) + try await Web3Wallet.instance.rejectSession(proposalId: proposal.id, reason: .userRejected) } catch { logger?.error("WC v2 can't reject proposal, cause: \(error.localizedDescription)") throw error @@ -275,7 +291,7 @@ extension WalletConnectService { public func disconnect(topic: String, reason _: WalletConnectSign.Reason) { Task { [weak self, logger] in do { - try await Sign.instance.disconnect(topic: topic) + try await Web3Wallet.instance.disconnect(topic: topic) self?.updateSessions() } catch { logger?.error("WC v2 can't disconnect topic, cause: \(error.localizedDescription)") @@ -293,6 +309,7 @@ extension WalletConnectService { Task { [weak self] in do { try await Sign.instance.respond(topic: request.topic, requestId: request.id, response: .response(result)) + stat(page: .walletConnectRequest, event: .approveRequest(chainUid: request.chainId.absoluteString)) self?.pendingRequestsUpdatedRelay.accept(()) } } @@ -301,7 +318,8 @@ extension WalletConnectService { public func reject(request: WalletConnectSign.Request) { Task { [weak self] in do { - try await Sign.instance.respond(topic: request.topic, requestId: request.id, response: .error(.init(code: 5000, message: "Reject by User"))) + try await Web3Wallet.instance.respond(topic: request.topic, requestId: request.id, response: .error(.init(code: 5000, message: "Reject by User"))) + stat(page: .walletConnectRequest, event: .rejectRequest(chainUid: request.chainId.absoluteString)) self?.pendingRequestsUpdatedRelay.accept(()) } } @@ -365,3 +383,20 @@ extension WalletConnectSign.SocketConnectionStatus { } } } + +struct DefaultCryptoProvider: CryptoProvider { + public func recoverPubKey(signature: EthereumSignature, message: Data) throws -> Data { + let signature = Data(signature.r + signature.s + [signature.v]) + let messageHash = keccak256(message) + var pubKey = HsCryptoKit.Crypto.ellipticPublicKey(signature: signature, of: messageHash, compressed: false) + pubKey?.remove(at: 0) + + return pubKey ?? Data() + } + + public func keccak256(_ data: Data) -> Data { + let digest = SHA3(variant: .keccak256) + let hash = digest.calculate(for: [UInt8](data)) + return Data(hash) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/WalletConnectSocketConnectionService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/WalletConnectSocketConnectionService.swift index 2e4dcd473f..0b92997fa4 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/WalletConnectSocketConnectionService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/WalletConnect/WalletConnectSocketConnectionService.swift @@ -4,9 +4,11 @@ import HsToolKit import RxCocoa import RxRelay import RxSwift +import UIKit import WalletConnectRelay import WalletConnectSign import WalletConnectUtils +import Web3Wallet class WalletConnectSocketConnectionService { private static let retryInterval = 10 @@ -20,12 +22,11 @@ class WalletConnectSocketConnectionService { private let statusRelay = PublishRelay() private(set) var status = Status.disconnected { didSet { - logger?.debug("wc v2 change socket status: \(status)") statusRelay.accept(status) } } - weak var relayClient: RelayClient? { + var relayClient: NetworkingClient? { didSet { updateClient() } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/CellComponent.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/CellComponent.swift index 037c802aba..a237dbaf8e 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/CellComponent.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/CellComponent.swift @@ -351,10 +351,10 @@ enum AmountType { case neutral case secondary - var showSign: Bool { + var signType: ValueFormatter.SignType { switch self { - case .incoming, .outgoing, .secondary: return true - case .neutral: return false + case .incoming, .outgoing, .secondary: return .always + case .neutral: return .never } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/Cells/AmountInputCell.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/Cells/AmountInputCell.swift index 1e87606335..48fe336544 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/Cells/AmountInputCell.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/Cells/AmountInputCell.swift @@ -3,12 +3,15 @@ import ThemeKit import UIKit class AmountInputCell: UITableViewCell { + static let cornerRadius: CGFloat = .cornerRadius8 + private let formValidatedView: FormValidatedView private let formAmountInputView: FormAmountInputView init(viewModel: AmountInputViewModel) { formAmountInputView = FormAmountInputView(viewModel: viewModel) formValidatedView = FormValidatedView(contentView: formAmountInputView) + formValidatedView.set(cornerRadius: AmountInputCell.cornerRadius) super.init(style: .default, reuseIdentifier: nil) diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/Cells/Views/CheckboxView.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/Cells/Views/CheckboxView.swift index 93253d2566..ff13da7517 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/Cells/Views/CheckboxView.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/Cells/Views/CheckboxView.swift @@ -26,18 +26,17 @@ class CheckboxView: UIView { maker.size.equalTo(CheckboxView.checkBoxSize) } - checkBoxView.layer.cornerRadius = .cornerRadius4 - checkBoxView.layer.cornerCurve = .continuous - checkBoxView.layer.borderColor = UIColor.themeGray.cgColor - checkBoxView.layer.borderWidth = .heightOneDp + .heightOnePixel + checkBoxView.layer.cornerRadius = .cornerRadius12 + checkBoxView.layer.backgroundColor = UIColor.themeSteel20.cgColor checkBoxView.addSubview(checkBoxImageView) checkBoxImageView.snp.makeConstraints { maker in maker.center.equalToSuperview() } - checkBoxImageView.image = UIImage(named: "check_2_20")?.withRenderingMode(.alwaysTemplate) - checkBoxImageView.tintColor = .themeJacob + checkBoxImageView.image = UIImage(named: "check_2_24")?.withRenderingMode(.alwaysTemplate) + checkBoxImageView.isHidden = true + checkBoxImageView.tintColor = .themeDark addSubview(descriptionLabel) descriptionLabel.snp.makeConstraints { maker in @@ -68,7 +67,10 @@ class CheckboxView: UIView { var checked: Bool { get { !checkBoxImageView.isHidden } - set { checkBoxImageView.isHidden = !newValue } + set { + checkBoxImageView.isHidden = !newValue + checkBoxView.layer.backgroundColor = newValue ? UIColor.themeYellowD.cgColor : UIColor.themeSteel20.cgColor + } } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/Components/DiffLabel.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/Components/DiffLabel.swift index 3829540436..b897d91351 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/Components/DiffLabel.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/Components/DiffLabel.swift @@ -18,6 +18,11 @@ class DiffLabel: UILabel { textColor = Self.color(value: value, highlight: highlightText) } + func set(value: ValueDiff?) { + text = value?.value + textColor = value?.trend == .down ? .themeLucian : .themeRemus + } + func set(text: String?, color: UIColor) { self.text = text textColor = color diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/Components/FormValidatedView.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/Components/FormValidatedView.swift index 43e3af6a8a..ae872a1e79 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/Components/FormValidatedView.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/Components/FormValidatedView.swift @@ -19,7 +19,7 @@ class FormValidatedView: UIView { } wrapperView.backgroundColor = .themeLawrence - wrapperView.layer.cornerRadius = .cornerRadius8 + wrapperView.layer.cornerRadius = InputView.cornerRadius wrapperView.layer.cornerCurve = .continuous wrapperView.layer.borderWidth = CGFloat.heightOneDp wrapperView.layer.borderColor = UIColor.themeSteel20.cgColor @@ -37,6 +37,10 @@ class FormValidatedView: UIView { } extension FormValidatedView { + func set(cornerRadius: CGFloat) { + wrapperView.layer.cornerRadius = cornerRadius + } + func set(cautionType: CautionType?) { let borderColor: UIColor diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/Components/InputView.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/Components/InputView.swift index ea4dc15ec6..b5a0260e49 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/Components/InputView.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/Components/InputView.swift @@ -3,6 +3,8 @@ import ThemeKit import UIKit class InputView: UIView { + static let cornerRadius: CGFloat = .cornerRadius12 + private let formValidatedView: FormValidatedView private let inputStackView: InputStackView diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/Extensions/CellElement.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/Extensions/CellElement.swift index b16633546c..09203435e2 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/Extensions/CellElement.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/Extensions/CellElement.swift @@ -1,4 +1,5 @@ import ComponentKit +import MarketKit import UIKit extension CellBuilderNew.CellElement { // prepared cell elements for most frequency used layouts @@ -42,9 +43,12 @@ extension CellBuilderNew.CellElement { // prepared cell elements for most freque if let image = image.image { // setup local image component.imageView.image = image } else if let url = image.url { // setup global url with placeholder - component.imageView.setImage(withUrlString: url, placeholder: image.placeholder.flatMap { - UIImage(named: $0) - }) + component.imageView.setImage( + withUrlString: url, + placeholder: image.placeholder.flatMap { UIImage(named: $0) } + ) + } else if let coin = image.coin { + component.imageView.setImage(coin: coin) } else { component.isHidden = true } @@ -84,12 +88,14 @@ extension CellBuilderNew.CellElement { // prepared cell elements for most freque extension CellBuilderNew.CellElement { struct Image { - static func local(_ image: UIImage?) -> Self { Image(image: image, url: nil, placeholder: nil) } - static func url(_ url: String?, placeholder: String? = nil) -> Self { Image(image: nil, url: url, placeholder: placeholder) } + static func local(_ image: UIImage?) -> Self { Image(image: image, url: nil, placeholder: nil, coin: nil) } + static func url(_ url: String?, placeholder: String? = nil) -> Self { Image(image: nil, url: url, placeholder: placeholder, coin: nil) } + static func url(_ coin: Coin?) -> Self { Image(image: nil, url: nil, placeholder: nil, coin: coin) } let image: UIImage? let url: String? let placeholder: String? + let coin: Coin? } enum ImageSize { diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/Extensions/ChartConfiguration.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/Extensions/ChartConfiguration.swift index e563972abc..60c2670fa1 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/Extensions/ChartConfiguration.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/Extensions/ChartConfiguration.swift @@ -19,6 +19,10 @@ extension ChartConfiguration { ChartConfiguration().applyColors().applyBase().applyBars() } + static var baseHistogramChart: ChartConfiguration { + ChartConfiguration().applyColors().applyBase().applyHistogram() + } + static var volumeBarChart: ChartConfiguration { baseBarChart.applyVolume() } @@ -50,6 +54,15 @@ extension ChartConfiguration { return self } + @discardableResult private func clearGradients() -> Self { + let clear = [UIColor.clear] + trendUpGradient = clear + trendDownGradient = clear + pressedGradient = clear + neutralGradient = clear + return self + } + @discardableResult private func applyPreview(height: CGFloat, curveWidth: CGFloat = 2) -> Self { mainHeight = height indicatorHeight = 0 @@ -63,11 +76,7 @@ extension ChartConfiguration { showVerticalLines = false isInteractive = false - let clear = [UIColor.clear] - trendUpGradient = clear - trendDownGradient = clear - pressedGradient = clear - neutralGradient = clear + clearGradients() return self } @@ -76,10 +85,19 @@ extension ChartConfiguration { curveType = .bars curveBottomInset = 18 - trendUpGradient = [.clear] - trendDownGradient = [.clear] - pressedGradient = [.clear] - neutralGradient = [.clear] + clearGradients() + + return self + } + + @discardableResult private func applyHistogram() -> Self { + curveType = .histogram + curveBottomInset = 18 + + indicatorHeight = 0 + timelineHeight = 0 + + clearGradients() return self } @@ -137,4 +155,22 @@ public extension ChartIndicator.LineConfiguration { let indicator = PrecalculatedIndicator(id: MarketGlobalModule.dominance, enabled: true, values: [], configuration: dominance) return indicator.json } + + static var totalAssets: Self { + Self(color: ChartColor(.themeNina.withAlphaComponent(0.5)), width: 1) + } + + static var totalAssetId: String { + let indicator = PrecalculatedIndicator(id: MarketGlobalModule.totalAssets, enabled: true, values: [], configuration: totalAssets) + return indicator.json + } + + static var totalInflow: Self { + Self(color: ChartColor(.themeNina.withAlphaComponent(0.5)), width: 1) + } + + static var totalInflowId: String { + let indicator = PrecalculatedIndicator(id: MarketGlobalModule.totalInflow, enabled: false, values: [], configuration: totalAssets) + return indicator.json + } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/Extensions/UIImageView.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/Extensions/UIImageView.swift new file mode 100644 index 0000000000..ce67351262 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/Extensions/UIImageView.swift @@ -0,0 +1,37 @@ +import Alamofire +import Kingfisher +import MarketKit +import UIKit + +extension UIImageView { + func setImage(coin: Coin?, placeholder: String? = nil) { + let options: [KingfisherOptionsInfoItem] = [.scaleFactor(UIScreen.main.scale)] + let placeholder = UIImage(named: placeholder ?? "placeholder_circle_32") + + if let alternativeUrlString = coin?.image, let alternativeUrl = URL(string: alternativeUrlString) { + if ImageCache.default.isCached(forKey: alternativeUrlString) { + kf.setImage( + with: alternativeUrl, + placeholder: placeholder, + options: options + ) + } else { + kf.setImage( + with: URL(string: coin?.imageUrl ?? ""), + placeholder: placeholder, + options: options + [.alternativeSources([.network(alternativeUrl)])] + ) + } + } else { + kf.setImage( + with: URL(string: coin?.imageUrl ?? ""), + placeholder: placeholder, + options: options + ) + + return + } + + cornerRadius = CGFloat.iconSize32 / 2 + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/ScanQr/ScanQrView.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/ScanQr/ScanQrView.swift index 54e48b4b64..07bbe13b44 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/ScanQr/ScanQrView.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/ScanQr/ScanQrView.swift @@ -156,7 +156,7 @@ class ScanQrView: UIView { func startCaptureSession() { if let captureSession, !captureSession.isRunning { - DispatchQueue.main.async { + scanQueue.async { captureSession.startRunning() } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Alert/AlertView.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Alert/AlertView.swift index 596431085c..2b63c30b08 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Alert/AlertView.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Alert/AlertView.swift @@ -85,7 +85,7 @@ struct ButtonsAlertView: View { case true: opacity = 0 - withAnimation(.spring(response: 0.3, dampingFraction: 0.9, blendDuration: 0).delay(0.2)) { + withAnimation(.easeIn(duration: 0.2)) { opacity = 1 backgroundOpacity = 1 scale = 1 diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Alert/TextFieldAlert.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Alert/TextFieldAlert.swift new file mode 100644 index 0000000000..95494454b3 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Alert/TextFieldAlert.swift @@ -0,0 +1,122 @@ +import Combine +import SwiftUI +import UIKit + +class TextFieldAlertViewController: UIViewController { + private let alertTitle: String + private let message: String? + @Published private var text: String = "" + private var isPresented: Binding? + private var amountChanged: ((String) -> Void)? + + private var subscription: AnyCancellable? + + init(title: String, message: String?, initial: String, isPresented: Binding?, amountChanged: ((String) -> Void)?) { + alertTitle = title + self.message = message + text = initial + self.isPresented = isPresented + self.amountChanged = amountChanged + + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + presentAlertController() + } + + private func presentAlertController() { + guard subscription == nil else { return } // present only once + + let vc = UIAlertController(title: alertTitle, message: message, preferredStyle: .alert) + + // add a textField and create a subscription to update the `text` binding + vc.addTextField { [weak self] textField in + guard let self else { return } + textField.keyboardType = .decimalPad + textField.text = text + subscription = NotificationCenter.default + .publisher(for: UITextField.textDidChangeNotification, object: textField) + .map { ($0.object as? UITextField)?.text ?? "" } + .assign(to: \.text, on: self) + } + + // create a `Done` action that updates the `isPresented` binding when tapped + // this is just for Demo only but we should really inject + // an array of buttons (with their title, style and tap handler) + let actionCancel = UIAlertAction(title: "button.cancel".localized, style: .default) { [weak self] _ in + self?.isPresented?.wrappedValue = false + } + + let actionConfirm = UIAlertAction(title: "button.confirm".localized, style: .default) { [weak self] _ in + self?.amountChanged?(self?.text ?? "") + self?.isPresented?.wrappedValue = false + } + vc.addAction(actionCancel) + vc.addAction(actionConfirm) + present(vc, animated: true, completion: nil) + } +} + +struct TextFieldAlert { + // MARK: Properties + + let title: String + let message: String? + let initial: String + var isPresented: Binding? + var amountChanged: ((String) -> Void)? + + // MARK: Modifiers + + func dismissable(_ isPresented: Binding? = nil, _ amountChanged: ((String) -> Void)? = nil) -> TextFieldAlert { + TextFieldAlert(title: title, message: message, initial: initial, isPresented: isPresented, amountChanged: amountChanged) + } +} + +extension TextFieldAlert: UIViewControllerRepresentable { + typealias UIViewControllerType = TextFieldAlertViewController + + func makeUIViewController(context _: UIViewControllerRepresentableContext) -> UIViewControllerType { + TextFieldAlertViewController(title: title, message: message, initial: initial, isPresented: isPresented, amountChanged: amountChanged) + } + + func updateUIViewController(_: UIViewControllerType, + context _: UIViewControllerRepresentableContext) + { + // no update needed + } +} + +struct TextFieldWrapper: View { + @Binding var isPresented: Bool + var amountChanged: ((String) -> Void)? = nil + + let presentingView: PresentingView + let content: () -> TextFieldAlert + + var body: some View { + ZStack { + if isPresented { content().dismissable($isPresented, amountChanged) } + presentingView + } + } +} + +extension View { + func textFieldAlert(isPresented: Binding, + amountChanged: ((String) -> Void)?, + content: @escaping () -> TextFieldAlert) -> some View + { + TextFieldWrapper(isPresented: isPresented, + amountChanged: amountChanged, + presentingView: self, + content: content) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/BadgeViewNew.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/BadgeViewNew.swift index 893bebd6f4..bab7a640da 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/BadgeViewNew.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/BadgeViewNew.swift @@ -2,15 +2,35 @@ import SwiftUI import ThemeKit struct BadgeViewNew: View { - let text: String + private let text: String + private let change: Int? + + init(text: String, change: Int? = nil) { + self.text = text + self.change = change + } var body: some View { - Text(text.uppercased()) - .font(.themeMicroSB) - .foregroundColor(.themeBran) - .padding(.horizontal, .margin4) - .padding(.vertical, .margin2) - .background(RoundedRectangle(cornerRadius: .cornerRadius4, style: .continuous).fill(Color.themeJeremy)) - .clipShape(RoundedRectangle(cornerRadius: .cornerRadius4, style: .continuous)) + HStack(spacing: .margin2) { + Text(text.uppercased()) + .font(.themeMicroSB) + .foregroundColor(.themeBran) + + if let change, change != 0 { + if change > 0 { + Text(verbatim: "↑\(change)") + .font(.themeMicroSB) + .foregroundColor(.themeRemus) + } else { + Text(verbatim: "↓\(abs(change))") + .font(.themeMicroSB) + .foregroundColor(.themeLucian) + } + } + } + .padding(.horizontal, .margin6) + .padding(.vertical, .margin2) + .background(RoundedRectangle(cornerRadius: .cornerRadius8, style: .continuous).fill(Color.themeJeremy)) + .clipShape(RoundedRectangle(cornerRadius: .cornerRadius8, style: .continuous)) } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/BottomGradientWrapper.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/BottomGradientWrapper.swift index c5af015933..3e8d734778 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/BottomGradientWrapper.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/BottomGradientWrapper.swift @@ -7,9 +7,7 @@ struct BottomGradientWrapper: View { var body: some View { VStack(spacing: 0) { ZStack { - ScrollView { - content - } + content VStack { Spacer() diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/CheckBoxUiView.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/CheckBoxUiView.swift new file mode 100644 index 0000000000..25772903d3 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/CheckBoxUiView.swift @@ -0,0 +1,19 @@ +import SwiftUI + +struct CheckBoxUiView: View { + private let size: CGFloat = 24 + + @Binding var checked: Bool + + var body: some View { + ZStack { + Circle() + .fill(checked ? Color.themeYellow : Color.themeSteel20) + .frame(width: size, height: size) + + Image("check_2_24") + .themeIcon(color: .themeDark) + .opacity(checked ? 1 : 0) + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/CheckboxStyle.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/CheckboxStyle.swift index 4ff8103098..8e52ecbad3 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/CheckboxStyle.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/CheckboxStyle.swift @@ -2,21 +2,12 @@ import SwiftUI import ThemeKit struct CheckboxStyle: ToggleStyle { - private let size: CGFloat = .margin24 - .heightOneDp - func makeBody(configuration: Configuration) -> some View { Button(action: { configuration.isOn.toggle() }, label: { - Image("check_2_20") - .themeIcon(color: .themeJacob) - .opacity(configuration.isOn ? 1 : 0) - .frame(width: size, height: size, alignment: .center) + CheckBoxUiView(checked: configuration.$isOn) }) - .overlay( - RoundedRectangle(cornerRadius: .cornerRadius4, style: .continuous) - .stroke(Color.themeGray, lineWidth: .heightOneDp + .heightOnePixel) - ) configuration.label } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ClickableRow.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ClickableRow.swift index ce789ee79b..d0b3027ea6 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ClickableRow.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ClickableRow.swift @@ -1,11 +1,13 @@ import SwiftUI struct ClickableRow: View { + private let padding: EdgeInsets private let spacing: CGFloat private let action: () -> Void @ViewBuilder private let content: Content - init(spacing: CGFloat = .margin16, action: @escaping () -> Void, @ViewBuilder content: () -> Content) { + init(padding: EdgeInsets = EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin12, trailing: .margin16), spacing: CGFloat = .margin16, action: @escaping () -> Void, @ViewBuilder content: () -> Content) { + self.padding = padding self.spacing = spacing self.action = action self.content = content() @@ -13,11 +15,10 @@ struct ClickableRow: View { var body: some View { Button(action: action, label: { - ListRow(spacing: spacing) { + ListRow(padding: padding, spacing: spacing) { content } }) .buttonStyle(RowButtonStyle()) - .contentShape(Rectangle()) } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/CoinIconView.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/CoinIconView.swift new file mode 100644 index 0000000000..d1018ad7fb --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/CoinIconView.swift @@ -0,0 +1,43 @@ +import Kingfisher +import MarketKit +import SwiftUI + +struct CoinIconView: View { + let coin: Coin? + let placeholderImage: Image? + + init(coin: Coin?, placeholderImage: Image? = nil) { + self.coin = coin + self.placeholderImage = placeholderImage + } + + var body: some View { + if let alternativeUrlString = coin?.image, let alternativeUrl = URL(string: alternativeUrlString) { + if ImageCache.default.isCached(forKey: alternativeUrlString) { + icon(alternativeUrl) + .clipShape(Circle()) + .frame(width: .iconSize32, height: .iconSize32) + } else { + icon(coin.flatMap { URL(string: $0.imageUrl) }).alternativeSources([.network(alternativeUrl)]) + .clipShape(Circle()) + .frame(width: .iconSize32, height: .iconSize32) + } + } else { + icon(coin.flatMap { URL(string: $0.imageUrl) }) + .clipShape(Circle()) + .frame(width: .iconSize32, height: .iconSize32) + } + } + + @ViewBuilder func icon(_ url: URL?) -> some KFImageProtocol { + KFImage.url(url) + .resizable() + .placeholder { + if let placeholderImage { + placeholderImage + } else { + Circle().fill(Color.themeSteel20) + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/DiffText.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/DiffText.swift new file mode 100644 index 0000000000..6693dae373 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/DiffText.swift @@ -0,0 +1,57 @@ +import Foundation +import SwiftUI +import ThemeKit + +struct DiffText: View { + private let diff: Diff? + private let font: Font + + init(_ diff: Diff?, font: Font = .themeSubhead2) { + self.diff = diff + self.font = font + } + + init(_ diff: Decimal?, font: Font = .themeSubhead2) { + self.diff = diff.map { .percent(value: $0) } + self.font = font + } + + init(_ change: Decimal?, currency: Currency, font: Font = .themeSubhead2) { + diff = change.map { .change(value: $0, currency: currency) } + self.font = font + } + + var body: some View { + if let (text, value) = resolved { + Text(text) + .foregroundColor(value == 0 ? .themeGray : (value.isSignMinus ? .themeLucian : .themeRemus)) + .font(font) + .lineLimit(1) + .truncationMode(.middle) + } else { + Text("----") + .foregroundColor(.themeGray) + .font(font) + .lineLimit(1) + .truncationMode(.middle) + } + } + + private var resolved: (String, Decimal)? { + guard let diff else { + return nil + } + + switch diff { + case let .percent(value): return ValueFormatter.instance.format(percentValue: value).map { ($0, value) } + case let .change(value, currency): return ValueFormatter.instance.formatShort(currency: currency, value: value, signType: .always).map { ($0, value) } + } + } +} + +extension DiffText { + enum Diff { + case percent(value: Decimal) + case change(value: Decimal, currency: Currency) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Extensions/Text.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Extensions/Text.swift index e41b2fccdc..d16d1fa6cf 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Extensions/Text.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/Extensions/Text.swift @@ -1,7 +1,7 @@ import SwiftUI import ThemeKit -extension Text { +extension View { func textBody(color: Color = .themeLeah) -> some View { foregroundColor(color).font(.themeBody) } @@ -18,10 +18,18 @@ extension Text { foregroundColor(color).font(.themeCaption) } + func textCaptionSB(color: Color = .themeGray) -> some View { + foregroundColor(color).font(.themeCaptionSB) + } + func textHeadline1(color: Color = .themeLeah) -> some View { foregroundColor(color).font(.themeHeadline1) } + func textHeadline2(color: Color = .themeLeah) -> some View { + foregroundColor(color).font(.themeHeadline2) + } + func textMicro(color: Color = .themeGray) -> some View { foregroundColor(color).font(.themeMicro) } @@ -43,9 +51,7 @@ extension Text { } func themeCaptionSB(color: Color = .themeGray, alignment: Alignment = .leading) -> some View { - frame(maxWidth: .infinity, alignment: alignment) - .foregroundColor(color) - .font(.themeCaptionSB) + textCaptionSB(color: color).frame(maxWidth: .infinity, alignment: alignment) } func themeHeadline1(color: Color = .themeLeah, alignment: Alignment = .leading) -> some View { @@ -53,8 +59,10 @@ extension Text { } func themeHeadline2(color: Color = .themeLeah, alignment: Alignment = .leading) -> some View { - frame(maxWidth: .infinity, alignment: alignment) - .foregroundColor(color) - .font(.themeHeadline2) + textHeadline2(color: color).frame(maxWidth: .infinity, alignment: alignment) + } + + func themeMicro(color: Color = .themeGray, alignment: Alignment = .leading) -> some View { + textMicro(color: color).frame(maxWidth: .infinity, alignment: alignment) } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/FirstAppearModifier.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/FirstAppearModifier.swift index cddbdcff4e..725c95a7f2 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/FirstAppearModifier.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/FirstAppearModifier.swift @@ -1,6 +1,6 @@ import SwiftUI -struct FirstAppear: ViewModifier { +struct FirstAppearModifier: ViewModifier { let action: () -> Void @StateObject private var isFirstAppear = FirstAppearState() @@ -18,3 +18,9 @@ struct FirstAppear: ViewModifier { private class FirstAppearState: ObservableObject { @Published var value: Bool = true } + +extension View { + func onFirstAppear(action: @escaping () -> Void) -> some View { + modifier(FirstAppearModifier(action: action)) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InputTextRow.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InputTextRow.swift index 20a713dfeb..3720dd84b1 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InputTextRow.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InputTextRow.swift @@ -14,8 +14,8 @@ struct InputTextRow: View { content } .padding(EdgeInsets(top: vertical, leading: .margin16, bottom: vertical, trailing: .margin16)) - .background(RoundedRectangle(cornerRadius: .cornerRadius8, style: .continuous).fill(Color.themeLawrence)) - .overlay(RoundedRectangle(cornerRadius: .cornerRadius8, style: .continuous).stroke(Color.themeSteel20, lineWidth: .heightOneDp)) + .background(RoundedRectangle(cornerRadius: .cornerRadius12, style: .continuous).fill(Color.themeLawrence)) + .overlay(RoundedRectangle(cornerRadius: .cornerRadius12, style: .continuous).stroke(Color.themeSteel20, lineWidth: .heightOneDp)) .frame(minHeight: .heightSingleLineCell) } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InputTextView.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InputTextView.swift index 04b3bd70a8..bd2ef62d1e 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InputTextView.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/InputTextView.swift @@ -4,6 +4,7 @@ import ThemeKit struct InputTextView: View { var placeholder: String = "" var multiline: Bool + var font: Font var text: Binding @@ -14,9 +15,10 @@ struct InputTextView: View { var isValidText: ((String) -> Bool)? - init(placeholder: String = "", multiline: Bool = false, text: Binding, secured: Binding = .constant(false), isValidText: ((String) -> Bool)? = nil) { + init(placeholder: String = "", multiline: Bool = false, font: Font = .themeBody, text: Binding, secured: Binding = .constant(false), isValidText: ((String) -> Bool)? = nil) { self.placeholder = placeholder self.multiline = multiline + self.font = font self.text = text _secured = secured @@ -25,7 +27,7 @@ struct InputTextView: View { var body: some View { editView() - .font(.themeBody) + .font(font) .accentColor(.themeLeah) .modifier(Validated(text: text, isValidText: isValidText)) } @@ -80,7 +82,7 @@ struct CautionBorder: ViewModifier { let cornerRadius: CGFloat @Binding var cautionState: CautionState - init(cornerRadius: CGFloat = .cornerRadius8, cautionState: Binding) { + init(cornerRadius: CGFloat = InputView.cornerRadius, cautionState: Binding) { self.cornerRadius = cornerRadius _cautionState = cautionState } @@ -98,7 +100,7 @@ struct FieldCautionBorder: ViewModifier { let cornerRadius: CGFloat @Binding var cautionState: FieldCautionState - init(cornerRadius: CGFloat = .cornerRadius8, cautionState: Binding) { + init(cornerRadius: CGFloat = InputView.cornerRadius, cautionState: Binding) { self.cornerRadius = cornerRadius _cautionState = cautionState } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/MarqueeView.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/MarqueeView.swift new file mode 100644 index 0000000000..784d962583 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/MarqueeView.swift @@ -0,0 +1,145 @@ +import SwiftUI + +struct MarqueeView: View { + let content: Content + + @State private var containerWidth: CGFloat? = nil + @State private var model: Model + + private var targetVelocity: Double + private var spacing: CGFloat + + init(targetVelocity: Double, spacing: CGFloat = .margin8, @ViewBuilder content: () -> Content) { + self.content = content() + _model = .init(wrappedValue: Model(targetVelocity: targetVelocity, spacing: spacing)) + self.targetVelocity = targetVelocity + self.spacing = spacing + } + + var body: some View { + TimelineView(.animation) { context in + HStack(spacing: model.spacing) { + HStack(spacing: model.spacing) { + content + } + .measureWidth { model.contentWidth = $0 } + + ForEach(Array(0 ..< extraContentInstances), id: \.self) { _ in + content + } + } + .offset(x: model.offset) + .fixedSize() + .onChange(of: context.date) { newDate in + DispatchQueue.main.async { + model.tick(at: newDate) + } + } + } + .measureWidth { containerWidth = $0 } + .gesture(dragGesture) + .onAppear { model.previousTick = .now } + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + } + + private var extraContentInstances: Int { + let contentPlusSpacing = ((model.contentWidth ?? 0) + model.spacing) + + guard contentPlusSpacing != 0 else { + return 1 + } + + return Int(((containerWidth ?? 0) / contentPlusSpacing).rounded(.up)) + } + + private var dragGesture: some Gesture { + DragGesture(minimumDistance: 0) + .onChanged { value in + model.dragChanged(value) + }.onEnded { value in + model.dragEnded(value) + } + } +} + +extension MarqueeView { + struct Model { + var contentWidth: CGFloat? + var offset: CGFloat + var dragStartOffset: CGFloat? + var dragTranslation: CGFloat = 0 + var currentVelocity: CGFloat = 0 + + var previousTick: Date = .now + var targetVelocity: Double + var spacing: CGFloat + + init(targetVelocity: Double, spacing: CGFloat) { + self.targetVelocity = targetVelocity + self.spacing = spacing + + offset = spacing + } + + mutating func tick(at time: Date) { + let delta = time.timeIntervalSince(previousTick) + + defer { previousTick = time } + + currentVelocity += (targetVelocity - currentVelocity) * delta * 3 + + if let dragStartOffset { + offset = dragStartOffset + dragTranslation + } else { + offset -= delta * currentVelocity + } + + if let c = contentWidth { + offset.formTruncatingRemainder(dividingBy: c + spacing) + + while offset > 0 { + offset -= c + spacing + } + } + } + + mutating func dragChanged(_ value: DragGesture.Value) { + if dragStartOffset == nil { + dragStartOffset = offset + } + + dragTranslation = value.translation.width + } + + mutating func dragEnded(_ value: DragGesture.Value) { + guard let dragStartOffset else { + return + } + + offset = dragStartOffset + value.translation.width + + self.dragStartOffset = nil + + currentVelocity = (value.location.x - value.predictedEndLocation.x) * 3 + } + } +} + +extension View { + func measureWidth(_ onChange: @escaping (CGFloat) -> Void) -> some View { + background { + GeometryReader { proxy in + let width = proxy.size.width + + Color.clear + .onAppear { + DispatchQueue.main.async { + onChange(width) + } + }.onChange(of: width) { + onChange($0) + } + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/NavigationRow.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/NavigationRow.swift index 7c39a09b58..34cef02324 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/NavigationRow.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/NavigationRow.swift @@ -1,12 +1,25 @@ import SwiftUI struct NavigationRow: View { + private let padding: EdgeInsets + private let spacing: CGFloat + private let minHeight: CGFloat + @ViewBuilder let destination: Destination var isActive: Binding? @ViewBuilder let content: Content + init(padding: EdgeInsets = EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin12, trailing: .margin16), spacing: CGFloat = .margin16, minHeight: CGFloat = .heightCell48, @ViewBuilder destination: () -> Destination, isActive: Binding? = nil, @ViewBuilder content: () -> Content) { + self.padding = padding + self.spacing = spacing + self.minHeight = minHeight + self.destination = destination() + self.isActive = isActive + self.content = content() + } + var body: some View { - let row = ListRow { + let row = ListRow(padding: padding, spacing: spacing, minHeight: minHeight) { content } if let isActive { diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/PriceRow.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/PriceRow.swift new file mode 100644 index 0000000000..02101e88c5 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/PriceRow.swift @@ -0,0 +1,51 @@ +import Foundation +import MarketKit +import SwiftUI + +struct PriceRow: View { + let title: String + let tokenA: Token + let tokenB: Token + let amountA: Decimal + let amountB: Decimal + + @State private var flipped = false + + var body: some View { + if let text { + ListRow { + Text(title).textSubhead2() + + Spacer() + + Button(action: { + flipped.toggle() + }) { + HStack(spacing: .margin8) { + Text(text) + .textSubhead1(color: .themeLeah) + .multilineTextAlignment(.trailing) + + Image("arrow_swap_3_20").themeIcon() + } + } + } + } + } + + private var text: String? { + var showAsIn = amountA < amountB + + if flipped { + showAsIn.toggle() + } + + let _tokenA = showAsIn ? tokenA : tokenB + let _tokenB = showAsIn ? tokenB : tokenA + let _amountA = showAsIn ? amountA : amountB + let _amountB = showAsIn ? amountB : amountA + + let formattedValue = ValueFormatter.instance.formatFull(value: _amountB / _amountA, decimalCount: _tokenB.decimals) + return formattedValue.map { "1 \(_tokenA.coin.code) = \($0) \(_tokenB.coin.code)" } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/RedactedModifier.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/RedactedModifier.swift new file mode 100644 index 0000000000..c3b19b96e8 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/RedactedModifier.swift @@ -0,0 +1,17 @@ +import SwiftUI + +struct RedactedModifier: ViewModifier { + let value: Any? + + func body(content: Content) -> some View { + content + .redacted(reason: value == nil ? .placeholder : .init()) + .shimmering(active: value == nil) + } +} + +extension View { + func redacted(value: Any? = nil) -> some View { + modifier(RedactedModifier(value: value)) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/RowButtonStyle.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/RowButtonStyle.swift index b36285cc88..a8034caa07 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/RowButtonStyle.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/RowButtonStyle.swift @@ -5,6 +5,7 @@ struct RowButtonStyle: ButtonStyle { func makeBody(configuration: Self.Configuration) -> some View { configuration.label + .contentShape(Rectangle()) .modifier(ThemeListStyleButtonModifier(themeListStyle: listStyle, isPressed: configuration.isPressed)) } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ScrollableTabHeaderView.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ScrollableTabHeaderView.swift new file mode 100644 index 0000000000..5728900c66 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ScrollableTabHeaderView.swift @@ -0,0 +1,192 @@ +import SwiftUI + +struct ScrollableTabHeaderView: View { + let tabs: [String] + + @Binding var currentTabIndex: Int + @Namespace var menuItemTransition + + var body: some View { + ZStack(alignment: .bottom) { + Rectangle() + .fill(Color.themeSteel10) + .frame(maxWidth: .infinity) + .frame(height: 1) + + ScrollViewReader { proxy in + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: .margin8) { + ForEach(tabs.indices, id: \.self) { index in + item(title: tabs[index], isActive: index == currentTabIndex, namespace: menuItemTransition) + .onTapGesture { + withAnimation(.spring().speed(1.5)) { + currentTabIndex = index + } + } + } + } + .padding(.horizontal, .margin12) + } + .onChange(of: currentTabIndex) { index in + withAnimation(.spring().speed(1.5)) { + proxy.scrollTo(index, anchor: .center) + } + } + .onFirstAppear { + proxy.scrollTo(currentTabIndex, anchor: .center) + } + } + } + .animation(.spring().speed(1.5), value: currentTabIndex) + } + + @ViewBuilder private func item(title: String, isActive: Bool, namespace: Namespace.ID) -> some View { + if isActive { + Text(title) + .font(.themeSubhead1) + .foregroundColor(.themeLeah) + .contentShape(Rectangle()) + .padding(.horizontal, .margin12) + .frame(height: 44) + .overlay(alignment: .bottom) { + RoundedRectangle(cornerRadius: 2, style: .continuous) + .fill(Color.themeJacob) + .frame(height: 4) + .offset(x: 0, y: 2) + .matchedGeometryEffect(id: "highlightmenuitem", in: namespace) + } + } else { + Text(title) + .font(.themeSubhead1) + .foregroundColor(.themeGray) + .contentShape(Rectangle()) + .padding(.horizontal, .margin12) + .frame(height: 44) + } + } +} + +public struct Shimmer: ViewModifier { + private let animation: Animation + private let gradient: Gradient + private let min, max: CGFloat + @State private var isInitialState = true + @Environment(\.layoutDirection) private var layoutDirection + + /// Initializes his modifier with a custom animation, + /// - Parameters: + /// - animation: A custom animation. Defaults to ``Shimmer/defaultAnimation``. + /// - gradient: A custom gradient. Defaults to ``Shimmer/defaultGradient``. + /// - bandSize: The size of the animated mask's "band". Defaults to 0.3 unit points, which corresponds to + /// 30% of the extent of the gradient. + public init( + animation: Animation = Self.defaultAnimation, + gradient: Gradient = Self.defaultGradient, + bandSize: CGFloat = 0.3 + ) { + self.animation = animation + self.gradient = gradient + // Calculate unit point dimensions beyond the gradient's edges by the band size + self.min = 0 - bandSize + self.max = 1 + bandSize + } + + /// The default animation effect. + public static let defaultAnimation = Animation.linear(duration: 1.5).delay(0.25).repeatForever(autoreverses: false) + + // A default gradient for the animated mask. + public static let defaultGradient = Gradient(colors: [ + .black.opacity(0.3), // translucent + .black, // opaque + .black.opacity(0.3), // translucent + ]) + + /* + Calculating the gradient's animated start and end unit points: + min,min + \ + ┌───────┐ ┌───────┐ + │0,0 │ Animate │ │ "forward" gradient + LTR │ │ ───────►│ 1,1│ / // / + └───────┘ └───────┘ + \ + max,max + max,min + / + ┌───────┐ ┌───────┐ + │ 1,0│ Animate │ │ "backward" gradient + RTL │ │ ───────►│0,1 │ \ \\ \ + └───────┘ └───────┘ + / + min,max + */ + + /// The start unit point of our gradient, adjusting for layout direction. + var startPoint: UnitPoint { + if layoutDirection == .rightToLeft { + return isInitialState ? UnitPoint(x: max, y: min) : UnitPoint(x: 0, y: 1) + } else { + return isInitialState ? UnitPoint(x: min, y: min) : UnitPoint(x: 1, y: 1) + } + } + + /// The end unit point of our gradient, adjusting for layout direction. + var endPoint: UnitPoint { + if layoutDirection == .rightToLeft { + return isInitialState ? UnitPoint(x: 1, y: 0) : UnitPoint(x: min, y: max) + } else { + return isInitialState ? UnitPoint(x: 0, y: 0) : UnitPoint(x: max, y: max) + } + } + + public func body(content: Content) -> some View { + content + .mask(LinearGradient(gradient: gradient, startPoint: startPoint, endPoint: endPoint)) + .animation(animation, value: isInitialState) + .onAppear { + // Delay the animation until the initial layout is established + // to prevent animating the appearance of the view + DispatchQueue.main.asyncAfter(deadline: .now()) { + isInitialState = false + } + } + } +} + +public extension View { + /// Adds an animated shimmering effect to any view, typically to show that an operation is in progress. + /// - Parameters: + /// - active: Convenience parameter to conditionally enable the effect. Defaults to `true`. + /// - animation: A custom animation. Defaults to ``Shimmer/defaultAnimation``. + /// - gradient: A custom gradient. Defaults to ``Shimmer/defaultGradient``. + /// - bandSize: The size of the animated mask's "band". Defaults to 0.3 unit points, which corresponds to + /// 20% of the extent of the gradient. + @ViewBuilder func shimmering( + active: Bool = true, + animation: Animation = Shimmer.defaultAnimation, + gradient: Gradient = Shimmer.defaultGradient, + bandSize: CGFloat = 0.3 + ) -> some View { + if active { + modifier(Shimmer(animation: animation, gradient: gradient, bandSize: bandSize)) + } else { + self + } + } + + /// Adds an animated shimmering effect to any view, typically to show that an operation is in progress. + /// - Parameters: + /// - active: Convenience parameter to conditionally enable the effect. Defaults to `true`. + /// - duration: The duration of a shimmer cycle in seconds. + /// - bounce: Whether to bounce (reverse) the animation back and forth. Defaults to `false`. + /// - delay:A delay in seconds. Defaults to `0.25`. + @available(*, deprecated, message: "Use shimmering(active:animation:gradient:bandSize:) instead.") + @ViewBuilder func shimmering( + active: Bool = true, duration: Double, bounce: Bool = false, delay: Double = 0.25 + ) -> some View { + shimmering( + active: active, + animation: .linear(duration: duration).delay(delay).repeatForever(autoreverses: bounce) + ) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SearchBar.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SearchBar.swift index 9cadd676d2..153cbfd27d 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SearchBar.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SearchBar.swift @@ -21,3 +21,45 @@ struct SearchBar: View { .padding(.bottom, .margin12) } } + +struct SearchBarWithCancel: View { + @Binding var text: String + let prompt: String + @FocusState.Binding var focused: Bool + + @State private var cancelVisible = false + + var body: some View { + ZStack { + HStack(spacing: .margin12) { + HStack(spacing: 6) { + Image(systemName: "magnifyingglass").themeIcon(color: .themeGray) + TextField("", text: $text, prompt: Text(prompt) + .foregroundColor(.themeGray)) + .font(.themeBody) + .focused($focused) + } + .padding(.horizontal, .margin8) + .padding(.vertical, 7) + .background(Color.themeSteel.opacity(0.12)) + .clipShape(RoundedRectangle(cornerRadius: 11, style: .continuous)) + + if cancelVisible { + Button(action: { + focused = false + text = "" + }) { + Text("button.cancel".localized) + } + .transition(.move(edge: .trailing).combined(with: .opacity)) + } + } + .animation(.easeOut(duration: 0.2), value: cancelVisible) + } + .padding(.horizontal, .margin16) + .padding(.bottom, .margin12) + .onChange(of: focused) { focused in + cancelVisible = focused + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryActiveButtonStyle.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryActiveButtonStyle.swift index 55b2500ce6..a7c76dec9a 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryActiveButtonStyle.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryActiveButtonStyle.swift @@ -14,12 +14,14 @@ struct SecondaryActiveButtonStyle: ButtonStyle { @Environment(\.isEnabled) var isEnabled func makeBody(configuration: Configuration) -> some View { - HStack(spacing: .margin4) { + HStack(spacing: .margin2) { accessoryView(accessory: leftAccessory, configuration: configuration) labelView(configuration: configuration) accessoryView(accessory: rightAccessory, configuration: configuration) } - .padding(EdgeInsets(top: 5.5, leading: .margin16, bottom: 5.5, trailing: .margin16)) + .padding(.leading, leftAccessory.padding) + .padding(.trailing, rightAccessory.padding) + .frame(height: 28) .background(style.backgroundColor(isEnabled: isEnabled, isPressed: configuration.isPressed)) .clipShape(Capsule(style: .continuous)) .animation(.easeOut(duration: 0.2), value: configuration.isPressed) @@ -27,7 +29,7 @@ struct SecondaryActiveButtonStyle: ButtonStyle { @ViewBuilder func labelView(configuration: Configuration) -> some View { configuration.label - .font(.themeSubhead1) + .font(.themeCaptionSB) .foregroundColor(style.foregroundColor(isEnabled: isEnabled, isPressed: configuration.isPressed)) } @@ -77,6 +79,13 @@ struct SecondaryActiveButtonStyle: ButtonStyle { } } + var padding: CGFloat { + switch self { + case .none: return .margin16 + default: return .margin8 + } + } + func foregroundColor(isEnabled: Bool, isPressed _: Bool) -> Color { switch self { case let .custom(_, enabled, disabled): return isEnabled ? enabled : disabled diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryButtonStyle.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryButtonStyle.swift index b5a9bbb7c2..5e96ad9267 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryButtonStyle.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryButtonStyle.swift @@ -14,12 +14,14 @@ struct SecondaryButtonStyle: ButtonStyle { @Environment(\.isEnabled) var isEnabled func makeBody(configuration: Configuration) -> some View { - HStack(spacing: .margin4) { + HStack(spacing: .margin2) { accessoryView(accessory: leftAccessory, configuration: configuration) labelView(configuration: configuration) accessoryView(accessory: rightAccessory, configuration: configuration) } - .padding(EdgeInsets(top: 5.5, leading: .margin16, bottom: 5.5, trailing: .margin16)) + .padding(.leading, leftAccessory.padding) + .padding(.trailing, rightAccessory.padding) + .frame(height: 28) .background(style.backgroundColor(isEnabled: isEnabled, isPressed: configuration.isPressed)) .clipShape(Capsule(style: .continuous)) .animation(.easeOut(duration: 0.2), value: configuration.isPressed) @@ -27,13 +29,13 @@ struct SecondaryButtonStyle: ButtonStyle { @ViewBuilder func labelView(configuration: Configuration) -> some View { configuration.label - .font(.themeSubhead1) + .font(.themeCaptionSB) .foregroundColor(style.foregroundColor(isEnabled: isEnabled, isPressed: configuration.isPressed)) } @ViewBuilder func accessoryView(accessory: Accessory, configuration: Configuration) -> some View { - if let icon = accessory.icon { - Image(icon) + if let image = accessory.image { + image .renderingMode(.template) .foregroundColor(accessory.foregroundColor(isEnabled: isEnabled, isPressed: configuration.isPressed)) } else { @@ -67,14 +69,21 @@ struct SecondaryButtonStyle: ButtonStyle { case none case dropDown case info - case custom(icon: String, pressedColor: Color = Self.pressedColor, activeColor: Color = Self.enabledColor, disabledColor: Color = Self.disabledColor) + case custom(image: Image, pressedColor: Color = Self.pressedColor, activeColor: Color = Self.enabledColor, disabledColor: Color = Self.disabledColor) - var icon: String? { + var image: Image? { switch self { case .none: return nil - case .dropDown: return "arrow_small_down_20" - case .info: return "circle_information_20" - case let .custom(icon, _, _, _): return icon + case .dropDown: return Image("arrow_small_down_20") + case .info: return Image("circle_information_20") + case let .custom(image, _, _, _): return image + } + } + + var padding: CGFloat { + switch self { + case .none: return .margin16 + default: return .margin8 } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryCircleButtonStyle.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryCircleButtonStyle.swift index c506e248fb..398b89e911 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryCircleButtonStyle.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryCircleButtonStyle.swift @@ -1,15 +1,21 @@ import SwiftUI struct SecondaryCircleButtonStyle: ButtonStyle { - let style: Style + private let style: Style + private let isActive: Bool @Environment(\.isEnabled) private var isEnabled + init(style: Style = .default, isActive: Bool = false) { + self.style = style + self.isActive = isActive + } + func makeBody(configuration: Configuration) -> some View { configuration.label .padding(.margin4) - .foregroundColor(style.foregroundColor(isEnabled: isEnabled, isPressed: configuration.isPressed)) - .background(style.backgroundColor(isEnabled: isEnabled, isPressed: configuration.isPressed)) + .foregroundColor(style.foregroundColor(isEnabled: isEnabled, isActive: isActive, isPressed: configuration.isPressed)) + .background(style.backgroundColor(isEnabled: isEnabled, isActive: isActive, isPressed: configuration.isPressed)) .clipShape(Circle()) .animation(.easeOut(duration: 0.2), value: configuration.isPressed) } @@ -19,17 +25,18 @@ struct SecondaryCircleButtonStyle: ButtonStyle { case transparent case red - func foregroundColor(isEnabled: Bool, isPressed: Bool) -> Color { + func foregroundColor(isEnabled: Bool, isActive: Bool, isPressed: Bool) -> Color { switch self { - case .default: return isEnabled ? (isPressed ? .themeGray : .themeLeah) : .themeGray50 + case .default: return isEnabled ? (isActive ? .themeDark : (isPressed ? .themeGray : .themeLeah)) : .themeGray50 case .transparent: return isEnabled ? (isPressed ? .themeGray50 : .themeGray) : .themeGray50 case .red: return isEnabled ? (isPressed ? .themeRed50 : .themeLucian) : .themeGray50 } } - func backgroundColor(isEnabled _: Bool, isPressed: Bool) -> Color { + func backgroundColor(isEnabled _: Bool, isActive: Bool, isPressed: Bool) -> Color { switch self { - case .default, .red: return isPressed ? .themeSteel10 : .themeSteel20 + case .default: return isActive ? (isPressed ? .themeYellow50 : .themeYellow) : (isPressed ? .themeSteel10 : .themeSteel20) + case .red: return isPressed ? .themeSteel10 : .themeSteel20 case .transparent: return .clear } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SelectorButtonStyle.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SelectorButtonStyle.swift index e8ea533fae..2c4bdf53a3 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SelectorButtonStyle.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SelectorButtonStyle.swift @@ -7,9 +7,9 @@ struct SelectorButtonStyle: ButtonStyle { @Environment(\.isEnabled) private var isEnabled func makeBody(configuration: Configuration) -> some View { - HStack(spacing: .margin4) { + HStack(spacing: .margin2) { configuration.label - .font(.themeSubhead1) + .font(.themeCaptionSB) .foregroundColor(isEnabled ? (configuration.isPressed ? .themeGray : .themeLeah) : .themeGray50) VStack(spacing: count == 2 ? 4 : 2) { @@ -28,7 +28,9 @@ struct SelectorButtonStyle: ButtonStyle { } .frame(width: .iconSize20, height: .iconSize20) } - .padding(EdgeInsets(top: .margin4, leading: .margin16, bottom: .margin4, trailing: .margin12)) + .padding(.leading, .margin16) + .padding(.trailing, .margin12) + .frame(height: 28) .background(isEnabled ? (configuration.isPressed ? Color.themeSteel10 : Color.themeSteel20) : Color.themeSteel20) .clipShape(Capsule(style: .continuous)) .animation(.easeOut(duration: 0.2), value: configuration.isPressed) diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ShortcutButtonsView.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ShortcutButtonsView.swift index af20c23b4d..91925b75e7 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ShortcutButtonsView.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ShortcutButtonsView.swift @@ -26,7 +26,7 @@ struct ShortcutButtonsView: View { Button(action: { onTap(index) }, label: { - Text(title).textSubhead1(color: .themeLeah) + Text(title) }) .buttonStyle(SecondaryButtonStyle(style: .default)) case let .icon(name): diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ThemeList.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ThemeList.swift index e620aa6bc4..e78b7a2092 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ThemeList.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ThemeList.swift @@ -1,39 +1,102 @@ import SwiftUI +import UIKit -struct ThemeList: View { - let items: [Item] - @ViewBuilder let itemContent: (Item) -> Content +let themeListTopViewId = "theme_list_top_view_id" - @Environment(\.themeListStyle) var themeListStyle +struct ThemeList: View { + private let content: () -> Content + private let bottomSpacing: CGFloat? + private let invisibleTopView: Bool + + init(bottomSpacing: CGFloat? = nil, invisibleTopView: Bool = false, @ViewBuilder _ content: @escaping () -> Content) { + self.bottomSpacing = bottomSpacing + self.invisibleTopView = invisibleTopView + self.content = content + } + + init( + _ items: [Item], + bottomSpacing: CGFloat? = nil, + invisibleTopView: Bool = false, + onMove: ((IndexSet, Int) -> Void)? = nil, + @ViewBuilder itemContent: @escaping (Item) -> ItemContent + ) where Content == ListForEach { + self.bottomSpacing = bottomSpacing + self.invisibleTopView = invisibleTopView + + content = { + ListForEach(items, onMove: onMove, itemContent: itemContent) + } + } var body: some View { - switch themeListStyle { - case .lawrence, .bordered, .transparentInline, .borderedLawrence: - Text("todo") - case .transparent: - List { - ForEach(items, id: \.self) { item in - VStack(spacing: 0) { - if items.first == item { - HorizontalDivider() - } - - itemContent(item) - - HorizontalDivider() - } + List { + if invisibleTopView { + Color.clear + .id(themeListTopViewId) + .frame(height: 0) .listRowBackground(Color.clear) .listRowInsets(EdgeInsets()) .listRowSeparator(.hidden) - } + } + + content() + if let bottomSpacing { Spacer() - .frame(height: .margin16) + .frame(height: bottomSpacing) .listRowBackground(Color.clear) .listRowInsets(EdgeInsets()) .listRowSeparator(.hidden) } - .listStyle(.plain) } + .environment(\.defaultMinListRowHeight, 0) + .listStyle(.plain) + .themeListStyle(.transparent) + } +} + +struct ThemeListSectionHeader: View { + let text: String + + var body: some View { + Text(text) + .themeSubhead1(alignment: .leading) + .textCase(.uppercase) + .padding(.horizontal, .margin16) + .frame(height: 44) + .frame(maxWidth: .infinity) + .listRowInsets(EdgeInsets()) + .background(Color.themeTyler) + } +} + +struct ListForEach: View { + private let items: [Item] + private let onMove: ((IndexSet, Int) -> Void)? + private let itemContent: (Item) -> Content + + init(_ items: [Item], onMove: ((IndexSet, Int) -> Void)? = nil, @ViewBuilder itemContent: @escaping (Item) -> Content) { + self.items = items + self.onMove = onMove + self.itemContent = itemContent + } + + var body: some View { + ForEach(items, id: \.self) { item in + VStack(spacing: 0) { + if items.first == item { + HorizontalDivider() + } + + itemContent(item) + + HorizontalDivider() + } + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + } + .onMove(perform: onMove) } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ThemeListStyle.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ThemeListStyle.swift index 23b52235bd..6ac85213dc 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ThemeListStyle.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ThemeListStyle.swift @@ -31,7 +31,7 @@ struct ThemeListStyleModifier: ViewModifier { case .bordered: content .clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)) - .overlay(RoundedRectangle(cornerRadius: cornerRadius).stroke(selected ? Color.themeJacob : Color.themeSteel20, lineWidth: .heightOneDp)) + .overlay(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous).stroke(selected ? Color.themeJacob : Color.themeSteel20, lineWidth: .heightOneDp)) case .transparent, .transparentInline: content } @@ -45,7 +45,8 @@ struct ThemeListStyleButtonModifier: ViewModifier { func body(content: Content) -> some View { switch themeListStyle { case .lawrence, .borderedLawrence: content.background(isPressed ? Color.themeLawrencePressed : Color.themeLawrence) - case .bordered, .transparent, .transparentInline: content.background(isPressed ? Color.themeLawrencePressed : Color.themeTyler) + case .bordered: content.background(isPressed ? Color.themeLawrencePressed : Color.clear) + case .transparent, .transparentInline: content.background(isPressed ? Color.themeLawrencePressed : Color.themeTyler) } } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/TabHeaderView.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/TabHeaderView.swift index 5c4f39dcf8..b95321a33f 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/TabHeaderView.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/TabHeaderView.swift @@ -37,6 +37,9 @@ struct TabHeaderView: View { offset = geo.size.width / CGFloat(tabs.count) * CGFloat(index) } } + .onAppear { + offset = geo.size.width / CGFloat(tabs.count) * CGFloat(currentTabIndex) + } } .frame(height: 2) } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/ValueFormatter.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/ValueFormatter.swift index 61d5bd4960..234d9285fc 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/ValueFormatter.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/ValueFormatter.swift @@ -142,7 +142,7 @@ class ValueFormatter { return (value: value, digits: max(digits, minDigits)) } - private func decorated(string: String, suffix: String? = nil, symbol: String? = nil, signValue: Decimal? = nil, tooSmall: Bool = false) -> String { + private func decorated(string: String, suffix: String? = nil, symbol: String? = nil, signType: SignType = .never, signValue: Decimal, tooSmall: Bool = false) -> String { var string = string if let suffix { @@ -153,12 +153,14 @@ class ValueFormatter { string = "\(string) \(symbol)" } - if let signValue { - var sign = "" - if !signValue.isZero { - sign = signValue.isSignMinus ? "-" : "+" - } - string = "\(sign)\(string)" + var sign = "" + if !signValue.isZero { + sign = signValue.isSignMinus ? "-" : "+" + } + switch signType { + case .never: () + case .always: string = "\(sign)\(string)" + case .auto: if signValue.isSignMinus { string = "\(sign)\(string)" } } if tooSmall { @@ -189,7 +191,7 @@ class ValueFormatter { return nil } - return pattern.replacingOccurrences(of: "1", with: decorated(string: string, suffix: suffix)) + return pattern.replacingOccurrences(of: "1", with: decorated(string: string, suffix: suffix, signValue: value)) } } @@ -206,10 +208,10 @@ extension ValueFormatter { return nil } - return decorated(string: string, suffix: suffix, tooSmall: tooSmall) + return decorated(string: string, suffix: suffix, signValue: value, tooSmall: tooSmall) } - func formatShort(value: Decimal, decimalCount: Int, symbol: String? = nil, showSign: Bool = false) -> String? { + func formatShort(value: Decimal, decimalCount: Int, symbol: String? = nil, signType: SignType = .never) -> String? { let (transformedValue, digits, suffix, tooSmall) = transformedShort(value: value, maxDigits: decimalCount) let string: String? = rawFormatterQueue.sync { @@ -221,10 +223,10 @@ extension ValueFormatter { return nil } - return decorated(string: string, suffix: suffix, symbol: symbol, signValue: showSign ? value : nil, tooSmall: tooSmall) + return decorated(string: string, suffix: suffix, symbol: symbol, signType: signType, signValue: value, tooSmall: tooSmall) } - func formatFull(value: Decimal, decimalCount: Int, symbol: String? = nil, showSign: Bool = false) -> String? { + func formatFull(value: Decimal, decimalCount: Int, symbol: String? = nil, signType: SignType = .never) -> String? { let (transformedValue, digits) = transformedFull(value: value, maxDigits: decimalCount, minDigits: min(decimalCount, 4)) let string: String? = rawFormatterQueue.sync { @@ -236,46 +238,46 @@ extension ValueFormatter { return nil } - return decorated(string: string, symbol: symbol, signValue: showSign ? value : nil) + return decorated(string: string, symbol: symbol, signType: signType, signValue: value) } - func formatShort(coinValue: CoinValue, showCode: Bool = true, showSign: Bool = false) -> String? { - formatShort(value: coinValue.value, decimalCount: coinValue.decimals, symbol: showCode ? coinValue.symbol : nil, showSign: showSign) + func formatShort(coinValue: CoinValue, showCode: Bool = true, signType: SignType = .never) -> String? { + formatShort(value: coinValue.value, decimalCount: coinValue.decimals, symbol: showCode ? coinValue.symbol : nil, signType: signType) } - func formatFull(coinValue: CoinValue, showCode: Bool = true, showSign: Bool = false) -> String? { - formatFull(value: coinValue.value, decimalCount: coinValue.decimals, symbol: showCode ? coinValue.symbol : nil, showSign: showSign) + func formatFull(coinValue: CoinValue, showCode: Bool = true, signType: SignType = .never) -> String? { + formatFull(value: coinValue.value, decimalCount: coinValue.decimals, symbol: showCode ? coinValue.symbol : nil, signType: signType) } - func formatShort(currency: Currency, value: Decimal, showSign: Bool = false) -> String? { + func formatShort(currency: Currency, value: Decimal, signType: SignType = .never) -> String? { let (transformedValue, digits, suffix, tooSmall) = transformedShort(value: value) guard let string = formattedCurrency(value: transformedValue, digits: digits, code: currency.code, symbol: currency.symbol, suffix: suffix) else { return nil } - return decorated(string: string, signValue: showSign ? value : nil, tooSmall: tooSmall) + return decorated(string: string, signType: signType, signValue: value, tooSmall: tooSmall) } - func formatShort(currencyValue: CurrencyValue, showSign: Bool = false) -> String? { - formatShort(currency: currencyValue.currency, value: currencyValue.value, showSign: showSign) + func formatShort(currencyValue: CurrencyValue, signType: SignType = .never) -> String? { + formatShort(currency: currencyValue.currency, value: currencyValue.value, signType: signType) } - func formatFull(currency: Currency, value: Decimal, showSign: Bool = false) -> String? { + func formatFull(currency: Currency, value: Decimal, signType: SignType = .never) -> String? { let (transformedValue, digits) = transformedFull(value: value, maxDigits: 18) guard let string = formattedCurrency(value: transformedValue, digits: digits, code: currency.code, symbol: currency.symbol) else { return nil } - return decorated(string: string, signValue: showSign ? value : nil) + return decorated(string: string, signType: signType, signValue: value) } - func formatFull(currencyValue: CurrencyValue, showSign: Bool = false) -> String? { - formatFull(currency: currencyValue.currency, value: currencyValue.value, showSign: showSign) + func formatFull(currencyValue: CurrencyValue, signType: SignType = .never) -> String? { + formatFull(currency: currencyValue.currency, value: currencyValue.value, signType: signType) } - func format(percentValue: Decimal, showSign: Bool = true) -> String? { + func format(percentValue: Decimal, signType: SignType = .always) -> String? { let (transformedValue, digits) = transformedFull(value: percentValue, maxDigits: 2) let string: String? = rawFormatterQueue.sync { @@ -287,6 +289,14 @@ extension ValueFormatter { return nil } - return decorated(string: string, signValue: showSign ? percentValue : nil) + "%" + return decorated(string: string, signType: signType, signValue: percentValue) + "%" + } +} + +extension ValueFormatter { + enum SignType { + case never + case auto + case always } } diff --git a/UnstoppableWallet/UnstoppableWallet/de.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/de.lproj/Localizable.strings index 04fda78f83..8afadcad6e 100644 --- a/UnstoppableWallet/UnstoppableWallet/de.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/de.lproj/Localizable.strings @@ -228,6 +228,7 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "extended_key.purpose" = "Zweck"; "extended_key.blockchain" = "Blockchain"; "extended_key.account" = "Account"; +"extended_key.account.description" = "Dies ist eine Einstellung für fortgeschrittene Benutzer. Wenn Sie versuchen, ein Wallet (über einen erweiterten privaten Schlüssel) oder eine Transaktionsliste (über einen erweiterten öffentlichen Schlüssel) zu importieren, benötigen Sie das Konto 0."; "extended_key.tap_to_show" = "Zum Anzeigen des erweiterten privaten Schlüssels tippen"; // Backup @@ -318,7 +319,6 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "balance.downloading_blocks" = "Blöcke herunterladen"; "balance.scanning_blocks" = "Scanne Blöcke"; "balance.enhancing_transactions" = "Transaktionen verbessern"; -"wait_for_synchronization" = "Warten auf Synchronisation"; "balance.searching.count" = "%@ tx"; "balance.syncing_percent" = "Synchronisieren... %@"; @@ -348,9 +348,11 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "balance.token.staked" = "Absteckung"; "balance.token.staked.info.title" = "Absteckter Titel"; "balance.token.staked.info.description" = "Absteckungstext"; -"balance.token.frozen" = "Frozen"; +"balance.token.frozen" = "Eingefroren"; "balance.token.frozen.info.title" = "gefrorener Titel"; "balance.token.frozen.info.description" = "gefrorener Beschreibungstext"; +"balance.token.account.inactive.title" = "Konto nicht aktiv"; +"balance.token.account.inactive.description" = "Neue TRON-Wallets erfordern eine Einzahlung von mindestens 1 TRX, um aktiv zu werden. Inaktive Wallets können Token halten und empfangen, korrigieren jedoch keine Guthaben, bis sie aktiviert sind."; // Account switcher @@ -445,6 +447,8 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "send.transaction_inputs_outputs_info.shuffle.description" = "Die Reihenfolge der Transaktionsausgänge wird bei jeder Transaktion zufällig. Manchmal können Änderungen die erste Ausgabe sein, manchmal kann es die zweite sein. Wenn ein Benutzer dem Entwickler der App vertraut, dann erwäge dies eine empfohlene Option."; "send.transaction_inputs_outputs_info.deterministic.title" = "2. Deterministic"; "send.transaction_inputs_outputs_info.deterministic.description" = "Es gibt einen allgemein vereinbarten Standard für die Bestellung von Transaktionsausgängen (bekannt als BIP69). In Open-Source-Brieftaschen stellt dieser Standard sicher, dass Wallet-Benutzer nicht darauf vertrauen müssen, wie Entwickler der App die Reihenfolge der Ausgaben implementieren. Da dieser Standard neu ist, haben noch nicht viele Brieftaschen ihn implementiert. Infolgedessen ist es einigermaßen möglich, auf der Blockchain zu sehen, ob eine Transaktion von einer Brieftasche gesendet wurde, die diesen Standard verwendet hat oder nicht."; +"send.select_all" = "Alles Auswählen"; +"send.unselect_all" = "Alle abwählen"; "send.confirmation.title" = "Bestätigen"; "send.confirmation.you_send" = "Sie Senden"; @@ -462,18 +466,20 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "send.confirmation.time_lock" = "TimeLock"; "send.confirmation.replace_by_fee" = "Ersetzen durch Gebühr"; "send.confirmation.replaced_transactions" = "Ersetzte Transaktionen"; - +"send.confirmation.input" = "Eingabe"; + +"send.confirmation.sync_failed" = "Synchronisation fehlgeschlagen"; +"send.confirmation.invalid_data" = "Ungültige Daten"; +"send.confirmation.refresh" = "Aktualisieren"; +"send.confirmation.please_wait" = "Bitte warten"; +"send.confirmation.expires_in" = "Läuft ab in %@"; +"send.confirmation.expired" = "Abgelaufen"; "send.confirmation.slide_to_send" = "Zum Senden wischen"; "send.confirmation.sending" = "Wird gesendet"; "send.confirmation.sent" = "Gesendet"; "send.confirmation.slide_to_approve" = "Zum Genehmigen wischen"; -"send.confirmation.approving" = "Wird bestätigt"; -"send.confirmation.approved" = "Bestätigt"; - "send.confirmation.slide_to_revoke" = "Zum Widerrufen wischen"; -"send.confirmation.revoking" = "Widerrufen"; -"send.confirmation.revoked" = "Widerrufen"; "send.confirmation.slide_to_resend" = "Nochmal senden"; "send.confirmation.slide_to_cancel" = "Transaktion abbrechen"; @@ -506,6 +512,7 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "send.lock_time" = "TimeLock"; "send.unspent_outputs" = "UTxOs"; +"send.unspent_outputs.description" = "Manuell die UTxO auswählen, um die Mittel im Guthaben auszugeben"; "send.unspent_outputs.send_to" = "Senden an"; "send.unspent_outputs.change" = "Ändern"; @@ -513,6 +520,14 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "approve.confirmation.you_revoke" = "Du widerrufen"; "approve.confirmation.spender" = "Spender"; +"send.enter_amount" = "Betrag eingeben"; +"send.enter_address" = "Adresse eingeben"; +"send.invalid_address" = "Ungültige Adresse"; +"send.token_not_enabled" = "Token nicht aktiviert"; +"send.token_syncing" = "Token-Synchronisation"; +"send.token_not_synced" = "Token nicht synchronisiert"; +"send.insufficient_balance" = "Unzureichendes Guthaben"; + // Donate "donate.list.title" = "Spenden mit"; @@ -549,7 +564,7 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "swap.trade_error.wrap_unwrap_not_allowed" = "Dieser Service erlaubt keine Verpackung/Auspackung. Bitte versuchen Sie einen anderen Swap-Service. 1Inch empfohlen"; "swap.button_error.insufficient_balance" = "Unzureichendes Guthaben"; "swap.switch_provider.title" = "Dienst wechseln"; -"swap.amount_type.coin" = "Coin"; +"swap.amount_type.coin" = "Münze"; "swap.price" = "Preis"; "swap.buy_price" = "Kaufpreis"; @@ -643,7 +658,6 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "swap.confirmation.slide_to_swap" = "Wischen zum Tauschen"; "swap.confirmation.swapping" = "Swapping"; -"swap.confirmation.swapped" = "Swapped"; "swap.confirmation.refresh" = "Aktualisieren"; "swap.confirmation.impact_too_high" = "%@ hat Swap-Aktion für diesen Handel deaktiviert, da du einen extrem ungünstigen Preis bekommst. Dies ist auf extrem niedrige Liquidität zurückzuführen.\nWenn Sie trotzdem wechseln möchten, verwenden Sie stattdessen %@ Webseite."; "swap.confirmation.impact_warning" = "Wichtig! Du bekommst einen extrem ungünstigen Preis. Dies ist auf die extrem niedrige Liquidität zurückzuführen."; @@ -686,6 +700,51 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "market.defi_cap" = "DeFi MK"; "market.defi_tvl" = "TVL in DeFi"; +"market.global.market_cap" = "Total Cap"; +"market.global.volume" = "24 Std. Vol."; +"market.global.btc_dominance" = "BTC-Dominanz"; +"market.global.etf_inflow" = "ETF Inflow"; +"market.global.tvl_in_defi" = "TVL in DeFi"; + +"market.tab.news" = "Nachrichten"; +"market.tab.coins" = "Münzen"; +"market.tab.watchlist" = "Merkliste"; +"market.tab.platforms" = "Plattformen"; +"market.tab.pairs" = "Paare"; +"market.tab.sectors" = "Sektoren"; + +"market.sort_by.title" = "Sortieren nach"; +"market.sort_by.manual" = "Manuell"; +"market.sort_by.highest_cap" = "Höchste Obergrenze"; +"market.sort_by.lowest_cap" = "Niedrigste Obergrenze"; +"market.sort_by.gainers" = "Gewinner"; +"market.sort_by.losers" = "Verlierer"; +"market.sort_by.highest_volume" = "Höchste Lautstärke"; +"market.sort_by.lowest_volume" = "Geringste Lautstärke"; + +"market.top_coins.title" = "Münzen"; +"market.top_coins" = "Top %@"; + +"market.time_period.title" = "Zeitraum"; +"market.time_period.1d" = "1 Tag"; +"market.time_period.1w" = "1 Woche"; +"market.time_period.2w" = "2 Wochen"; +"market.time_period.1m" = "1 Monate"; +"market.time_period.3m" = "3 Monate"; +"market.time_period.6m" = "6 Monate"; +"market.time_period.1y" = "1 Jahr"; +"market.time_period.2y" = "2 Jahre"; +"market.time_period.5y" = "5 Jahre"; +"market.time_period.1d.short" = "1T"; +"market.time_period.1w.short" = "1W"; +"market.time_period.2w.short" = "2W"; +"market.time_period.1m.short" = "1M"; +"market.time_period.3m.short" = "3M"; +"market.time_period.6m.short" = "6M"; +"market.time_period.1y.short" = "1J"; +"market.time_period.2y.short" = "2J"; +"market.time_period.5y.short" = "5J"; + "market.project_has_no_coin" = "Dieses Projekt hat keine eigene Kryptowährung"; "market.top.section.header.see_all" = "Alle anzeigen"; @@ -722,6 +781,7 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "market.top.top_platforms" = "Top Plattformen"; "market.top.protocols" = "Protocols"; +"market.pairs.volume" = "Lautstärke"; "top_pairs.title" = "Top Marktpaare"; "top_pairs.description" = "Top-Handelspaare nach Volumen in jedem Exchange"; @@ -730,6 +790,8 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "top_platform.title" = "%@ Ökosystem"; "top_platform.description" = "Marktkapitalisierung aller Protokolle auf der %@ Kette"; +"top_platform.total_cap" = "Total Cap"; + "market.search.recent" = "Neueste"; "market.search.popular" = "Beliebt"; @@ -741,10 +803,22 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "market_discovery.not_found" = "Keine Ergebnisse gefunden"; "market_watchlist.empty.caption" = "Ihre Beobachtungsliste ist leer."; +"market.watchlist.signals" = "Signale"; +"market.watchlist.empty" = "Ihre Beobachtungsliste ist leer"; +"market.watchlist.signals.description" = "Die folgenden Signale basieren auf den Bollinger-Bändern und dem RSI-technischen Preisindikator über etwa die letzten 30 Tage. Diese Signale sind algorithmisch und können sich häufig ändern."; +"market.watchlist.signals.strong_buy.description" = "Hohe Zuversicht in Preisanstieg"; +"market.watchlist.signals.buy.description" = "Voraussichtlicher Preisanstieg in naher Zukunft"; +"market.watchlist.signals.neutral.description" = "Kein klarer Trend, der Markt befindet sich im Gleichgewicht"; +"market.watchlist.signals.sell.description" = "Voraussichtlicher Preisanstieg in naher Zukunft"; +"market.watchlist.signals.strong_sell.description" = "Hohe Wahrscheinlichkeit für Preissenkungen"; +"market.watchlist.signals.risky.description" = "Erhöhtes Risikoniveau, erfordert Vorsicht"; +"market.watchlist.signals.warning" = "Denken Sie immer daran, das Risikomanagement anzuwenden und beachten Sie, dass dies keine finanzielle Beratung ist."; +"market.watchlist.signals.turn_on" = "Einschalten"; "market.advanced_search.title" = "Filter"; "market.advanced_search.show_results" = "Ergebnisse anzeigen"; "market.advanced_search.empty_results" = "Leere Ergebnisse"; +"market.advanced_search.retry" = "Wiederholen"; "market.advanced_search.dex_description" = "Diese Einstellung gilt für Token, die auf Ethereum (Uniswap DEX) und Binance Smart Chain (Pancake DEX) gehandelt werden."; "market.advanced_search.24h" = "24 Std"; @@ -764,17 +838,10 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "market.advanced_search.liquidity" = "DEX-Liquidität"; "market.advanced_search.blockchains" = "Blockchains"; -"market.advanced_search.technical_advice" = "Handelssignale"; +"market.advanced_search.signal" = "Handelssignal"; "market.advanced_search.price_period" = "Preiszeitraum"; "market.advanced_search.price_change" = "Preisänderung"; -"market.advanced_search.technical_advice.risk_trade" = "Handelsrisiko"; -"market.advanced_search.technical_advice.strong_buy" = "Starker Kauf"; -"market.advanced_search.technical_advice.buy" = "Kaufen"; -"market.advanced_search.technical_advice.neutral" = "Neutral"; -"market.advanced_search.technical_advice.sell" = "Verkaufen"; -"market.advanced_search.technical_advice.strong_sell" = "Starker Verkauf"; - "market.advanced_search.outperformed_btc" = "Übertraf BTC"; "market.advanced_search.outperformed_eth" = "Übertraf ETH"; "market.advanced_search.outperformed_bnb" = "Übertraf BNB"; @@ -809,9 +876,9 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "market.advanced_search.more_500_b" = "> 500 Mrd"; "market.advanced_search.day" = "1 Tag"; -"market.advanced_search.week" = "1Week"; +"market.advanced_search.week" = "1 Woche"; "market.advanced_search.week2" = "2 Wochen"; -"market.advanced_search.month" = "1 Monat"; +"market.advanced_search.month" = "1 Monate"; "market.advanced_search.month6" = "6 Monate"; "market.advanced_search.year" = "1 Jahr"; @@ -830,10 +897,36 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "market.global.defi_cap.title" = "DeFi MK"; "market.global.defi_cap.description" = "Gesamter Marktwert von DeFi-Projekten"; -"market.global.tvl_in_defi.title" = "TVL in DeFi"; -"market.global.tvl_in_defi.description" = "Gesamtwert gesperrt (TVL) in DeFi"; -"market.global.tvl_in_defi.multi_chain" = "Mehrkettig"; -"market.global.tvl_in_defi.filter_by_chain" = "Filtern nach Kette"; +"market.etf.title" = "Gesamtnettonzufluss"; +"market.etf.description" = "Der Nettogeldzufluss eines ETFs entspricht seinen Geldeingängen abzüglich der Abflüsse."; +"market.etf.total_net_assets" = "Gesamte Netto-Assets"; +"market.etf.sort_by.highest_assets" = "Höchste Vermögenswerte"; +"market.etf.sort_by.lowest_assets" = "Niedrigste Vermögenswerte"; +"market.etf.sort_by.inflow" = "Zufluss"; +"market.etf.sort_by.outflow" = "Abfluss"; +"market.etf.period.all" = "Alle"; + +"market.market_cap.title" = "Gesamtmarktzahl"; +"market.market_cap.description" = "Gesamtmarktwert aller Kryptowährungen"; +"market.market_cap.market_cap" = "Marktkappe"; + +"market.tvl_in_defi.title" = "TVL in DeFi"; +"market.tvl_in_defi.description" = "Gesamtwert gesperrt (TVL) in DeFi"; +"market.tvl_in_defi.tvl" = "TVL"; +"market.tvl_in_defi.multi_chain" = "Mehrketten"; +"market.tvl_in_defi.filter_by_chain" = "Filtern nach Kette"; +"market.tvl_in_defi.filter.all" = "Alle"; + +"market.volume.title" = "Lautstärke"; +"market.volume.description" = "Das 24h Handelsvolumen des Krypto-Marktes"; +"market.volume.volume" = "Lautstärke"; + +"market.signal.risky" = "Riskant"; +"market.signal.strong_buy" = "Starker Kauf"; +"market.signal.buy" = "Kaufen"; +"market.signal.neutral" = "Neutral"; +"market.signal.sell" = "Verkaufen"; +"market.signal.strong_sell" = "Starker Verkauf"; // Coin Page @@ -854,10 +947,11 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "coin_overview.genesis_date" = "Startdatum"; "coin_overview.trading_volume" = "Handelsvolumen"; -"coin_overview.roi.hour24" = "1 Tag"; -"coin_overview.roi.day7" = "1Week"; +"coin_overview.roi.hour24" = "24 Stunden"; +"coin_overview.roi.day1" = "1 Tag"; +"coin_overview.roi.day7" = "1 Woche"; "coin_overview.roi.day14" = "2 Wochen"; -"coin_overview.roi.day30" = "1 Monat"; +"coin_overview.roi.day30" = "1 Monate"; "coin_overview.roi.day200" = "6 Monate"; "coin_overview.roi.year1" = "1 Jahr"; @@ -895,35 +989,35 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "technical_advice.neutral.rsi" = "RSI = %@ bestätigt ebenfalls das Fehlen eines starken Trends."; "technical_advice.neutral.indicators" = "Das Asset befand sich in der überkauften/überverkauften Zone, aber momentan hat der Preis wieder den Bollinger-Band-Kanal in der neutralen Zone erreicht. Der RSI beträgt %@, was auch das Fehlen eines starken Trends bestätigt. Insgesamt bewegt sich der Assetpreis also in Richtung Durchschnittsbildung und weitere Bewegungen sind in jede Richtung möglich."; -"technical_advice.neutral.advice" = " In general, the asset price is moving towards averaging and further movement is possible in any direction."; +"technical_advice.neutral.advice" = " Im Allgemeinen bewegt sich der Preis des Vermögenswerts in Richtung Mittelwert, und weitere Bewegungen in jede Richtung sind möglich."; -"technical_advice.other.title" = "Please note:"; +"technical_advice.other.title" = "Bitte beachten Sie:"; -"technical_advice.ema.above" = "above"; -"technical_advice.ema.below" = "below"; -"technical_advice.ema.growth" = "growth"; -"technical_advice.ema.decrease" = "decrease"; -"technical_advice.ema.advice" = "EMA 200. Determines the overall sentiment and trend. The daily price of the asset is located %@ the EMA (%@). This means that globally the asset is set for %@."; +"technical_advice.ema.above" = "oben"; +"technical_advice.ema.below" = "unten"; +"technical_advice.ema.growth" = "wachstum"; +"technical_advice.ema.decrease" = "verringern"; +"technical_advice.ema.advice" = "EMA 200. Bestimmt die allgemeine Stimmung und den Trend. Der tägliche Preis des Vermögenswerts liegt %@ der EMA (%@). Dies bedeutet, dass der Vermögenswert global für %@ eingestellt ist."; -"technical_advice.macd.positive" = "above"; -"technical_advice.macd.negative" = "below"; -"technical_advice.macd.advice" = "MACD. Assesses the strength of the trend considering the average price change. The daily value of the histogram is %@ (%@). The price of the asset globally may move %@."; +"technical_advice.macd.positive" = "oben"; +"technical_advice.macd.negative" = "unten"; +"technical_advice.macd.advice" = "MACD. Bewertet die Stärke des Trends unter Berücksichtigung der durchschnittlichen Preisänderung. Der tägliche Wert des Histogramms beträgt %@ (%@). Der Preis des Vermögenswerts könnte sich global %@ bewegen."; "coin_analytics.indicators.title" = "Technische Indikatoren"; -"coin_analytics.indicators.disclaimer" = "Always remember to apply risk management, and note that this is not financial advice."; +"coin_analytics.indicators.disclaimer" = "Denken Sie immer daran, das Risikomanagement anzuwenden und beachten Sie, dass dies keine finanzielle Beratung ist."; "coin_analytics.indicators.info.title" = "Technische Indikatoren"; -"coin_analytics.indicators.info.description" = "We use the Bollinger Bands + RSI strategy to determine trading signals. All calculations are based on daily candlesticks and provide advice for a moderately long term. The essence of the strategy is that the asset price should reach an extreme, breaking out of the Bollinger Bands channel, and the RSI should be in the overbought/oversold zone. After the price returns to the channel, there is a high probability of the price returning to the mean values or attempting to break the channel from the other side. Note that the strategy may give several false signals during strong market movements before a correct signal appears.\n\nPlease remember that it is very important to apply risk management to trading and remember to cut losses if the market situation changes! "; +"coin_analytics.indicators.info.description" = "Wir verwenden die Bollinger-Bänder + RSI-Strategie, um Handelssignale zu bestimmen. Alle Berechnungen basieren auf täglichen Kerzenleuchtern und geben Ratschläge für eine moderat langfristige Strategie. Die Essenz der Strategie besteht darin, dass der Asset-Preis ein Extrem erreichen sollte, indem er aus dem Bollinger-Bänder-Kanal ausbricht, und der RSI sollte sich in der überkauften/überverkauften Zone befinden. Nachdem der Preis zum Kanal zurückkehrt, besteht eine hohe Wahrscheinlichkeit, dass der Preis zu den Mittelwerten zurückkehrt oder versucht, den Kanal von der anderen Seite zu durchbrechen. Beachten Sie, dass die Strategie während starker Marktbewegungen mehrere falsche Signale geben kann, bevor ein korrektes Signal erscheint.\n\nBitte denken Sie daran, dass es sehr wichtig ist, Risikomanagement beim Handel anzuwenden und daran zu denken, Verluste zu begrenzen, wenn sich die Marktsituation ändert! "; "coin_analytics.indicators.hide_details" = "Details ausblenden"; "coin_analytics.indicators.show_details" = "Details anzeigen"; "coin_analytics.indicators.summary" = "Zusammenfassung"; "coin_analytics.indicators.no_data" = "Keine Daten"; -"coin_analytics.indicators.oversold" = "Sehr risikoreich zu handeln"; -"coin_analytics.indicators.strong_buy" = "Stark Kaufen"; +"coin_analytics.indicators.oversold" = "Riskant"; +"coin_analytics.indicators.strong_buy" = "Starker Kauf"; "coin_analytics.indicators.buy" = "Kaufen"; "coin_analytics.indicators.neutral" = "Neutral"; "coin_analytics.indicators.sell" = "Verkaufen"; -"coin_analytics.indicators.strong_sell" = "Stark Verkaufen"; -"coin_analytics.indicators.overbought" = "Sehr risikoreich zu handeln"; +"coin_analytics.indicators.strong_sell" = "Starker Verkauf"; +"coin_analytics.indicators.overbought" = "Riskant"; "coin_analytics.period" = "Zeitraum"; "coin_analytics.period.select_title" = "Zeitraum auswählen"; "coin_analytics.period.1h" = "1 Stunde"; @@ -944,6 +1038,7 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "coin_analytics.cex_volume" = "CEX-Volume"; "coin_analytics.cex_volume_rank" = "CEX Volumenrang"; "coin_analytics.cex_volume_rank.description" = "Token, sortiert nach Handelsvolumen für den Token auf zentralisierten Börsen."; +"coin_analytics.cex_volume_rank.sorting_field" = "Lautstärke"; "coin_analytics.cex_volume.info1" = "Total trading volume for the token on leading centralized exchanges over 30-day period."; "coin_analytics.cex_volume.info2" = "Diagramm, das die Schwankungen im täglichen Handelsvolumen des Tokens an führenden zentralisierten Börsen über einen Zeitraum von 1 Jahr zeigt."; "coin_analytics.cex_volume.info3" = "Die Rangfolge des Tokens basiert auf dem Handelsvolumen an führenden zentralisierten Börsen über einen Zeitraum von 30 Tagen."; @@ -952,6 +1047,7 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "coin_analytics.dex_volume" = "DEX-Volume"; "coin_analytics.dex_volume_rank" = "DEX-Volumenrang"; "coin_analytics.dex_volume_rank.description" = "Token, sortiert nach Handelsvolumen für den Token auf dezentralen Börsen."; +"coin_analytics.dex_volume_rank.sorting_field" = "Lautstärke"; "coin_analytics.dex_volume.info1" = "Total trading volume for the token on leading centralized exchanges over 30-day period."; "coin_analytics.dex_volume.info2" = "Diagramm, das die Schwankungen im täglichen Handelsvolumen des Tokens an führenden dezentralen Börsen über einen Zeitraum von 1 Jahr zeigt."; "coin_analytics.dex_volume.info3" = "Rang des Tokens basierend auf dem Handelsvolumen an führenden dezentralen Börsen über einen Zeitraum von 30 Tagen."; @@ -963,6 +1059,7 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "coin_analytics.dex_liquidity" = "DEX-Liquidität"; "coin_analytics.dex_liquidity_rank" = "DEX Liquiditätsrang"; "coin_analytics.dex_liquidity_rank.description" = "Token, sortiert nach verfügbarer Liquidität auf dezentralen Börsen."; +"coin_analytics.dex_liquidity_rank.sorting_field" = "Liquidität"; "coin_analytics.dex_liquidity.info1" = "Gesamte derzeit verfügbare Liquidität für den Token an führenden dezentralen Börsen."; "coin_analytics.dex_liquidity.info2" = "Diagramm, das die Schwankungen in der verfügbaren Liquidität für den Token an führenden dezentralen Börsen über einen Zeitraum von 1 Jahr zeigt."; "coin_analytics.dex_liquidity.info3" = "Liste aller Tokens basierend auf der verfügbaren Liquidität für das Token an führenden dezentralen Börsen."; @@ -974,6 +1071,7 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "coin_analytics.active_addresses.30_day_unique_addresses" = "30-Tage Einzigartige Adressen"; "coin_analytics.active_addresses_rank" = "Aktive Adressen Rang"; "coin_analytics.active_addresses_rank.description" = "Token, die nach der Anzahl der eindeutigen Adressen eingestuft werden, die mit dem Token Transaktionen tätigen."; +"coin_analytics.active_addresses_rank.sorting_field" = "Aktiv"; "coin_analytics.active_addresses.info1" = "Total number of unique daily active addresses over 24-hour period."; "coin_analytics.active_addresses.info2" = "Diagramm, das die Schwankungen in der Anzahl der täglich aktiven Adressen über einen Zeitraum von einem Jahr zeigt."; "coin_analytics.active_addresses.info3" = "Gesamtzahl der eindeutigen Blockchain-Adressen, die über einen Zeitraum von 30 Tagen mit einem Token handeln."; @@ -983,6 +1081,7 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "coin_analytics.transaction_count" = "Transaktionsanzahl"; "coin_analytics.transaction_count_rank" = "Rang nach Transaktionsanzahl"; "coin_analytics.transaction_count_rank.description" = "Token werden nach der Anzahl von Transaktionen auf einer Blockchain eingestuft."; +"coin_analytics.transaction_count_rank.sorting_field" = "Anzahl"; "coin_analytics.transaction_count.info1" = "Gesamtzahl der einzigartigen Blockchain-Transaktionen mit Token über einen Zeitraum von 30 Tagen."; "coin_analytics.transaction_count.info2" = "Diagramm, das die Schwankungen in der Anzahl der Transaktionen über einen Zeitraum von einem Jahr zeigt."; "coin_analytics.transaction_count.info3" = "Der Rang des Tokens basiert auf der Anzahl der Transaktionen innerhalb des 30-Tage-Zeitraums des Tokens."; @@ -992,6 +1091,7 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "coin_analytics.holders" = "Halter"; "coin_analytics.holders_rank" = "Halter Rang"; "coin_analytics.holders_rank.description" = "Ranking von Token nach einzigartigen Adressen, die diese auf mehreren Blockchains halten."; +"coin_analytics.holders_rank.sorting_field" = "Halter"; "coin_analytics.holders.info1" = "Gesamtzahl der einzigartigen Adressen, die den Token auf verschiedenen Blockchains halten."; "coin_analytics.holders.info2" = "Die Top 10 Wallets, die den Token auf jeder Blockchain halten."; "coin_analytics.holders.tracked_blockchains" = "Verfolgte Blockchains: Ethereum, Binance Smart Chain, Optimism, Arbitrum, Celo, Cronos, Avalanche, Fantom, Polygon"; @@ -1011,10 +1111,12 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "coin_analytics.project_fee" = "Projektgebühr"; "coin_analytics.project_fee_rank" = "Projektgebührenrang"; "coin_analytics.project_fee_rank.description" = "Token werden nach den von den jeweiligen Projekten generierten Gebühren eingestuft. Die Art und Weise, wie Gebühren gesammelt werden, variiert von Projekt zu Projekt."; +"coin_analytics.project_fee_rank.sorting_field" = "Lautstärke"; "coin_analytics.project_revenue" = "Projekteinnahmen"; "coin_analytics.project_revenue_rank" = "Projekt-Umsatzrang"; "coin_analytics.project_revenue_rank.description" = "Token werden nach Einnahmen klassifiziert, die über Mechanismen wie Stapel- oder Tokenbrennen für Inhaber generiert werden."; +"coin_analytics.project_revenue_rank.sorting_field" = "Umsatz"; "coin_analytics.other_data" = "Andere Daten"; @@ -1200,6 +1302,8 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "settings.rate_us" = "Bewerten Sie uns"; "settings.tell_friends" = "Freunden erzählen"; "settings.contact_us" = "Kontaktiere uns"; +"settings.social_networks.label" = "Be Unstoppable"; +"settings.social_networks.footer" = "Lernen Sie Kryptographie über exklusive Videos. Lernen Sie uns informell kennen. Sei der Erste, der Dinge sieht, an denen wir arbeiten."; // Settings -> Base Currency @@ -1417,9 +1521,7 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; // Settings -> About App "settings.about_app.title" = "Über die App"; -"settings.about_app.app_name" = "%@ Wallet"; -"settings.about_app.description" = "Die Brieftasche %@ wurde für diejenigen gebaut, die Kryptowährungen auf private und unabhängige Weise investieren und speichern möchten.\n\nEs handelt sich um eine nicht-Custodial, Peer-to-Peer-Wallet, bei der nur der Benutzer die Kontrolle über das Guthaben hat. Es sammelt keine Daten und hält den Benutzer unabhängig, indem er das Guthaben des Benutzers nicht an eine bestimmte Wallet-App sperrt.\n\nDie %@ Brieftasche ist vollständig Open-Source und jeder kann bestätigen, dass die App genau so funktioniert, wie sie es vorgibt."; -"settings.about_app.whats_new" = "Das ist neu"; +"settings.about_app.app_version" = "App-Version"; "settings.about_app.website" = "Website"; // Settings -> About App -> Contact @@ -1431,11 +1533,11 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; // Settings -> Privacy "settings.privacy" = "Privatsphäre"; -"settings.privacy.description" = "%@ sammelt keine Daten oder nutzt Analysewerkzeuge, die möglicherweise Daten über ihre Benutzer enthüllen. Die Brieftasche ist so konzipiert, dass sie den Benutzern ein hohes Maß an Privatsphäre garantiert."; -"settings.privacy.statement.user_data_storage" = "Benutzerdaten bleiben immer auf dem Gerät des Benutzers."; +"settings.privacy.description" = "%@ sammelt keine persönlichen Daten, die Ihre privaten Informationen offen legen, z.B. Münzsalden oder Adressen. Während wir einige UI-Nutzungsstatistiken sammeln, dient es allein dazu, unsere Benutzer- und App-Nutzungstrends zu verstehen. Dies kann deaktiviert werden, wenn Sie es wünschen."; "settings.privacy.statement.data_usage" = "Das Wallet sammelt keine Daten über Benutzer."; -"settings.privacy.statement.data_privacy" = "Das Wallet teilt keine Daten über Benutzer."; -"settings.privacy.statement.user_account" = "Es gibt keine Benutzerkonten oder Datenbanken, die Benutzerdaten woanders speichern."; +"settings.privacy.statement.data_storage" = "Es gibt keine Benutzerkonten oder Datenbanken, die Benutzerdaten speichern."; +"settings.privacy.statement.user_account" = "Wenn erlaubt, teilt die Brieftasche App-Nutzungsgewohnheiten mit dem unaufhaltsamen Team. Dies ist zu verstehen, welche Funktionen von unseren Benutzern verwendet werden (oder nicht). Als datenschutzorientierte App brauchen wir eine Möglichkeit, unsere Bemühungen zu bewerten, und ohne dies haben wir keine Ahnung, ob die von uns erstellten Funktionen genutzt werden oder nicht."; +"settings.privacy.allow" = "UI-Daten teilen"; // Settings -> Appearance @@ -1446,21 +1548,25 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "appearance.theme.dark" = "Dunkel"; "appearance.theme.light" = "Hell"; -"appearance.tab_settings" = "Tab-Einstellungen"; "appearance.markets_tab" = "Markt-Tab"; +"appearance.hide_markets" = "Märkte ausblenden"; +"appearance.price_change" = "Preisänderung"; +"appearance.price_change.24h" = "24Std"; +"appearance.price_change.1d" = "Mitternacht UTC"; + "appearance.launch_screen" = "Startbildschirm"; "appearance.launch_screen.auto" = "Auto"; "appearance.launch_screen.balance" = "Guthaben"; "appearance.launch_screen.market_overview" = "Marktübersicht"; "appearance.launch_screen.watchlist" = "Merkliste"; -"appearance.app_icon" = "App-Symbol"; - -"appearance.balance_conversion" = "Saldokonvertierung"; - +"appearance.balance_tab" = "Balance Tab"; +"appearance.hide_buttons" = "Schaltflächen ausblenden"; "appearance.balance_value" = "Saldo Wert"; -"appearance.balance_value.coin_value" = "Münzwert"; -"appearance.balance_value.fiat_value" = "Fiat-Wert"; +"appearance.balance_value.coin_fiat" = "Münze / Fiat"; +"appearance.balance_value.fiat_coin" = "Fiat / Münze"; + +"appearance.app_icon" = "App-Symbol"; // Settings -> Contacts @@ -1913,7 +2019,7 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "nft.activity.empty_list" = "Noch keine Artikel-Aktivität"; "nft.activity.event_types" = "Ereignistypen"; "nft.activity.event_type.all" = "Alle Ereignisse"; -"nft.activity.event_type.sale" = "Sale"; +"nft.activity.event_type.sale" = "Kauf"; "nft.activity.event_type.transfer" = "Überweisung"; "nft.activity.event_type.mint" = "Mint"; "nft.activity.event_type.list" = "Liste"; @@ -1959,7 +2065,6 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "tron.send.fee.info" = "Die geschätzten Kosten für den Versand einer Transaktion im Netzwerk (ohne Energie-, Bandbreiten- und Aktivierungsgebühren)"; "tron.send.resources_consumed.info" = "Bandwidth ist die Einheit, die die Größe der in der Blockchain-Datenbank gespeicherten Transaktionsbytes misst. Je größer die Transaktion, desto mehr Ressourcen werden verbraucht.\n\nEnergie ist die Einheit, die die Anzahl der Berechnungen misst, die von der virtuellen TRON-Maschine benötigt werden, um bestimmte Operationen im TRON-Netzwerk durchzuführen.\n\nDa intelligente Vertragsabschlüsse die Verwendung von Rechnerressourcen erfordern, muss für jeden Smart Contract eine Energiegebühr bezahlt werden."; "tron.send.activation_fee.info" = "Die Übertragung von TRX- oder TRC-10-Token an eine inaktive Konto-Adresse wird das Konto aktivieren."; -"tron.send.inactive_address" = "Diese Adresse ist nicht aktiv"; // Cex Coin Select @@ -2025,7 +2130,7 @@ Gehe zu Einstellungen - > %@ und erlaube Zugriff auf die Kamera."; "transaction_filter.blockchain" = "Blockchain"; "transaction_filter.all_blockchains" = "Alle Blockchains"; -"transaction_filter.coin" = "Coin"; +"transaction_filter.coin" = "Münze"; "transaction_filter.all_coins" = "Alle Coins"; "transaction_filter.contact" = "Kontakt"; "transaction_filter.all_contacts" = "Alle Kontakte"; diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index ab2e0967d4..3be3092064 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -226,6 +226,7 @@ "extended_key.purpose" = "Purpose"; "extended_key.blockchain" = "Blockchain"; "extended_key.account" = "Account"; +"extended_key.account.description" = "This is a setting for advanced users. If you are trying to import wallet (via extended private key )or transactions list (via  extended public key) you need account 0."; "extended_key.tap_to_show" = "Tap to show extended private key"; // Backup @@ -316,7 +317,6 @@ "balance.downloading_blocks" = "Downloading Blocks"; "balance.scanning_blocks" = "Scanning Blocks"; "balance.enhancing_transactions" = "Enhancing Transactions"; -"wait_for_synchronization" = "Wait for synchronization"; "balance.searching.count" = "%@ tx"; "balance.syncing_percent" = "Syncing... %@"; @@ -349,6 +349,8 @@ "balance.token.frozen" = "Frozen"; "balance.token.frozen.info.title" = "Frozen title"; "balance.token.frozen.info.description" = "Frozen Description Text"; +"balance.token.account.inactive.title" = "Account Not Active"; +"balance.token.account.inactive.description" = "New TRON wallets require a deposit of at least 1 TRX to become active. Inactive wallets can hold and receive tokens but won’t correct balances until activated."; // Account switcher @@ -443,6 +445,8 @@ "send.transaction_inputs_outputs_info.shuffle.description" = "The order of transaction outputs is randomized on every transaction. Sometimes change can be the first output, sometimes it can be the second. If a user trusts the developer of the app, then consider this a recommended option."; "send.transaction_inputs_outputs_info.deterministic.title" = "2. Deterministic"; "send.transaction_inputs_outputs_info.deterministic.description" = "There is a commonly agreed standard for ordering transaction outputs (known as BIP69). In open-source wallets, that standard ensures wallet users do not need to trust how developers of the app implement the ordering of the outputs. As this standard is new, not many wallets have implemented it yet. As a result, it's somewhat possible to see on the blockchain whether a transaction was sent from a wallet that uses that standard or not."; +"send.select_all" = "Select All"; +"send.unselect_all" = "Unselect All"; "send.confirmation.title" = "Confirm"; "send.confirmation.you_send" = "You Send"; @@ -460,18 +464,20 @@ "send.confirmation.time_lock" = "Time Lock"; "send.confirmation.replace_by_fee" = "Replace by Fee"; "send.confirmation.replaced_transactions" = "Replaced Transactions"; - +"send.confirmation.input" = "Input"; + +"send.confirmation.sync_failed" = "Sync Failed"; +"send.confirmation.invalid_data" = "Invalid Data"; +"send.confirmation.refresh" = "Refresh"; +"send.confirmation.please_wait" = "Please Wait"; +"send.confirmation.expires_in" = "Expires in %@"; +"send.confirmation.expired" = "Expired"; "send.confirmation.slide_to_send" = "Slide to Send"; "send.confirmation.sending" = "Sending"; "send.confirmation.sent" = "Sent"; "send.confirmation.slide_to_approve" = "Slide to Approve"; -"send.confirmation.approving" = "Approving"; -"send.confirmation.approved" = "Approved"; - "send.confirmation.slide_to_revoke" = "Slide to Revoke"; -"send.confirmation.revoking" = "Revoking"; -"send.confirmation.revoked" = "Revoked"; "send.confirmation.slide_to_resend" = "Resend"; "send.confirmation.slide_to_cancel" = "Cancel Transaction"; @@ -504,6 +510,7 @@ "send.lock_time" = "TimeLock"; "send.unspent_outputs" = "UTxOs"; +"send.unspent_outputs.description" = "Manually select UTxO to spend the funds in the balance"; "send.unspent_outputs.send_to" = "Send To"; "send.unspent_outputs.change" = "Change"; @@ -511,6 +518,14 @@ "approve.confirmation.you_revoke" = "You Revoke"; "approve.confirmation.spender" = "Spender"; +"send.enter_amount" = "Enter Amount"; +"send.enter_address" = "Enter Address"; +"send.invalid_address" = "Invalid Address"; +"send.token_not_enabled" = "Token Not Enabled"; +"send.token_syncing" = "Token Syncing"; +"send.token_not_synced" = "Token Not Synced"; +"send.insufficient_balance" = "Insufficient Balance"; + // Donate "donate.list.title" = "Donate with"; @@ -641,7 +656,6 @@ "swap.confirmation.slide_to_swap" = "Slide to Swap"; "swap.confirmation.swapping" = "Swapping"; -"swap.confirmation.swapped" = "Swapped"; "swap.confirmation.refresh" = "Refresh"; "swap.confirmation.impact_too_high" = "%@ has disabled swap action for this trade because you're getting an extremely unfavorable price. This is due to extremely low liquidity.\n\nIf you still want to swap please use %@ website instead."; "swap.confirmation.impact_warning" = "Important! You're getting an extremely unfavorable price. This is due to extremely low liquidity."; @@ -684,6 +698,51 @@ "market.defi_cap" = "DeFi Cap"; "market.defi_tvl" = "TVL in DeFi"; +"market.global.market_cap" = "Total Cap"; +"market.global.volume" = "24h Vol."; +"market.global.btc_dominance" = "BTC Dominance"; +"market.global.etf_inflow" = "ETF Inflow"; +"market.global.tvl_in_defi" = "TVL in DeFi"; + +"market.tab.news" = "News"; +"market.tab.coins" = "Coins"; +"market.tab.watchlist" = "Watchlist"; +"market.tab.platforms" = "Platforms"; +"market.tab.pairs" = "Pairs"; +"market.tab.sectors" = "Sectors"; + +"market.sort_by.title" = "Sort By"; +"market.sort_by.manual" = "Manual"; +"market.sort_by.highest_cap" = "Highest Cap"; +"market.sort_by.lowest_cap" = "Lowest Cap"; +"market.sort_by.gainers" = "Gainers"; +"market.sort_by.losers" = "Losers"; +"market.sort_by.highest_volume" = "Highest Volume"; +"market.sort_by.lowest_volume" = "Lowest Volume"; + +"market.top_coins.title" = "Coins"; +"market.top_coins" = "Top %@"; + +"market.time_period.title" = "Period"; +"market.time_period.1d" = "1 Day"; +"market.time_period.1w" = "1 Week"; +"market.time_period.2w" = "2 Weeks"; +"market.time_period.1m" = "1 Month"; +"market.time_period.3m" = "3 Months"; +"market.time_period.6m" = "6 Months"; +"market.time_period.1y" = "1 Year"; +"market.time_period.2y" = "2 Years"; +"market.time_period.5y" = "5 Years"; +"market.time_period.1d.short" = "1D"; +"market.time_period.1w.short" = "1W"; +"market.time_period.2w.short" = "2W"; +"market.time_period.1m.short" = "1M"; +"market.time_period.3m.short" = "3M"; +"market.time_period.6m.short" = "6M"; +"market.time_period.1y.short" = "1Y"; +"market.time_period.2y.short" = "2Y"; +"market.time_period.5y.short" = "5Y"; + "market.project_has_no_coin" = "This project doesn’t have a coin"; "market.top.section.header.see_all" = "See All"; @@ -720,6 +779,7 @@ "market.top.top_platforms" = "Top Platforms"; "market.top.protocols" = "Protocols: %@"; +"market.pairs.volume" = "Volume"; "top_pairs.title" = "Top Market Pairs"; "top_pairs.description" = "Top trading pairs by volume in every exchanges"; @@ -728,6 +788,8 @@ "top_platform.title" = "%@ Ecosystem"; "top_platform.description" = "Market cap of all protocols on the %@ chain"; +"top_platform.total_cap" = "Total Cap"; + "market.search.recent" = "Recent"; "market.search.popular" = "Popular"; @@ -739,10 +801,22 @@ "market_discovery.not_found" = "No results found"; "market_watchlist.empty.caption" = "Your watchlist is empty."; +"market.watchlist.signals" = "Signals"; +"market.watchlist.empty" = "Your watchlist is empty"; +"market.watchlist.signals.description" = "Below signals are based on the Bollinger Bands and RSI technical price indicators over approx. the last 30 days. These signals are algorithmic and can change frequently."; +"market.watchlist.signals.strong_buy.description" = "High confidence in price increase"; +"market.watchlist.signals.buy.description" = "Likely price increase in near future"; +"market.watchlist.signals.neutral.description" = "No clear trend, market is in equilibrium"; +"market.watchlist.signals.sell.description" = "Likely price decrease in near future"; +"market.watchlist.signals.strong_sell.description" = "High probability of price decrease"; +"market.watchlist.signals.risky.description" = "Elevated risk level, requires caution"; +"market.watchlist.signals.warning" = "Always remember to apply risk management, and note that this is not financial advice."; +"market.watchlist.signals.turn_on" = "Turn On"; "market.advanced_search.title" = "Filters"; "market.advanced_search.show_results" = "Show Results"; "market.advanced_search.empty_results" = "Empty Results"; +"market.advanced_search.retry" = "Retry"; "market.advanced_search.dex_description" = "This setting applies to tokens traded on Ethereum (Uniswap DEX) and Binance Smart Chain (Pancake DEX)."; "market.advanced_search.24h" = "24h"; @@ -762,17 +836,10 @@ "market.advanced_search.liquidity" = "DEX Liquidity"; "market.advanced_search.blockchains" = "Blockchains"; -"market.advanced_search.technical_advice" = "Trading Signals"; +"market.advanced_search.signal" = "Trading Signal"; "market.advanced_search.price_period" = "Price Period"; "market.advanced_search.price_change" = "Price Change"; -"market.advanced_search.technical_advice.risk_trade" = "Risk To Trade"; -"market.advanced_search.technical_advice.strong_buy" = "Strong Buy"; -"market.advanced_search.technical_advice.buy" = "Buy"; -"market.advanced_search.technical_advice.neutral" = "Neutral"; -"market.advanced_search.technical_advice.sell" = "Sell"; -"market.advanced_search.technical_advice.strong_sell" = "Strong Sell"; - "market.advanced_search.outperformed_btc" = "Outperformed BTC"; "market.advanced_search.outperformed_eth" = "Outperformed ETH"; "market.advanced_search.outperformed_bnb" = "Outperformed BNB"; @@ -828,10 +895,36 @@ "market.global.defi_cap.title" = "DeFi Cap"; "market.global.defi_cap.description" = "Total market value of DeFi projects"; -"market.global.tvl_in_defi.title" = "TVL in DeFi"; -"market.global.tvl_in_defi.description" = "Total Value Locked (TVL) in DeFi"; -"market.global.tvl_in_defi.multi_chain" = "Multi-Chain"; -"market.global.tvl_in_defi.filter_by_chain" = "Filter by chain"; +"market.etf.title" = "Total Net Inflow"; +"market.etf.description" = "The net inflow of an ETF equals its cash inflows minus outflows."; +"market.etf.total_net_assets" = "Total Net Assets"; +"market.etf.sort_by.highest_assets" = "Highest Assets"; +"market.etf.sort_by.lowest_assets" = "Lowest Assets"; +"market.etf.sort_by.inflow" = "Inflow"; +"market.etf.sort_by.outflow" = "Outflow"; +"market.etf.period.all" = "All"; + +"market.market_cap.title" = "Total Market Cap"; +"market.market_cap.description" = "Total market value of all cryptocurrencies"; +"market.market_cap.market_cap" = "Market Cap"; + +"market.tvl_in_defi.title" = "TVL in DeFi"; +"market.tvl_in_defi.description" = "Total Value Locked (TVL) in DeFi"; +"market.tvl_in_defi.tvl" = "TVL"; +"market.tvl_in_defi.multi_chain" = "Multi-chain"; +"market.tvl_in_defi.filter_by_chain" = "Filter by chain"; +"market.tvl_in_defi.filter.all" = "All"; + +"market.volume.title" = "Volume"; +"market.volume.description" = "The 24h trading volume of crypto market"; +"market.volume.volume" = "Volume"; + +"market.signal.risky" = "Risky"; +"market.signal.strong_buy" = "Strong Buy"; +"market.signal.buy" = "Buy"; +"market.signal.neutral" = "Neutral"; +"market.signal.sell" = "Sell"; +"market.signal.strong_sell" = "Strong Sell"; // Coin Page @@ -852,7 +945,8 @@ "coin_overview.genesis_date" = "Inception Date"; "coin_overview.trading_volume" = "Trading Volume"; -"coin_overview.roi.hour24" = "1 Day"; +"coin_overview.roi.hour24" = "24 Hours"; +"coin_overview.roi.day1" = "1 Day"; "coin_overview.roi.day7" = "1 Week"; "coin_overview.roi.day14" = "2 Weeks"; "coin_overview.roi.day30" = "1 Month"; @@ -915,13 +1009,13 @@ "coin_analytics.indicators.show_details" = "Show Details"; "coin_analytics.indicators.summary" = "Summary"; "coin_analytics.indicators.no_data" = "No Data"; -"coin_analytics.indicators.oversold" = "Very Risky to Trade"; -"coin_analytics.indicators.strong_buy" = "Buy Signal"; -"coin_analytics.indicators.buy" = "Good Time to Buy"; +"coin_analytics.indicators.oversold" = "Risky"; +"coin_analytics.indicators.strong_buy" = "Strong Buy"; +"coin_analytics.indicators.buy" = "Buy"; "coin_analytics.indicators.neutral" = "Neutral"; -"coin_analytics.indicators.sell" = "Good Time to Sell"; -"coin_analytics.indicators.strong_sell" = "Sell Signal"; -"coin_analytics.indicators.overbought" = "Very Risky to Trade"; +"coin_analytics.indicators.sell" = "Sell"; +"coin_analytics.indicators.strong_sell" = "Strong Sell"; +"coin_analytics.indicators.overbought" = "Risky"; "coin_analytics.period" = "Period"; "coin_analytics.period.select_title" = "Select Period"; "coin_analytics.period.1h" = "1 hour"; @@ -942,6 +1036,7 @@ "coin_analytics.cex_volume" = "CEX Volume"; "coin_analytics.cex_volume_rank" = "CEX Volume Rank"; "coin_analytics.cex_volume_rank.description" = "Tokens ranked by trading volume for the token on centralized exchanges."; +"coin_analytics.cex_volume_rank.sorting_field" = "Volume"; "coin_analytics.cex_volume.info1" = "Total trading volume for the token on leading centralized exchanges over a 30-day period."; "coin_analytics.cex_volume.info2" = "Chart showing variation in daily trading volume for the token on leading centralized exchanges over 1 year period."; "coin_analytics.cex_volume.info3" = "Token's rank is based on trading volume on leading centralized exchanges over a 30-day period."; @@ -950,6 +1045,7 @@ "coin_analytics.dex_volume" = "DEX Volume"; "coin_analytics.dex_volume_rank" = "DEX Volume Rank"; "coin_analytics.dex_volume_rank.description" = "Tokens ranked by trading volume for the token on decentralized exchanges."; +"coin_analytics.dex_volume_rank.sorting_field" = "Volume"; "coin_analytics.dex_volume.info1" = "Total trading volume for the token on leading decentralized exchanges over a 30-day period."; "coin_analytics.dex_volume.info2" = "Chart showing variation in daily trading volume for the token on leading decentralized exchanges over 1 year period."; "coin_analytics.dex_volume.info3" = "Token's rank based on trading volume on leading decentralized exchanges over 30-day period."; @@ -961,6 +1057,7 @@ "coin_analytics.dex_liquidity" = "DEX Liquidity"; "coin_analytics.dex_liquidity_rank" = "DEX Liquidity Rank"; "coin_analytics.dex_liquidity_rank.description" = "Tokens ranked by available liquidity on decentralized exchanges."; +"coin_analytics.dex_liquidity_rank.sorting_field" = "Liquidity"; "coin_analytics.dex_liquidity.info1" = "Total currently available liquidity for the token on leading decentralized exchanges."; "coin_analytics.dex_liquidity.info2" = "Chart showing variation in available liquidity for the token on leading decentralized exchanges over 1 year period."; "coin_analytics.dex_liquidity.info3" = "List of all tokens ranked based on available liquidity for the token on leading decentralized exchanges."; @@ -972,6 +1069,7 @@ "coin_analytics.active_addresses.30_day_unique_addresses" = "30-Day Unique Addresses"; "coin_analytics.active_addresses_rank" = "Active Addresses Rank"; "coin_analytics.active_addresses_rank.description" = "Tokens ranked by number of unique addresses transacting with the token."; +"coin_analytics.active_addresses_rank.sorting_field" = "Active"; "coin_analytics.active_addresses.info1" = "Total number of unique daily active addresses over a 24-hour period."; "coin_analytics.active_addresses.info2" = "Chart showing variation in daily active address count over 1 year period."; "coin_analytics.active_addresses.info3" = "Total number of unique blockchain addresses transacting with token over a 30-day period."; @@ -981,6 +1079,7 @@ "coin_analytics.transaction_count" = "Transaction Count"; "coin_analytics.transaction_count_rank" = "Tx Count Rank"; "coin_analytics.transaction_count_rank.description" = "Tokens are ranked by a number of transactions on a blockchain."; +"coin_analytics.transaction_count_rank.sorting_field" = "Count"; "coin_analytics.transaction_count.info1" = "Total number of unique blockchain transactions with tokens over a 30-day period."; "coin_analytics.transaction_count.info2" = "Chart showing variation in transaction count over 1 year period."; "coin_analytics.transaction_count.info3" = "Token's rank is based on the number of transactions within the token 30-day period."; @@ -990,6 +1089,7 @@ "coin_analytics.holders" = "Holders"; "coin_analytics.holders_rank" = "Holders Rank"; "coin_analytics.holders_rank.description" = "Ranking tokens by unique addresses holding them on multiple blockchains."; +"coin_analytics.holders_rank.sorting_field" = "Holders"; "coin_analytics.holders.info1" = "Total number of unique addresses holding the token on various blockchains."; "coin_analytics.holders.info2" = "Top 10 wallets holding the token on each blockchain."; "coin_analytics.holders.tracked_blockchains" = "Tracked blockchains: Ethereum, Binance Smart Chain, Optimism, Arbitrum, Celo, Cronos, Avalanche, Fantom, Polygon"; @@ -1009,10 +1109,12 @@ "coin_analytics.project_fee" = "Project Fee"; "coin_analytics.project_fee_rank" = "Project Fee Rank"; "coin_analytics.project_fee_rank.description" = "Tokens are ranked according to fees generated by respective projects. The way fees are collected varies from project to project."; +"coin_analytics.project_fee_rank.sorting_field" = "Volume"; "coin_analytics.project_revenue" = "Project Revenue"; "coin_analytics.project_revenue_rank" = "Project Revenue Rank"; "coin_analytics.project_revenue_rank.description" = "Tokens are ranked by revenue generated for holders via mechanisms i.e. staking or token burns."; +"coin_analytics.project_revenue_rank.sorting_field" = "Revenue"; "coin_analytics.other_data" = "Other Data"; @@ -1198,6 +1300,8 @@ "settings.rate_us" = "Rate Us"; "settings.tell_friends" = "Tell Friends"; "settings.contact_us" = "Contact Us"; +"settings.social_networks.label" = "Be Unstoppable"; +"settings.social_networks.footer" = "Learn and master crypto via exclusive videos. Get to know us informally. Be the first to see things we work on."; // Settings -> Base Currency @@ -1415,9 +1519,7 @@ // Settings -> About App "settings.about_app.title" = "About App"; -"settings.about_app.app_name" = "%@ Wallet"; -"settings.about_app.description" = "The %@ wallet is built for those looking to invest and store cryptocurrencies in a private and independent manner.\n\nIt's a non-custodial, peer-to-peer wallet where only the user has control over the funds. It doesn't collect any data and keeps the user independent by not locking the user's funds to a specific wallet app.\n\nThe %@ wallet is fully open-source and anyone can confirm the app works exactly as it claims to."; -"settings.about_app.whats_new" = "What's New"; +"settings.about_app.app_version" = "App Version"; "settings.about_app.website" = "Website"; // Settings -> About App -> Contact @@ -1429,11 +1531,11 @@ // Settings -> Privacy "settings.privacy" = "Privacy"; -"settings.privacy.description" = "%@ doesn't collect any data or use analytics tools that may expose any data about its users. The wallet is designed to ensure a high level of privacy for its users."; -"settings.privacy.statement.user_data_storage" = "User data always remains on the user's device."; +"settings.privacy.description" = "%@ doesn't collect personal data that expose your private information i.e. coin balances or addresses. While we gather some UI usage statistics, it's solely for understanding our user base and app usage trends. That can be disabled if you wish."; "settings.privacy.statement.data_usage" = "The wallet doesn't collect any data about users."; -"settings.privacy.statement.data_privacy" = "The wallet doesn't share any data about users."; -"settings.privacy.statement.user_account" = "There are no user accounts or databases keeping user data elsewhere."; +"settings.privacy.statement.data_storage" = "There are no user accounts or databases storing user data."; +"settings.privacy.statement.user_account" = "If allowed the wallet will share app usage habits with Unstoppable team. This is to understand which features are being used (or not) by our users. Being a privacy focused app we need some way to evaluate our efforts and without this we have no idea whether the features we built are being used or not."; +"settings.privacy.allow" = "Share UI Data"; // Settings -> Appearance @@ -1444,21 +1546,25 @@ "appearance.theme.dark" = "Dark"; "appearance.theme.light" = "Light"; -"appearance.tab_settings" = "Tab Settings"; "appearance.markets_tab" = "Markets Tab"; +"appearance.hide_markets" = "Hide Markets"; +"appearance.price_change" = "Price Change"; +"appearance.price_change.24h" = "24H"; +"appearance.price_change.1d" = "Midnight UTC"; + "appearance.launch_screen" = "Launch Screen"; "appearance.launch_screen.auto" = "Auto"; "appearance.launch_screen.balance" = "Balance"; "appearance.launch_screen.market_overview" = "Market Overview"; "appearance.launch_screen.watchlist" = "Watchlist"; -"appearance.app_icon" = "App Icon"; - -"appearance.balance_conversion" = "Balance Conversion"; - +"appearance.balance_tab" = "Balance Tab"; +"appearance.hide_buttons" = "Hide Buttons"; "appearance.balance_value" = "Balance Value"; -"appearance.balance_value.coin_value" = "Coin Value"; -"appearance.balance_value.fiat_value" = "Fiat Value"; +"appearance.balance_value.coin_fiat" = "Coin / Fiat"; +"appearance.balance_value.fiat_coin" = "Fiat / Coin"; + +"appearance.app_icon" = "App Icon"; // Settings -> Contacts @@ -1957,7 +2063,6 @@ "tron.send.fee.info" = "The estimated cost of sending a given transaction on the network. (Without excluding Energy, Bandwidth, and Activating Fee)"; "tron.send.resources_consumed.info" = "Bandwidth is the unit that measures the size of the transaction bytes stored in the blockchain database. The larger the transaction, the more bandwidth resources will be consumed.\n\nEnergy is the unit that measures the amount of computation required by the TRON virtual machine to perform specific operations on the TRON network.\n\nSince smart contract transactions require computing resources to execute, each smart contract transaction requires to pay for the energy fee."; "tron.send.activation_fee.info" = "Transferring TRX or TRC-10 tokens to an inactive account address will activate the account."; -"tron.send.inactive_address" = "This address is not active"; // Cex Coin Select diff --git a/UnstoppableWallet/UnstoppableWallet/es.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/es.lproj/Localizable.strings index 489f8d2e3e..baf7542907 100644 --- a/UnstoppableWallet/UnstoppableWallet/es.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/es.lproj/Localizable.strings @@ -228,6 +228,7 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "extended_key.purpose" = "Propósito"; "extended_key.blockchain" = "Blockchain"; "extended_key.account" = "Account"; +"extended_key.account.description" = "Esta es una configuración para usuarios avanzados. Si estás intentando importar una billetera (a través de una clave privada extendida) o una lista de transacciones (a través de una clave pública extendida), necesitas la cuenta 0."; "extended_key.tap_to_show" = "Toque para mostrar extended private key"; // Backup @@ -318,7 +319,6 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "balance.downloading_blocks" = "Descargando bloques"; "balance.scanning_blocks" = "Bloques de escaneo"; "balance.enhancing_transactions" = "Mejorar las transacciones"; -"wait_for_synchronization" = "Esperando sincronización"; "balance.searching.count" = "%@ tx"; "balance.syncing_percent" = "Sincronizando... %@"; @@ -351,6 +351,8 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "balance.token.frozen" = "Congelados"; "balance.token.frozen.info.title" = "Título congelado"; "balance.token.frozen.info.description" = "Texto de descripción congelada"; +"balance.token.account.inactive.title" = "Cuenta No Activa"; +"balance.token.account.inactive.description" = "Las nuevas billeteras TRON requieren un depósito de al menos 1 TRX para activarse. Las billeteras inactivas pueden mantener y recibir tokens, pero no corregirán los saldos hasta que se activen."; // Account switcher @@ -445,6 +447,8 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "send.transaction_inputs_outputs_info.shuffle.description" = "El orden de salida de las transacciones se aleatoriza en cada transacción. A veces el cambio puede ser la primera salida, a veces puede ser la segunda. Si un usuario confía en el desarrollador de la aplicación, considere esta una opción recomendada."; "send.transaction_inputs_outputs_info.deterministic.title" = "2. Deterministic"; "send.transaction_inputs_outputs_info.deterministic.description" = "Existe un estándar comúnmente acordado para ordenar las salidas de las transacciones (conocido como BIP69). En carteras de código abierto, ese estándar asegura que los usuarios de la cartera no necesiten confiar en cómo los desarrolladores de la aplicación implementan el orden de las salidas. Dado que este estándar es nuevo, todavía no muchos monederos lo han implementado. Como resultado, es algo posible ver en la cadena de bloques si una transacción fue enviada desde una cartera que utiliza ese estándar o no."; +"send.select_all" = "Seleccionar Todo"; +"send.unselect_all" = "Deseleccionar Todo"; "send.confirmation.title" = "Confirmar"; "send.confirmation.you_send" = "Usted envía"; @@ -462,18 +466,20 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "send.confirmation.time_lock" = "TimeLock"; "send.confirmation.replace_by_fee" = "Reemplazar por tarifa"; "send.confirmation.replaced_transactions" = "Transacciones Reemplazadas"; - +"send.confirmation.input" = "Entrada"; + +"send.confirmation.sync_failed" = "Error de sincronización"; +"send.confirmation.invalid_data" = "Datos no válidos"; +"send.confirmation.refresh" = "Actualizar"; +"send.confirmation.please_wait" = "Por favor, espere"; +"send.confirmation.expires_in" = "Expira en %@"; +"send.confirmation.expired" = "Expirado"; "send.confirmation.slide_to_send" = "Deslizar para enviar"; "send.confirmation.sending" = "Enviar"; "send.confirmation.sent" = "Enviado"; "send.confirmation.slide_to_approve" = "Deslizar para aprobar"; -"send.confirmation.approving" = "Aprobando"; -"send.confirmation.approved" = "Aprobar"; - "send.confirmation.slide_to_revoke" = "Deslizar para revocar"; -"send.confirmation.revoking" = "Revocando"; -"send.confirmation.revoked" = "Revocada"; "send.confirmation.slide_to_resend" = "Reenviar"; "send.confirmation.slide_to_cancel" = "Cancelar transacción"; @@ -506,6 +512,7 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "send.lock_time" = "Timelock"; "send.unspent_outputs" = "UTXOs"; +"send.unspent_outputs.description" = "Seleccione manualmente UTxO para gastar los fondos en el saldo"; "send.unspent_outputs.send_to" = "Enviar a"; "send.unspent_outputs.change" = "Cambio"; @@ -513,6 +520,14 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "approve.confirmation.you_revoke" = "Revocas"; "approve.confirmation.spender" = "Gastador"; +"send.enter_amount" = "Introducir cantidad"; +"send.enter_address" = "Introduce una dirección"; +"send.invalid_address" = "Dirección inválida"; +"send.token_not_enabled" = "Token no habilitado"; +"send.token_syncing" = "Sincronización de tokens"; +"send.token_not_synced" = "Token no sincronizado"; +"send.insufficient_balance" = "Saldo insuficiente"; + // Donate "donate.list.title" = "Donar con"; @@ -643,7 +658,6 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "swap.confirmation.slide_to_swap" = "Deslizar para intercambiar"; "swap.confirmation.swapping" = "Intercambio"; -"swap.confirmation.swapped" = "Cambiado"; "swap.confirmation.refresh" = "Actualizar"; "swap.confirmation.impact_too_high" = "%@ ha desactivado la acción de intercambio para esta operación porque está recibiendo un precio extremadamente desfavorable. Esto se debe a una liquidez extremadamente baja. \nSi aún desea intercambiar, utilice el sitio web %@ en su lugar."; "swap.confirmation.impact_warning" = "¡Importante! Has obtenido un precio extremadamente desfavorable debido a la baja liquidez del mercado."; @@ -686,6 +700,51 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "market.defi_cap" = "DeFi Cap"; "market.defi_tvl" = "TVL en DeFi"; +"market.global.market_cap" = "Capitalización Total"; +"market.global.volume" = "Vol. de 24 h"; +"market.global.btc_dominance" = "Dominio de BTC"; +"market.global.etf_inflow" = "Entrada ETF"; +"market.global.tvl_in_defi" = "TVL en DeFi"; + +"market.tab.news" = "Noticias"; +"market.tab.coins" = "Monedas"; +"market.tab.watchlist" = "Lista de Seguimiento"; +"market.tab.platforms" = "Plataformas"; +"market.tab.pairs" = "Pares"; +"market.tab.sectors" = "Sectores"; + +"market.sort_by.title" = "Ordenar por"; +"market.sort_by.manual" = "Manual"; +"market.sort_by.highest_cap" = "Cap Mayor"; +"market.sort_by.lowest_cap" = "Cap Menor"; +"market.sort_by.gainers" = "Ganadores"; +"market.sort_by.losers" = "Perdedores"; +"market.sort_by.highest_volume" = "Mayor Volumen"; +"market.sort_by.lowest_volume" = "Menor Volumen"; + +"market.top_coins.title" = "Monedas"; +"market.top_coins" = "Top %@"; + +"market.time_period.title" = "Period"; +"market.time_period.1d" = "1 Día"; +"market.time_period.1w" = "1 Semana"; +"market.time_period.2w" = "2 Semanas"; +"market.time_period.1m" = "1 Mes"; +"market.time_period.3m" = "3 Meses"; +"market.time_period.6m" = "6 Meses"; +"market.time_period.1y" = "1 Año"; +"market.time_period.2y" = "2 Años"; +"market.time_period.5y" = "5 Años"; +"market.time_period.1d.short" = "1D"; +"market.time_period.1w.short" = "1S"; +"market.time_period.2w.short" = "2S"; +"market.time_period.1m.short" = "1M"; +"market.time_period.3m.short" = "3M"; +"market.time_period.6m.short" = "6M"; +"market.time_period.1y.short" = "1A"; +"market.time_period.2y.short" = "2A"; +"market.time_period.5y.short" = "5A"; + "market.project_has_no_coin" = "Este proyecto no tiene una moneda"; "market.top.section.header.see_all" = "Ver Todos"; @@ -710,8 +769,8 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "market.top.title" = "Monedas Top"; "market.top.description" = "Mejores monedas por rango de límite de mercado"; -"market.top.highest_cap" = "Capitalización Mayor"; -"market.top.lowest_cap" = "Capitalización Menor"; +"market.top.highest_cap" = "Cap Mayor"; +"market.top.lowest_cap" = "Cap Menor"; "market.top.highest_volume" = "Mayor Volumen"; "market.top.lowest_volume" = "Menor Volumen"; "market.top.top_gainers" = "Máximos Ganadores"; @@ -722,6 +781,7 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "market.top.top_platforms" = "Plataformas superiores"; "market.top.protocols" = "Protocolos"; +"market.pairs.volume" = "Volumen"; "top_pairs.title" = "Mejores pares de mercado"; "top_pairs.description" = "Mejores pares de trading por volumen en cada bolsa"; @@ -730,6 +790,8 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "top_platform.title" = "Ecosistema %@"; "top_platform.description" = "Límite de mercado de todos los protocolos en la cadena %@"; +"top_platform.total_cap" = "Capitalización Total"; + "market.search.recent" = "Reciente"; "market.search.popular" = "Popular"; @@ -741,10 +803,22 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "market_discovery.not_found" = "No se han encontrado resultados"; "market_watchlist.empty.caption" = "Tu Watchlist está vacía."; +"market.watchlist.signals" = "Señales"; +"market.watchlist.empty" = "Tu Watchlist está vacía"; +"market.watchlist.signals.description" = "Las señales a continuación se basan en los indicadores técnicos de precio Bollinger Bands y RSI durante aproximadamente los últimos 30 días. Estas señales son algorítmicas y pueden cambiar con frecuencia."; +"market.watchlist.signals.strong_buy.description" = "Alta confianza en aumento del precio"; +"market.watchlist.signals.buy.description" = "Probable aumento del precio en el futuro cercano"; +"market.watchlist.signals.neutral.description" = "No hay una tendencia clara, el mercado está en equilibrio"; +"market.watchlist.signals.sell.description" = "Probable disminución del precio en el futuro cercano"; +"market.watchlist.signals.strong_sell.description" = "High probability of price decrease"; +"market.watchlist.signals.risky.description" = "Nivel de riesgo elevado, requiere precaución"; +"market.watchlist.signals.warning" = "Recuerda siempre aplicar gestión de riesgos, y ten en cuenta que esto no es un consejo financiero."; +"market.watchlist.signals.turn_on" = "Activar"; "market.advanced_search.title" = "Filtros"; "market.advanced_search.show_results" = "Mostrar Resultados"; "market.advanced_search.empty_results" = "Resultados Vacíos"; +"market.advanced_search.retry" = "Reintentar"; "market.advanced_search.dex_description" = "Esta opción se aplica a las fichas negociadas en Ethereum (Uniswap DEX) y Binance Smart Chain (Pancake DEX)."; "market.advanced_search.24h" = "24 horas"; @@ -764,17 +838,10 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "market.advanced_search.liquidity" = "Liquidez DEX"; "market.advanced_search.blockchains" = "Blockchains"; -"market.advanced_search.technical_advice" = "Señales de Trading"; +"market.advanced_search.signal" = "Señal de Trading"; "market.advanced_search.price_period" = "Período de Precio"; "market.advanced_search.price_change" = "Cambio de Precio"; -"market.advanced_search.technical_advice.risk_trade" = "Riesgo para el Comercio"; -"market.advanced_search.technical_advice.strong_buy" = "Compra Fuerte"; -"market.advanced_search.technical_advice.buy" = "Comprar"; -"market.advanced_search.technical_advice.neutral" = "Neutral"; -"market.advanced_search.technical_advice.sell" = "Vender"; -"market.advanced_search.technical_advice.strong_sell" = "Vender fuerte"; - "market.advanced_search.outperformed_btc" = "BTC superado"; "market.advanced_search.outperformed_eth" = "ETH superado"; "market.advanced_search.outperformed_bnb" = "BNB superado"; @@ -808,7 +875,7 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "market.advanced_search.more_50_b" = "> 50MM"; "market.advanced_search.more_500_b" = "> 500MM"; -"market.advanced_search.day" = "1 día"; +"market.advanced_search.day" = "1 Día"; "market.advanced_search.week" = "1 Semana"; "market.advanced_search.week2" = "2 Semanas"; "market.advanced_search.month" = "1 Mes"; @@ -817,7 +884,7 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "market.advanced_search.day.short" = "24H"; "market.advanced_search.week.short" = "7D"; -"market.advanced_search.month.short" = "1 M"; +"market.advanced_search.month.short" = "1M"; "market.advanced_search_results.title" = "Resultados"; @@ -830,10 +897,36 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "market.global.defi_cap.title" = "DeFi Cap"; "market.global.defi_cap.description" = "Valor total del mercado de proyectos DeFi"; -"market.global.tvl_in_defi.title" = "TVL en DeFi"; -"market.global.tvl_in_defi.description" = "Valor total bloqueado (TVL) en DeFi"; -"market.global.tvl_in_defi.multi_chain" = "Cadena múltiple"; -"market.global.tvl_in_defi.filter_by_chain" = "Filtrar por cadena"; +"market.etf.title" = "Ingreso Neto Total"; +"market.etf.description" = "El ingreso neto de un ETF es igual a sus entradas de efectivo menos las salidas."; +"market.etf.total_net_assets" = "Activos Netos Totales"; +"market.etf.sort_by.highest_assets" = "Mayores Activos"; +"market.etf.sort_by.lowest_assets" = "Activos Más Bajos"; +"market.etf.sort_by.inflow" = "Ingreso"; +"market.etf.sort_by.outflow" = "Salida"; +"market.etf.period.all" = "Todo"; + +"market.market_cap.title" = "Cap.Total del Mercado"; +"market.market_cap.description" = "Valor total del mercado de todas las criptomonedas"; +"market.market_cap.market_cap" = "Capitalización del Mercado"; + +"market.tvl_in_defi.title" = "TVL en DeFi"; +"market.tvl_in_defi.description" = "Valor total bloqueado (TVL) en DeFi"; +"market.tvl_in_defi.tvl" = "TVL"; +"market.tvl_in_defi.multi_chain" = "Multi-chain"; +"market.tvl_in_defi.filter_by_chain" = "Filtrar por cadena"; +"market.tvl_in_defi.filter.all" = "Todo"; + +"market.volume.title" = "Volumen"; +"market.volume.description" = "El volumen de operaciones 24h del mercado de criptomonedas"; +"market.volume.volume" = "Volumen"; + +"market.signal.risky" = "Arriesgado"; +"market.signal.strong_buy" = "Compra Fuerte"; +"market.signal.buy" = "Comprar"; +"market.signal.neutral" = "Neutral"; +"market.signal.sell" = "Vender"; +"market.signal.strong_sell" = "Vender fuerte"; // Coin Page @@ -854,7 +947,8 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "coin_overview.genesis_date" = "Fecha de inicio"; "coin_overview.trading_volume" = "Volumen de Comercio"; -"coin_overview.roi.hour24" = "1 día"; +"coin_overview.roi.hour24" = "24 Horas"; +"coin_overview.roi.day1" = "1 Día"; "coin_overview.roi.day7" = "1 Semana"; "coin_overview.roi.day14" = "2 Semanas"; "coin_overview.roi.day30" = "1 Mes"; @@ -875,55 +969,55 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "coin_overview.whitepaper" = "Libro blanco"; // Coin Page -> Analytics -"technical_advice.over.bought" = "overbought"; -"technical_advice.over.sold" = "oversold"; -"technical_advice.down" = "down"; -"technical_advice.up" = "up"; +"technical_advice.over.bought" = "sobrecomprado"; +"technical_advice.over.sold" = "sobrevendido"; +"technical_advice.down" = "abajo"; +"technical_advice.up" = "abajo"; -"technical_advice.over.main" = "The actions with the asset are risky."; -"technical_advice.over.indicators.signal_date" = "Starting from the %@"; -"technical_advice.over.indicators" = "The asset is outside the Bollinger Band channel and %@."; -"technical_advice.over.rsi" = " RSI = %@, This also indicates that the asset is %@."; -"technical_advice.over.advice" = " There might be a strong %@ward movement, so it's better to wait for the asset price to return to the channel."; +"technical_advice.over.main" = "Las acciones con el activo son riesgosas."; +"technical_advice.over.indicators.signal_date" = "Comenzando desde el %@"; +"technical_advice.over.indicators" = "El activo está fuera del canal de bandas de Bollinger y %@."; +"technical_advice.over.rsi" = " RSI = %@. Esto también indica que el activo está %@."; +"technical_advice.over.advice" = " Podría haber un fuerte movimiento hacia %@, por lo que es mejor esperar a que el precio del activo regrese al canal."; -"technical_advice.strong.indicators" = "The asset was %@, but now it has returned to the Bollinger Band channel. This indicates a possible trend reversal."; -"technical_advice.strong.rsi" = " Meanwhile, the RSI is %@, which still indicates that it is %@."; -"technical_advice.strong.advice" = " This could be a very strong signal to enter the market. Keep in mind that there may be several attempts of %@ward movement after returning to the channel, so do not forget about risk management."; +"technical_advice.strong.indicators" = "El activo estaba %@, pero ahora ha regresado al canal de Bandas de Bollinger. Esto indica una posible reversión de la tendencia."; +"technical_advice.strong.rsi" = " Mientras tanto, el RSI es %@, lo que aún indica que está %@."; +"technical_advice.strong.advice" = " Este podría ser una señal muy fuerte para ingresar al mercado. Ten en cuenta que puede haber varios intentos de movimiento hacia %@ después de regresar al canal, así que no olvides el manejo del riesgo."; -"technical_advice.stable.rsi" = " Meanwhile, the RSI is %@, which also indicates a trend reversal (RSI crossed the boundary at 70%)."; -"technical_advice.stable.advice" = "The price is returning to neutral levels, however, there is still potential for upward movement. Keep in mind that RSI = 50 and the middle of the Bollinger Bands are strong resistances and possible trend reversal points. Do not forget about risk management."; +"technical_advice.stable.rsi" = " Mientras tanto, el RSI es %@, lo que también indica una reversión de la tendencia (el RSI cruzó el límite del 70%)."; +"technical_advice.stable.advice" = "El precio está volviendo a niveles neutrales; sin embargo, aún existe potencial para movimientos hacia arriba. Ten en cuenta que RSI = 50 y el centro de las Bandas de Bollinger son resistencias fuertes y posibles puntos de reversión de la tendencia. No olvides el manejo del riesgo."; -"technical_advice.neutral.rsi" = "RSI = %@ also confirms the absence of a strong trend."; -"technical_advice.neutral.indicators" = "The asset was in the overbought/oversold zone, but at the moment the price has returned to the Bollinger Band channel in the neutral zone. The RSI is %@ also confirms the absence of a strong trend, so overall the asset price is moving towards averaging and further movement is possible in any direction."; -"technical_advice.neutral.advice" = " In general, the asset price is moving towards averaging and further movement is possible in any direction."; +"technical_advice.neutral.rsi" = "RSI = %@ también confirma la ausencia de una tendencia fuerte."; +"technical_advice.neutral.indicators" = "El activo estaba en la zona de sobrecompra/sobreventa, pero en este momento el precio ha regresado al canal de las Bandas de Bollinger en la zona neutral. El RSI es %@, lo que también confirma la ausencia de una tendencia fuerte, por lo que en general el precio del activo se está moviendo hacia la media y es posible un movimiento adicional en cualquier dirección."; +"technical_advice.neutral.advice" = " En general, el precio del activo se está moviendo hacia la media y es posible que se produzca un movimiento en cualquier dirección."; -"technical_advice.other.title" = "Please note:"; +"technical_advice.other.title" = "Por favor, toma nota:"; -"technical_advice.ema.above" = "above"; -"technical_advice.ema.below" = "below"; -"technical_advice.ema.growth" = "growth"; -"technical_advice.ema.decrease" = "decrease"; -"technical_advice.ema.advice" = "EMA 200. Determines the overall sentiment and trend. The daily price of the asset is located %@ the EMA (%@). This means that globally the asset is set for %@."; +"technical_advice.ema.above" = "arriba"; +"technical_advice.ema.below" = "abajo"; +"technical_advice.ema.growth" = "crecimiento"; +"technical_advice.ema.decrease" = "disminución"; +"technical_advice.ema.advice" = "EMA 200. Determina el sentimiento y la tendencia general. El precio diario del activo se encuentra %@ la EMA (%@). Esto significa que globalmente el activo está configurado para %@."; -"technical_advice.macd.positive" = "above"; -"technical_advice.macd.negative" = "below"; -"technical_advice.macd.advice" = "MACD. Assesses the strength of the trend considering the average price change. The daily value of the histogram is %@ (%@). The price of the asset globally may move %@."; +"technical_advice.macd.positive" = "arriba"; +"technical_advice.macd.negative" = "abajo"; +"technical_advice.macd.advice" = "MACD. Evalúa la fuerza de la tendencia considerando el cambio promedio de precio. El valor diario del histograma es %@ (%@). El precio del activo a nivel global puede moverse %@."; "coin_analytics.indicators.title" = "Indicadores técnicos"; -"coin_analytics.indicators.disclaimer" = "Always remember to apply risk management, and note that this is not financial advice."; -"coin_analytics.indicators.info.title" = "Technical Indicators"; -"coin_analytics.indicators.info.description" = "We use the Bollinger Bands + RSI strategy to determine trading signals. All calculations are based on daily candlesticks and provide advice for a moderately long term. The essence of the strategy is that the asset price should reach an extreme, breaking out of the Bollinger Bands channel, and the RSI should be in the overbought/oversold zone. After the price returns to the channel, there is a high probability of the price returning to the mean values or attempting to break the channel from the other side. Note that the strategy may give several false signals during strong market movements before a correct signal appears.\n\nPlease remember that it is very important to apply risk management to trading and remember to cut losses if the market situation changes! "; -"coin_analytics.indicators.hide_details" = "Hide Details"; -"coin_analytics.indicators.show_details" = "Show Details"; +"coin_analytics.indicators.disclaimer" = "Recuerda siempre aplicar gestión de riesgos, y ten en cuenta que esto no es un consejo financiero."; +"coin_analytics.indicators.info.title" = "Indicadores Técnicos"; +"coin_analytics.indicators.info.description" = "Utilizamos la estrategia de Bollinger Bands + RSI para determinar señales de trading. Todos los cálculos se basan en velas diarias y proporcionan consejos para un término moderadamente largo. La esencia de la estrategia es que el precio del activo debe alcanzar un extremo, saliendo del canal de las Bandas de Bollinger, y el RSI debe estar en la zona de sobrecompra/sobreventa. Después de que el precio regrese al canal, hay una alta probabilidad de que el precio regrese a los valores medios o intente romper el canal desde el otro lado. Ten en cuenta que la estrategia puede dar varias señales falsas durante movimientos fuertes del mercado antes de que aparezca una señal correcta.\n\n¡Recuerda que es muy importante aplicar la gestión del riesgo al trading y recordar cortar pérdidas si la situación del mercado cambia! "; +"coin_analytics.indicators.hide_details" = "Ocultar Detalles"; +"coin_analytics.indicators.show_details" = "Mostrar Detalles"; "coin_analytics.indicators.summary" = "Resumen"; "coin_analytics.indicators.no_data" = "No hay datos"; -"coin_analytics.indicators.oversold" = "Very Risky to Trade"; -"coin_analytics.indicators.strong_buy" = "Compra fuerte"; +"coin_analytics.indicators.oversold" = "Arriesgado"; +"coin_analytics.indicators.strong_buy" = "Compra Fuerte"; "coin_analytics.indicators.buy" = "Comprar"; "coin_analytics.indicators.neutral" = "Neutral"; "coin_analytics.indicators.sell" = "Vender"; "coin_analytics.indicators.strong_sell" = "Vender fuerte"; -"coin_analytics.indicators.overbought" = "Very Risky to Trade"; +"coin_analytics.indicators.overbought" = "Arriesgado"; "coin_analytics.period" = "Period"; "coin_analytics.period.select_title" = "Seleccionar Periodo"; "coin_analytics.period.1h" = "1 hora"; @@ -936,7 +1030,7 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "coin_analytics.not_available" = "Este proyecto no tiene datos analíticos"; -"coin_analytics.technical_indicators" = "Technical Indicators"; +"coin_analytics.technical_indicators" = "Indicadores Técnicos"; "coin_analytics.technical_indicators.info1" = "Resumen: Esta es una visión general de los técnicos de un activo, considerando una variedad de indicadores técnicos y plazos. Proporciona un punto de vista de consenso (Compra, Venta o Neutral) basado en estos indicadores."; "coin_analytics.technical_indicators.info2" = "Promedio de movimiento (MA): Estos son indicadores técnicos comúnmente utilizados que suavizan los datos de precios para crear un indicador que sigue la tendencia. Ellos muestran el precio medio en un período de tiempo determinado. Hay varios tipos de EMg:\n\nMedia simple de movimiento (SMA): Esto calcula el promedio de un rango de precios seleccionado. por lo general cerrando los precios, por el número de períodos en ese rango.\n\nMedia de movimiento exponencial (EMA): Esto da más peso a los precios recientes, respondiendo así más rápidamente a los cambios recientes de precios."; "coin_analytics.technical_indicators.info3" = "Osciladores: son indicadores técnicos que fluctúan con el tiempo dentro de una banda (por encima y por debajo de una línea central o entre niveles establecidos). Están diseñados para ayudar a identificar condiciones de sobrecompra y sobreventa en un mercado. Aquí hay algunos osciladores comunes:\n\nÍndice de Fuerza Relativa (RSI): esto mide la velocidad y el cambio de los movimientos de precios. Por lo general, se utiliza para identificar condiciones de sobrecompra o sobreventa.\n\nConvergencia y Divergencia de Medias Móviles (MACD): se utiliza para identificar posibles señales de compra y venta. Dispara señales técnicas cuando cruza por encima (para comprar) o por debajo (para vender) de su línea de señal."; @@ -944,6 +1038,7 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "coin_analytics.cex_volume" = "Volumen CEX"; "coin_analytics.cex_volume_rank" = "Rango de Volumen CEX"; "coin_analytics.cex_volume_rank.description" = "Tokens clasificados por volumen de comercio del token en exchanges centralizados."; +"coin_analytics.cex_volume_rank.sorting_field" = "Volumen"; "coin_analytics.cex_volume.info1" = "Total trading volume for the token on leading centralized exchanges over 30-day period."; "coin_analytics.cex_volume.info2" = "Gráfico que muestra la variación en el volumen diario de comercio del token en los principales exchanges centralizados durante un período de 1 año."; "coin_analytics.cex_volume.info3" = "Token's rank based on trading volume on leading centralized exchanges over 30-day period."; @@ -952,6 +1047,7 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "coin_analytics.dex_volume" = "Volumen DEX"; "coin_analytics.dex_volume_rank" = "Rango de Volumen DEX"; "coin_analytics.dex_volume_rank.description" = "Tokens clasificados por volumen de negociación para el token en los intercambios descentralizados."; +"coin_analytics.dex_volume_rank.sorting_field" = "Volumen"; "coin_analytics.dex_volume.info1" = "Total trading volume for the token on leading centralized exchanges over 30-day period."; "coin_analytics.dex_volume.info2" = "Gráfico que muestra variación en el volumen de operaciones diarias para la ficha de los principales intercambios descentralizados en un período de 1 año."; "coin_analytics.dex_volume.info3" = "El rango de Token se basa en el volumen de negociación de los principales intercambios descentralizados durante un período de 30 días."; @@ -963,6 +1059,7 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "coin_analytics.dex_liquidity" = "Liquidez DEX"; "coin_analytics.dex_liquidity_rank" = "Rango de liquidez DEX"; "coin_analytics.dex_liquidity_rank.description" = "Tokens clasificados por liquidez disponible en los intercambios descentralizados."; +"coin_analytics.dex_liquidity_rank.sorting_field" = "Liquidez"; "coin_analytics.dex_liquidity.info1" = "Liquidez total actualmente disponible para el token en los principales exchanges descentralizados."; "coin_analytics.dex_liquidity.info2" = "Gráfico que muestra la variación en la liquidez disponible para el token en los principales exchanges descentralizados durante un período de 1 año."; "coin_analytics.dex_liquidity.info3" = "Lista de todos los tokens clasificados según la liquidez disponible del token en los principales exchanges descentralizados."; @@ -974,6 +1071,7 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "coin_analytics.active_addresses.30_day_unique_addresses" = "Direcciones únicas de 30 días"; "coin_analytics.active_addresses_rank" = "Rango de direcciones activas"; "coin_analytics.active_addresses_rank.description" = "Tokens clasificados por el número de direcciones únicas que transaccionan con el token."; +"coin_analytics.active_addresses_rank.sorting_field" = "Activo"; "coin_analytics.active_addresses.info1" = "Total number of unique daily active addresses over 24-hour period."; "coin_analytics.active_addresses.info2" = "Gráfico que muestra la variación en el recuento diario de direcciones activas durante un período de 1 año."; "coin_analytics.active_addresses.info3" = "Número total de direcciones únicas de blockchain que realizan transacciones con el token durante un período de 30 días."; @@ -983,6 +1081,7 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "coin_analytics.transaction_count" = "Número de transacciones"; "coin_analytics.transaction_count_rank" = "Rango de conteo Tx"; "coin_analytics.transaction_count_rank.description" = "Tokens clasificados por el número de transacciones en una cadena de bloques."; +"coin_analytics.transaction_count_rank.sorting_field" = "Contador"; "coin_analytics.transaction_count.info1" = "Total number of unique blockchain transactions with token over 30-day period."; "coin_analytics.transaction_count.info2" = "Gráfico que muestra la variación en el número de transacciones durante un período de 1 año."; "coin_analytics.transaction_count.info3" = "Token's rank based on the number of transactions with the token 30-day period."; @@ -992,6 +1091,7 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "coin_analytics.holders" = "Titulares"; "coin_analytics.holders_rank" = "Rango de tenedores"; "coin_analytics.holders_rank.description" = "Clasificación de tokens por direcciones únicas que los poseen en múltiples blockchains."; +"coin_analytics.holders_rank.sorting_field" = "Titulares"; "coin_analytics.holders.info1" = "Número total de direcciones únicas que poseen el token en varios blockchains."; "coin_analytics.holders.info2" = "Los 10 principales monederos que poseen el token en cada blockchain."; "coin_analytics.holders.tracked_blockchains" = "Blockchains seguidos: Ethereum, Binance Smart Chain, Optismo, Arbitrum, Celo, Cronos, Avalanche, Fantom, Polígono"; @@ -1011,10 +1111,12 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "coin_analytics.project_fee" = "Tarifa del proyecto"; "coin_analytics.project_fee_rank" = "Rango de tarifa del proyecto"; "coin_analytics.project_fee_rank.description" = "Las fichas se clasifican de acuerdo a las tarifas generadas por los proyectos respectivos. La forma en que se cobran los honorarios varía de un proyecto a otro."; +"coin_analytics.project_fee_rank.sorting_field" = "Volumen"; "coin_analytics.project_revenue" = "Ingresos del Proyecto"; "coin_analytics.project_revenue_rank" = "Rango de Ingresos del Proyecto"; "coin_analytics.project_revenue_rank.description" = "Las fichas clasificadas por los ingresos generados para los titulares a través de mecanismos como estocar o quemar fichas."; +"coin_analytics.project_revenue_rank.sorting_field" = "Ingresos"; "coin_analytics.other_data" = "Otros Datos"; @@ -1057,8 +1159,8 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "coin_analytics.analysis.title" = "Análisis de contratos inteligente"; "coin_analytics.analysis.footer" = "Desarrollado por De.Fi"; "coin_analytics.analysis.high_risk_items" = "Elementos de Alto Riesgo"; -"coin_analytics.analysis.medium_risk_items" = "Medium Risk Items"; -"coin_analytics.analysis.attention_required" = "Attention Required"; +"coin_analytics.analysis.medium_risk_items" = "Elementos de Riesgo Medio"; +"coin_analytics.analysis.attention_required" = "Atención Requerida"; "coin_analytics.analysis.token_detectors" = "Detectores de Tokens"; "coin_analytics.analysis.general_detectors" = "Detectores Generales"; "coin_analytics.analysis.issues" = "Issues: %@"; @@ -1200,6 +1302,8 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "settings.rate_us" = "Califícanos"; "settings.tell_friends" = "Recomendar a un amigo"; "settings.contact_us" = "Contáctenos"; +"settings.social_networks.label" = "Be Unstoppable"; +"settings.social_networks.footer" = "Aprende y domina criptografía a través de vídeos exclusivos. Conozca informalmente. Sé el primero en ver las cosas en las que trabajamos."; // Settings -> Base Currency @@ -1418,9 +1522,7 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; // Settings -> About App "settings.about_app.title" = "Sobre la App"; -"settings.about_app.app_name" = "Cartera %@"; -"settings.about_app.description" = "La cartera %@ está diseñada para aquellos que buscan invertir y almacenar criptomonedas de manera privada e independiente.\n\nEs una cartera no custodial y peer-to-peer donde solo el usuario tiene control sobre los fondos. No recopila ningún dato y mantiene al usuario independiente al no bloquear los fondos del usuario en una aplicación de cartera específica.\n\nLa cartera %@ es completamente de código abierto y cualquiera puede confirmar que la aplicación funciona exactamente como afirma."; -"settings.about_app.whats_new" = "Qué novedades hay"; +"settings.about_app.app_version" = "Versión de la Aplicación"; "settings.about_app.website" = "Website"; // Settings -> About App -> Contact @@ -1432,11 +1534,11 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; // Settings -> Privacy "settings.privacy" = "Privacidad"; -"settings.privacy.description" = "%@ no recopila ningún dato ni utiliza herramientas de análisis que puedan exponer cualquier dato sobre sus usuarios. La cartera está diseñada para garantizar un alto nivel de privacidad para sus usuarios."; -"settings.privacy.statement.user_data_storage" = "Los datos del usuario siempre permanecen en el dispositivo del usuario."; +"settings.privacy.description" = "%@ no recopila datos personales que exponen su información privada, es decir, saldos de monedas o direcciones. Mientras recopilamos algunas estadísticas de uso de la interfaz de usuario, es sólo para entender nuestras tendencias de uso de la base de usuarios y de la aplicación. Esto puede desactivarse si lo desea."; "settings.privacy.statement.data_usage" = "La cartera no recopila ningún dato sobre los usuarios."; -"settings.privacy.statement.data_privacy" = "La cartera no recopila ningún dato sobre los usuarios."; -"settings.privacy.statement.user_account" = "No hay cuentas de usuario o bases de datos que conserven los datos del usuario en otros lugares."; +"settings.privacy.statement.data_storage" = "No hay cuentas de usuario o bases de datos almacenando datos de usuario."; +"settings.privacy.statement.user_account" = "Si se permite el monedero compartirá los hábitos de uso de la aplicación con el equipo imparable. Esto es para entender qué características están siendo utilizadas (o no) por nuestros usuarios. Siendo una aplicación enfocada a la privacidad, necesitamos alguna forma de evaluar nuestros esfuerzos y sin esto no tenemos ni idea de si las características que construimos están siendo usadas o no."; +"settings.privacy.allow" = "Compartir datos de UI"; // Settings -> Appearance @@ -1447,21 +1549,25 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "appearance.theme.dark" = "Oscuro"; "appearance.theme.light" = "Claro"; -"appearance.tab_settings" = "Ajustes de pestaña"; "appearance.markets_tab" = "Pestaña de Mercados"; +"appearance.hide_markets" = "Ocultar Mercados"; +"appearance.price_change" = "Cambio de Precio"; +"appearance.price_change.24h" = "24H"; +"appearance.price_change.1d" = "Medianoche UTC"; + "appearance.launch_screen" = "Pantalla de lanzamiento"; "appearance.launch_screen.auto" = "Auto"; "appearance.launch_screen.balance" = "Saldo"; "appearance.launch_screen.market_overview" = "Visión General del Mercado"; "appearance.launch_screen.watchlist" = "Lista de Seguimiento"; -"appearance.app_icon" = "Icono de la App"; - -"appearance.balance_conversion" = "Conversión de balance"; - +"appearance.balance_tab" = "Balance Tab"; +"appearance.hide_buttons" = "Ocultar Botones"; "appearance.balance_value" = "Valor de Saldo"; -"appearance.balance_value.coin_value" = "Valor de la moneda"; -"appearance.balance_value.fiat_value" = "Valor Fiat"; +"appearance.balance_value.coin_fiat" = "Moneda / Fiat"; +"appearance.balance_value.fiat_coin" = "Fiat / Moneda"; + +"appearance.app_icon" = "Icono de la App"; // Settings -> Contacts @@ -1526,12 +1632,12 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "chart.time_duration.day" = "24H"; "chart.time_duration.week" = "7D"; -"chart.time_duration.week2" = "2 S"; -"chart.time_duration.month" = "1 M"; -"chart.time_duration.month3" = "3 M"; +"chart.time_duration.week2" = "2S"; +"chart.time_duration.month" = "1M"; +"chart.time_duration.month3" = "3M"; "chart.time_duration.halfyear" = "6M"; -"chart.time_duration.year" = "1 Año"; -"chart.time_duration.year2" = "2 Años"; +"chart.time_duration.year" = "1A"; +"chart.time_duration.year2" = "2A"; "chart.time_duration.year5" = "5A"; "chart.time_duration.all" = "TODO"; @@ -1960,7 +2066,6 @@ Ir a Ajustes - > %@ y permitir el acceso a la cámara."; "tron.send.fee.info" = "El coste estimado del envío de una transacción en la red. (sin excluir Energía, Ancho de Banda y Tarifa Activa)"; "tron.send.resources_consumed.info" = "El bandwidth es la unidad que mide el tamaño de los bytes de transacción almacenados en la base de datos de la cadena de bloques. Cuanto más grande sea la transacción, más recursos de ancho de banda se consumirán.\n\nEnerge es la unidad que mide la cantidad de computación requerida por la máquina virtual TRON para realizar operaciones específicas en la red TRON.\n\nDado que las transacciones de contratos inteligentes requieren recursos informáticos para ejecutarse, cada transacción de contrato inteligente debe pagar la tarifa de energy."; "tron.send.activation_fee.info" = "Transferir tokens TRX o TRC-10 a una dirección de cuenta inactiva activará la cuenta."; -"tron.send.inactive_address" = "Esta dirección no está activa"; // Cex Coin Select diff --git a/UnstoppableWallet/UnstoppableWallet/fr.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/fr.lproj/Localizable.strings index e43c9f76ea..cc2239d61b 100644 --- a/UnstoppableWallet/UnstoppableWallet/fr.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/fr.lproj/Localizable.strings @@ -63,7 +63,7 @@ "alert.enabled_coins" = "%@ pièces supplémentaires activées"; "alert.sending" = "En attente"; "alert.sent" = "Envoyée"; -"alert.swapping" = "Swapping"; +"alert.swapping" = "Échange"; "alert.swapped" = "Échangé"; "alert.approving" = "En cours d'approbation"; "alert.approved" = "Approuvé"; @@ -228,6 +228,7 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "extended_key.purpose" = "Objectif"; "extended_key.blockchain" = "Blockchain"; "extended_key.account" = "Compte"; +"extended_key.account.description" = "Il s'agit d'un paramètre pour les utilisateurs avancés. Si vous essayez d'importer un portefeuille (via une clé privée étendue) ou une liste de transactions (via une clé publique étendue), vous avez besoin du compte 0."; "extended_key.tap_to_show" = "Appuyez pour afficher extended private key"; // Backup @@ -318,7 +319,6 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "balance.downloading_blocks" = "Téléchargement des blocs"; "balance.scanning_blocks" = "Blocs de numérisation"; "balance.enhancing_transactions" = "Amélioration des transactions"; -"wait_for_synchronization" = "Attendez la synchronisation"; "balance.searching.count" = "%@ tx"; "balance.syncing_percent" = "Synchronisation... %@"; @@ -351,6 +351,8 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "balance.token.frozen" = "Gelée"; "balance.token.frozen.info.title" = "Titre gelé"; "balance.token.frozen.info.description" = "Texte de description gelé"; +"balance.token.account.inactive.title" = "Compte non actif"; +"balance.token.account.inactive.description" = "Les nouveaux portefeuilles TRON nécessitent un dépôt d'au moins 1 TRX pour devenir actifs. Les portefeuilles inactifs peuvent recevoir et détenir des jetons, mais les soldes ne seront pas corrigés tant qu'ils ne seront pas activés."; // Account switcher @@ -445,6 +447,8 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "send.transaction_inputs_outputs_info.shuffle.description" = "L'ordre des sorties de transaction est aléatoire à chaque transaction. Parfois, le changement peut être la première sortie, parfois la deuxième. Si un utilisateur fait confiance au développeur de l'application, alors considérez cela comme une option recommandée."; "send.transaction_inputs_outputs_info.deterministic.title" = "2. Déterministe"; "send.transaction_inputs_outputs_info.deterministic.description" = "Il existe une norme communément acceptée pour l'ordonnancement des sorties de transaction, connue sous le nom de BIP69 (Bitcoin Improvement Proposal 69). Dans les portefeuilles open source, cette norme garantit que les utilisateurs de portefeuille n'ont pas besoin de faire confiance à la manière dont les développeurs de l'application implémentent l'ordonnancement des sorties. Comme cette norme est relativement récente, peu de portefeuilles l'ont encore mise en œuvre. Par conséquent, il est quelque peu possible de déterminer sur la blockchain si une transaction a été envoyée depuis un portefeuille qui utilise cette norme ou non."; +"send.select_all" = "Tout sélectionner"; +"send.unselect_all" = "Désélectionner tout"; "send.confirmation.title" = "Confirmer"; "send.confirmation.you_send" = "Vous envoyez"; @@ -462,18 +466,20 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "send.confirmation.time_lock" = "TimeLock"; "send.confirmation.replace_by_fee" = "Remplacer par frais"; "send.confirmation.replaced_transactions" = "Transactions remplacées"; - +"send.confirmation.input" = "Entrées"; + +"send.confirmation.sync_failed" = "Échec de synchronisation"; +"send.confirmation.invalid_data" = "Données invalides"; +"send.confirmation.refresh" = "Actualiser"; +"send.confirmation.please_wait" = "Veuillez patienter"; +"send.confirmation.expires_in" = "Expire dans %@"; +"send.confirmation.expired" = "Expiré"; "send.confirmation.slide_to_send" = "Glisser pour envoyer"; "send.confirmation.sending" = "En attente"; "send.confirmation.sent" = "Envoyée"; "send.confirmation.slide_to_approve" = "Glisser pour approuver"; -"send.confirmation.approving" = "En cours d'approbation"; -"send.confirmation.approved" = "Approuvé"; - "send.confirmation.slide_to_revoke" = "Glisser pour révoquer"; -"send.confirmation.revoking" = "Révocation en cours"; -"send.confirmation.revoked" = "Révoqué"; "send.confirmation.slide_to_resend" = "Réenvoyer"; "send.confirmation.slide_to_cancel" = "Annuler la transaction"; @@ -506,6 +512,7 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "send.lock_time" = "TimeLock"; "send.unspent_outputs" = "UTxOs"; +"send.unspent_outputs.description" = "Choisissez manuellement les UTxO à dépenser les fonds du solde"; "send.unspent_outputs.send_to" = "Envoyer à"; "send.unspent_outputs.change" = "Changer"; @@ -513,6 +520,14 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "approve.confirmation.you_revoke" = "Vous révoquez"; "approve.confirmation.spender" = "Dépendant"; +"send.enter_amount" = "Entrez le montant"; +"send.enter_address" = "Entrer l'adresse"; +"send.invalid_address" = "Addresse invalide"; +"send.token_not_enabled" = "Token non activés"; +"send.token_syncing" = "Synchronisation des token"; +"send.token_not_synced" = "Token non synchronisé"; +"send.insufficient_balance" = "Solde insuffisant"; + // Donate "donate.list.title" = "Donnez avec"; @@ -549,7 +564,7 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "swap.trade_error.wrap_unwrap_not_allowed" = "Ce service n'autorise pas l'emballage/décompression. S'il vous plaît, essayez un autre service de swap. 1Inch recommandé"; "swap.button_error.insufficient_balance" = "Solde insuffisant"; "swap.switch_provider.title" = "Service de Swap"; -"swap.amount_type.coin" = "Coin"; +"swap.amount_type.coin" = "Pièce"; "swap.price" = "Prix"; "swap.buy_price" = "Prix d'achat"; @@ -642,8 +657,7 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "swap.confirmation.quote_failed" = "Échec du devis"; "swap.confirmation.slide_to_swap" = "Glisser pour changer"; -"swap.confirmation.swapping" = "Swapping"; -"swap.confirmation.swapped" = "Échangé"; +"swap.confirmation.swapping" = "Échange"; "swap.confirmation.refresh" = "Actualiser"; "swap.confirmation.impact_too_high" = "%@ a désactivé l'action d'échange pour cet échange parce que vous obtenez un prix extrêmement défavorable. Ceci est dû à une liquidité extrêmement faible.\nSi vous voulez toujours échanger, veuillez utiliser le site Web %@ à la place."; "swap.confirmation.impact_warning" = "Important ! Vous avez reçu un prix extrêmement défavorable en raison d'une liquidité extrêmement faible."; @@ -686,6 +700,51 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "market.defi_cap" = "Cap DeFi"; "market.defi_tvl" = "TVL en DeFi"; +"market.global.market_cap" = "Cap Totale"; +"market.global.volume" = "Vol. 24h"; +"market.global.btc_dominance" = "Dominance du BTC"; +"market.global.etf_inflow" = "Entrées d'ETF"; +"market.global.tvl_in_defi" = "TVL en DeFi"; + +"market.tab.news" = "Infos"; +"market.tab.coins" = "Pièces"; +"market.tab.watchlist" = "Liste de suivi"; +"market.tab.platforms" = "Platformes"; +"market.tab.pairs" = "Paires"; +"market.tab.sectors" = "Secteurs"; + +"market.sort_by.title" = "Classer par"; +"market.sort_by.manual" = "Manuel"; +"market.sort_by.highest_cap" = "Cap. marché la plus forte"; +"market.sort_by.lowest_cap" = "Cap. marché la plus basse"; +"market.sort_by.gainers" = "Gagnants"; +"market.sort_by.losers" = "Perdants"; +"market.sort_by.highest_volume" = "Volume le plus élevé"; +"market.sort_by.lowest_volume" = "Volume le plus bas"; + +"market.top_coins.title" = "Pièces"; +"market.top_coins" = "Top %@"; + +"market.time_period.title" = "Période"; +"market.time_period.1d" = "1 Jour"; +"market.time_period.1w" = "1 Semaine"; +"market.time_period.2w" = "2 Semaines"; +"market.time_period.1m" = "1 Mois"; +"market.time_period.3m" = "3 Mois"; +"market.time_period.6m" = "6 Mois"; +"market.time_period.1y" = "1 An"; +"market.time_period.2y" = "2 Ans"; +"market.time_period.5y" = "5 Ans"; +"market.time_period.1d.short" = "1J"; +"market.time_period.1w.short" = "1S"; +"market.time_period.2w.short" = "2S"; +"market.time_period.1m.short" = "1M"; +"market.time_period.3m.short" = "3 M"; +"market.time_period.6m.short" = "6 M"; +"market.time_period.1y.short" = "1A"; +"market.time_period.2y.short" = "2A"; +"market.time_period.5y.short" = "5A"; + "market.project_has_no_coin" = "Ce projet n'a pas de pièce de monnaie"; "market.top.section.header.see_all" = "Tout Afficher"; @@ -705,7 +764,7 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "market.tvl.platform_field.all" = "Tout"; -"market.sort_by" = "Trier par"; +"market.sort_by" = "Classer par"; "market.top.title" = "Top des coins"; "market.top.description" = "Top des coins par cap de marché"; @@ -722,6 +781,7 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "market.top.top_platforms" = "Meilleures plateformes"; "market.top.protocols" = "Protocoles"; +"market.pairs.volume" = "Volume"; "top_pairs.title" = "Principaux paires de marché"; "top_pairs.description" = "Principales paires de trading par volume sur chaque bourse"; @@ -730,6 +790,8 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "top_platform.title" = "Écosystème %@"; "top_platform.description" = "Capitalisation boursière de tous les protocoles de la chaîne %@"; +"top_platform.total_cap" = "Cap Totale"; + "market.search.recent" = "Récentes"; "market.search.popular" = "Populaire"; @@ -741,10 +803,22 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "market_discovery.not_found" = "Aucun résultat"; "market_watchlist.empty.caption" = "Votre liste de suivi est vide."; +"market.watchlist.signals" = "Signaux"; +"market.watchlist.empty" = "Votre liste de suivi est vide"; +"market.watchlist.signals.description" = "Les signaux ci-dessous sont basés sur les indicateurs techniques de prix Bollinger Bands et RSI sur environ les 30 derniers jours. Ces signaux sont algorithmiques et peuvent changer fréquemment."; +"market.watchlist.signals.strong_buy.description" = "Haute confiance dans l'augmentation des prix"; +"market.watchlist.signals.buy.description" = "Probable augmentation du prix dans un futur proche"; +"market.watchlist.signals.neutral.description" = "Aucune tendance claire, le marché est en équilibre"; +"market.watchlist.signals.sell.description" = "Probable diminution du prix dans un futur proche"; +"market.watchlist.signals.strong_sell.description" = "Probabilité élevée de diminution du prix"; +"market.watchlist.signals.risky.description" = "Niveau de risque élevé, nécessite de la prudence"; +"market.watchlist.signals.warning" = "N'oubliez pas d'appliquer une gestion des risques, et notez que ceci n'est pas un conseil financier."; +"market.watchlist.signals.turn_on" = "Allumer"; "market.advanced_search.title" = "Filtres"; "market.advanced_search.show_results" = "Afficher les résultats"; "market.advanced_search.empty_results" = "Aucun résultat"; +"market.advanced_search.retry" = "Réessayer"; "market.advanced_search.dex_description" = "Ce paramètre s'applique aux tokens échangés sur Ethereum (Uniswap DEX) et Binance Smart Chain (Pancake DEX)."; "market.advanced_search.24h" = "24h"; @@ -764,17 +838,10 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "market.advanced_search.liquidity" = "Liquidité DEX"; "market.advanced_search.blockchains" = "Blockchains"; -"market.advanced_search.technical_advice" = "Signaux de trading"; +"market.advanced_search.signal" = "Signal de trading"; "market.advanced_search.price_period" = "Période de prix"; "market.advanced_search.price_change" = "Variation de prix"; -"market.advanced_search.technical_advice.risk_trade" = "Risque de négociation"; -"market.advanced_search.technical_advice.strong_buy" = "Fort Achat"; -"market.advanced_search.technical_advice.buy" = "Acheter"; -"market.advanced_search.technical_advice.neutral" = "Neutre"; -"market.advanced_search.technical_advice.sell" = "Vendre"; -"market.advanced_search.technical_advice.strong_sell" = "Vente Forte"; - "market.advanced_search.outperformed_btc" = "A surpassé BTC"; "market.advanced_search.outperformed_eth" = "A surpassé ETH"; "market.advanced_search.outperformed_bnb" = "A surpassé BNB"; @@ -830,10 +897,36 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "market.global.defi_cap.title" = "Cap DeFi"; "market.global.defi_cap.description" = "Valeur de marché totale des projets DeFi"; -"market.global.tvl_in_defi.title" = "TVL en DeFi"; -"market.global.tvl_in_defi.description" = "Valeur totale verrouillée (TVL) dans DeFi"; -"market.global.tvl_in_defi.multi_chain" = "Multi-chaîne"; -"market.global.tvl_in_defi.filter_by_chain" = "Filtrer par chaîne"; +"market.etf.title" = "Flux net total"; +"market.etf.description" = "Le flux net entrant d'un ETF est égal à ses entrées de trésorerie moins ses sorties."; +"market.etf.total_net_assets" = "Total des actifs nets"; +"market.etf.sort_by.highest_assets" = "Actifs les plus élevés"; +"market.etf.sort_by.lowest_assets" = "Les actifs les moins élevés"; +"market.etf.sort_by.inflow" = "Flux entrant"; +"market.etf.sort_by.outflow" = "Flux sortant"; +"market.etf.period.all" = "Tout"; + +"market.market_cap.title" = "Capitalisation de marché totale"; +"market.market_cap.description" = "Valeur de marché totale de toutes les cryptomonnaies"; +"market.market_cap.market_cap" = "Cap. marché"; + +"market.tvl_in_defi.title" = "TVL en DeFi"; +"market.tvl_in_defi.description" = "La valeur totale verrouillée (TVL) dans la finance décentralisée"; +"market.tvl_in_defi.tvl" = "TVL"; +"market.tvl_in_defi.multi_chain" = "Multi-chaîne"; +"market.tvl_in_defi.filter_by_chain" = "Filtrer par chaîne"; +"market.tvl_in_defi.filter.all" = "Tout"; + +"market.volume.title" = "Volume"; +"market.volume.description" = "Le volume de trading de 24h du marché des cryptomonnaies"; +"market.volume.volume" = "Volume"; + +"market.signal.risky" = "Risqué"; +"market.signal.strong_buy" = "Fort Achat"; +"market.signal.buy" = "Acheter"; +"market.signal.neutral" = "Neutre"; +"market.signal.sell" = "Vendre"; +"market.signal.strong_sell" = "Vente Forte"; // Coin Page @@ -854,7 +947,8 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "coin_overview.genesis_date" = "Date de création"; "coin_overview.trading_volume" = "Volume de trading"; -"coin_overview.roi.hour24" = "1 Jour"; +"coin_overview.roi.hour24" = "24 Heures"; +"coin_overview.roi.day1" = "1 Jour"; "coin_overview.roi.day7" = "1 Semaine"; "coin_overview.roi.day14" = "2 Semaines"; "coin_overview.roi.day30" = "1 Mois"; @@ -878,36 +972,36 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "technical_advice.over.bought" = "suracheté"; "technical_advice.over.sold" = "sursoldé"; "technical_advice.down" = "En baisse"; -"technical_advice.up" = "up"; +"technical_advice.up" = "En haut"; -"technical_advice.over.main" = "The actions with the asset are risky."; -"technical_advice.over.indicators.signal_date" = "Starting from the %@"; -"technical_advice.over.indicators" = "The asset is outside the Bollinger Band channel and %@."; -"technical_advice.over.rsi" = " RSI = %@, This also indicates that the asset is %@."; -"technical_advice.over.advice" = " There might be a strong %@ward movement, so it's better to wait for the asset price to return to the channel."; +"technical_advice.over.main" = "Les actions liées à l'actif comportent des risques."; +"technical_advice.over.indicators.signal_date" = "À partir du %@"; +"technical_advice.over.indicators" = "L'actif est en dehors du canal de bandes de Bollinger et %@."; +"technical_advice.over.rsi" = " RSI = %@, Cela indique également que l'actif est %@."; +"technical_advice.over.advice" = " Il pourrait y avoir un mouvement %@ fort, il est donc préférable d'attendre que le prix de l'actif revienne au canal."; -"technical_advice.strong.indicators" = "The asset was %@, but now it has returned to the Bollinger Band channel. This indicates a possible trend reversal."; -"technical_advice.strong.rsi" = " Meanwhile, the RSI is %@, which still indicates that it is %@."; -"technical_advice.strong.advice" = " This could be a very strong signal to enter the market. Keep in mind that there may be several attempts of %@ward movement after returning to the channel, so do not forget about risk management."; +"technical_advice.strong.indicators" = "L'actif était %@, mais maintenant il est revenu au canal de bandes de Bollinger. Cela indique une possible inversion de tendance."; +"technical_advice.strong.rsi" = " Pendant ce temps, le RSI est %@, ce qui indique toujours qu'il est %@."; +"technical_advice.strong.advice" = " Cela pourrait être un signal très fort pour entrer sur le marché. Gardez à l'esprit qu'il peut y avoir plusieurs tentatives de mouvement %@ après le retour au canal, donc n'oubliez pas la gestion des risques."; -"technical_advice.stable.rsi" = " Meanwhile, the RSI is %@, which also indicates a trend reversal (RSI crossed the boundary at 70%)."; -"technical_advice.stable.advice" = "The price is returning to neutral levels, however, there is still potential for upward movement. Keep in mind that RSI = 50 and the middle of the Bollinger Bands are strong resistances and possible trend reversal points. Do not forget about risk management."; +"technical_advice.stable.rsi" = " Pendant ce temps, le RSI est %@, ce qui indique également un renversement de tendance (le RSI a franchi la limite à 70 %)."; +"technical_advice.stable.advice" = "Le prix revient à des niveaux neutres, cependant, il existe encore un potentiel de mouvement à la hausse. Gardez à l'esprit que le RSI = 50 et le milieu des bandes de Bollinger sont des résistances fortes et des points possibles de renversement de tendance. N'oubliez pas la gestion des risques."; -"technical_advice.neutral.rsi" = "RSI = %@ also confirms the absence of a strong trend."; -"technical_advice.neutral.indicators" = "The asset was in the overbought/oversold zone, but at the moment the price has returned to the Bollinger Band channel in the neutral zone. The RSI is %@ also confirms the absence of a strong trend, so overall the asset price is moving towards averaging and further movement is possible in any direction."; -"technical_advice.neutral.advice" = " In general, the asset price is moving towards averaging and further movement is possible in any direction."; +"technical_advice.neutral.rsi" = "Le RSI = %@ confirme également l'absence d'une forte tendance."; +"technical_advice.neutral.indicators" = "L'actif se trouvait dans la zone de surachat/survente, mais pour le moment, le prix est revenu dans le canal de Bandes de Bollinger dans la zone neutre. Le RSI est %@, ce qui confirme également l'absence d'une forte tendance, donc dans l'ensemble, le prix de l'actif se rapproche de la moyenne et un mouvement ultérieur est possible dans n'importe quelle direction."; +"technical_advice.neutral.advice" = " En général, le prix de l'actif se rapproche de la moyenne et un mouvement ultérieur est possible dans n'importe quelle direction."; -"technical_advice.other.title" = "Please note:"; +"technical_advice.other.title" = "Veuillez noter:"; -"technical_advice.ema.above" = "above"; -"technical_advice.ema.below" = "below"; -"technical_advice.ema.growth" = "growth"; -"technical_advice.ema.decrease" = "decrease"; -"technical_advice.ema.advice" = "EMA 200. Determines the overall sentiment and trend. The daily price of the asset is located %@ the EMA (%@). This means that globally the asset is set for %@."; +"technical_advice.ema.above" = "au-dessus"; +"technical_advice.ema.below" = "en dessous"; +"technical_advice.ema.growth" = "croissance"; +"technical_advice.ema.decrease" = "diminution"; +"technical_advice.ema.advice" = "EMA 200. Détermine le sentiment et la tendance généraux. Le prix quotidien de l'actif est situé %@ le EMA (%@). Cela signifie que globalement, l'actif est configuré pour %@."; -"technical_advice.macd.positive" = "above"; -"technical_advice.macd.negative" = "below"; -"technical_advice.macd.advice" = "MACD. Assesses the strength of the trend considering the average price change. The daily value of the histogram is %@ (%@). The price of the asset globally may move %@."; +"technical_advice.macd.positive" = "au-dessus"; +"technical_advice.macd.negative" = "en dessous"; +"technical_advice.macd.advice" = "MACD. Évalue la force de la tendance en tenant compte de la variation moyenne des prix. La valeur quotidienne de l'histogramme est %@ (%@). Le prix de l'actif peut globalement se déplacer %@."; "coin_analytics.indicators.title" = "Indicateurs techniques"; "coin_analytics.indicators.disclaimer" = "N'oubliez pas d'appliquer une gestion des risques, et notez que ceci n'est pas un conseil financier."; @@ -917,13 +1011,13 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "coin_analytics.indicators.show_details" = "Afficher les détails"; "coin_analytics.indicators.summary" = "Résumé"; "coin_analytics.indicators.no_data" = "Pas de données"; -"coin_analytics.indicators.oversold" = "Très risqué à négocier"; -"coin_analytics.indicators.strong_buy" = "Achat fort"; +"coin_analytics.indicators.oversold" = "Risqué"; +"coin_analytics.indicators.strong_buy" = "Fort Achat"; "coin_analytics.indicators.buy" = "Acheter"; "coin_analytics.indicators.neutral" = "Neutre"; "coin_analytics.indicators.sell" = "Vendre"; -"coin_analytics.indicators.strong_sell" = "Vente forte"; -"coin_analytics.indicators.overbought" = "Très risqué à négocier"; +"coin_analytics.indicators.strong_sell" = "Vente Forte"; +"coin_analytics.indicators.overbought" = "Risqué"; "coin_analytics.period" = "Période"; "coin_analytics.period.select_title" = "Sélectionnez la période"; "coin_analytics.period.1h" = "1 heure"; @@ -944,6 +1038,7 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "coin_analytics.cex_volume" = "Volume CEX"; "coin_analytics.cex_volume_rank" = "Rang de volume CEX"; "coin_analytics.cex_volume_rank.description" = "Les tokens classés en fonction du volume de trading du token sur les exchanges centralisés."; +"coin_analytics.cex_volume_rank.sorting_field" = "Volume"; "coin_analytics.cex_volume.info1" = "Volume total des échanges pour le jeton sur les principales plateformes d'échange centralisées au cours d'une période de 30 jours."; "coin_analytics.cex_volume.info2" = "Graphique montrant la variation du volume quotidien de trading du jeton sur les principales plateformes d'échange centralisées sur une période d'un an."; "coin_analytics.cex_volume.info3" = "Token's rank based on trading volume on leading centralized exchanges over 30-day period."; @@ -952,6 +1047,7 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "coin_analytics.dex_volume" = "Volume DEX"; "coin_analytics.dex_volume_rank" = "Rang de volume DEX"; "coin_analytics.dex_volume_rank.description" = "Les tokens classés en fonction du volume de trading du token sur les exchanges décentralisés."; +"coin_analytics.dex_volume_rank.sorting_field" = "Volume"; "coin_analytics.dex_volume.info1" = "Total trading volume for the token on leading centralized exchanges over a 30-day period."; "coin_analytics.dex_volume.info2" = "Votre demande a été traduite en français comme demandé. Si vous avez besoin d'autres traductions ou d'informations supplémentaires, n'hésitez pas à demander."; "coin_analytics.dex_volume.info3" = "Classement du jeton en fonction du volume de négociation sur les principales bourses décentralisées au cours d'une période de 30 jours."; @@ -963,6 +1059,7 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "coin_analytics.dex_liquidity" = "Liquidité DEX"; "coin_analytics.dex_liquidity_rank" = "Classement de liquidité DEX"; "coin_analytics.dex_liquidity_rank.description" = "Les tokens classés par liquidité disponible sur les exchanges décentralisés."; +"coin_analytics.dex_liquidity_rank.sorting_field" = "Liquidité"; "coin_analytics.dex_liquidity.info1" = "Total de liquidités actuellement disponibles pour le jeton sur les principales bourses décentralisées."; "coin_analytics.dex_liquidity.info2" = "Graphique montrant une variation de la liquidité disponible pour le jeton sur les principales bourses décentralisées sur une période de 1 an."; "coin_analytics.dex_liquidity.info3" = "Liste de tous les jetons classés en fonction de la liquidité disponible pour le jeton sur les principaux échanges décentralisés."; @@ -974,6 +1071,7 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "coin_analytics.active_addresses.30_day_unique_addresses" = "Adresses uniques de 30 jours"; "coin_analytics.active_addresses_rank" = "Classement des adresses actives"; "coin_analytics.active_addresses_rank.description" = "Tokens classés par nombre d'adresses uniques effectuant des transactions avec le token."; +"coin_analytics.active_addresses_rank.sorting_field" = "Actif"; "coin_analytics.active_addresses.info1" = "Total number of unique daily active addresses over 24-hour period."; "coin_analytics.active_addresses.info2" = "Total number of unique daily active addresses over 24-hour period."; "coin_analytics.active_addresses.info3" = "Nombre total d'adresses blockchain uniques transitant avec un jeton sur une période de 30 jours."; @@ -983,6 +1081,7 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "coin_analytics.transaction_count" = "Nombre de transactions"; "coin_analytics.transaction_count_rank" = "Classement par nombre de tr."; "coin_analytics.transaction_count_rank.description" = "Tokens classés en fonction du nombre de transactions sur une blockchain."; +"coin_analytics.transaction_count_rank.sorting_field" = "Compter"; "coin_analytics.transaction_count.info1" = "Nombre total de transactions blockchain uniques sur une période de 30 jours."; "coin_analytics.transaction_count.info2" = "Graphique montrant la variation du nombre de transactions sur une période de 1 an."; "coin_analytics.transaction_count.info3" = "Rang du jeton basé sur le nombre de transactions avec le jeton de période de 30 jours."; @@ -992,6 +1091,7 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "coin_analytics.holders" = "Porteurs"; "coin_analytics.holders_rank" = "Classement des porteurs"; "coin_analytics.holders_rank.description" = "Classement des jetons selon les adresses uniques qui les détiennent sur plusieurs chaînes de blocs."; +"coin_analytics.holders_rank.sorting_field" = "Porteurs"; "coin_analytics.holders.info1" = "Nombre total d'adresses uniques détenant le jeton sur différentes blockchains."; "coin_analytics.holders.info2" = "Top 10 des portefeuilles détenant le jeton sur chaque blockchain."; "coin_analytics.holders.tracked_blockchains" = "Blockchains suivies : Ethereum, Binance Smart Chain, Optimism, Arbitrum, Celo, Cronos, Avalanche, Fantom, Polygon"; @@ -1011,10 +1111,12 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "coin_analytics.project_fee" = "Frais de projet"; "coin_analytics.project_fee_rank" = "Rang des frais du projet"; "coin_analytics.project_fee_rank.description" = "Les jetons sont classés en fonction des frais générés par les projets respectifs. La manière dont les frais sont perçus varie selon les projets."; +"coin_analytics.project_fee_rank.sorting_field" = "Volume"; "coin_analytics.project_revenue" = "Revenus du projet"; "coin_analytics.project_revenue_rank" = "Classement en fonction des revenus du projet"; "coin_analytics.project_revenue_rank.description" = "Tokens classés en fonction des revenus générés pour les détenteurs via des mécanismes i.e. staking ou token burns."; +"coin_analytics.project_revenue_rank.sorting_field" = "Revenu"; "coin_analytics.other_data" = "Autres données"; @@ -1200,6 +1302,8 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "settings.rate_us" = "Evaluez-nous"; "settings.tell_friends" = "Partager avec ami"; "settings.contact_us" = "Contactez-nous"; +"settings.social_networks.label" = "Soyez Unstoppable"; +"settings.social_networks.footer" = "Apprenez et maîtrisez les crypto-monnaies grâce à des vidéos exclusives. Faites connaissance avec nous de manière informelle. Soyez les premiers à voir les projets sur lesquels nous travaillons."; // Settings -> Base Currency @@ -1335,7 +1439,7 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "enable_duress_mode.intro.title" = "Mode de Duress"; "enable_duress_mode.intro.description" = "Ce mode permet à l'utilisateur de configurer plusieurs codes d'accès d'application de déverrouillage où un code d'accès désiré n'affiche que les portefeuilles spécifiés. Conçu pour garder les portefeuilles sélectionnés en sécurité sous la contrainte ou les menaces."; -"enable_duress_mode.intro.notes" = "Notes"; +"enable_duress_mode.intro.notes" = "Remarques"; "enable_duress_mode.intro.biometrics.description" = "La fonction %@ fonctionnera pour déverrouiller le mode Duresse. Vous pouvez désactiver %@ pour plus de commodité."; "enable_duress_mode.intro.passcode_disabling" = "Désactivation du code d'accès"; "enable_duress_mode.intro.passcode_disabling.description" = "La désactivation du mot de passe en mode principal réinitialisera automatiquement le Mode Duresse."; @@ -1417,9 +1521,7 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; // Settings -> About App "settings.about_app.title" = "À propos de l'application"; -"settings.about_app.app_name" = "%@ Portefeuille"; -"settings.about_app.description" = "Le portefeuille %@ est conçu pour ceux qui cherchent à investir et à stocker des cryptomonnaies de manière privée et indépendante.\n\nC'est un portefeuille non-dépositaire, peer-to-peer où seul l'utilisateur a le contrôle sur les fonds. Il ne collecte aucune donnée et garde l'utilisateur indépendant en ne verrouillant pas les fonds de l'utilisateur sur une application spécifique de portefeuille.\n\nLe portefeuille %@ est entièrement open-source et tout le monde peut confirmer que l'application fonctionne exactement comme il le prétend."; -"settings.about_app.whats_new" = "Nouveautés"; +"settings.about_app.app_version" = "Version de l'application"; "settings.about_app.website" = "Site Web"; // Settings -> About App -> Contact @@ -1431,11 +1533,11 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; // Settings -> Privacy "settings.privacy" = "Confidentialité"; -"settings.privacy.description" = "%@ ne collecte aucune donnée et n'utilise aucun outil d'analyse susceptible d'exposer des données sur ses utilisateurs. Le portefeuille est conçu pour assurer un haut niveau de confidentialité à ses utilisateurs."; -"settings.privacy.statement.user_data_storage" = "Les données de l'utilisateur restent toujours sur l'appareil de l'utilisateur."; +"settings.privacy.description" = "%@ ne collecte pas de données personnelles exposant vos informations privées, telles que les soldes de coins ou les adresses. Bien que nous recueillions certaines statistiques d'utilisation de l'interface utilisateur, cela sert uniquement à comprendre notre base d'utilisateurs et les tendances d'utilisation de l'application. Cela peut être désactivé si vous le souhaitez."; "settings.privacy.statement.data_usage" = "Le portefeuille ne collecte aucune donnée sur les utilisateurs."; -"settings.privacy.statement.data_privacy" = "Le portefeuille ne partage aucune donnée sur les utilisateurs."; -"settings.privacy.statement.user_account" = "Aucun compte utilisateur ni aucune base de données ne conserve les données des utilisateurs à un autre endroit."; +"settings.privacy.statement.data_storage" = "Il n'y a pas de comptes utilisateurs ni de bases de données stockant les données des utilisateurs."; +"settings.privacy.statement.user_account" = "Si autorisé, le portefeuille partagera les habitudes d'utilisation de l'application avec l'équipe d'Unstoppable. Cela permet de comprendre quelles fonctionnalités sont utilisées (ou non) par nos utilisateurs. Étant une application axée sur la confidentialité, nous avons besoin d'une manière d'évaluer nos efforts, et sans cela, nous n'avons aucune idée si les fonctionnalités que nous avons développées sont utilisées ou non."; +"settings.privacy.allow" = "Partager les données UI"; // Settings -> Appearance @@ -1446,21 +1548,25 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "appearance.theme.dark" = "Sombre"; "appearance.theme.light" = "Classique"; -"appearance.tab_settings" = "Paramètres des onglets"; "appearance.markets_tab" = "Onglet Marchés"; +"appearance.hide_markets" = "Masquer les marchés"; +"appearance.price_change" = "Variation de prix"; +"appearance.price_change.24h" = "24H"; +"appearance.price_change.1d" = "Minuit UTC"; + "appearance.launch_screen" = "Écran de démarrage"; "appearance.launch_screen.auto" = "Auto"; "appearance.launch_screen.balance" = "Solde"; "appearance.launch_screen.market_overview" = "Vue d'ensemble du marché"; "appearance.launch_screen.watchlist" = "Liste de suivi"; -"appearance.app_icon" = "Icône de l’app"; - -"appearance.balance_conversion" = "Conversion du solde"; - +"appearance.balance_tab" = "Balance Tab"; +"appearance.hide_buttons" = "Masquer les boutons"; "appearance.balance_value" = "Valeur du solde"; -"appearance.balance_value.coin_value" = "Valeur de la pièce"; -"appearance.balance_value.fiat_value" = "Valeur Fiat"; +"appearance.balance_value.coin_fiat" = "Pièce / Fiat"; +"appearance.balance_value.fiat_coin" = "Fiat / Pièce"; + +"appearance.app_icon" = "Icône de l’app"; // Settings -> Contacts @@ -1528,8 +1634,8 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "chart.time_duration.week2" = "2S"; "chart.time_duration.month" = "1M"; "chart.time_duration.month3" = "3 M"; -"chart.time_duration.halfyear" = "6 m."; -"chart.time_duration.year" = "1J"; +"chart.time_duration.halfyear" = "6 M"; +"chart.time_duration.year" = "1A"; "chart.time_duration.year2" = "2A"; "chart.time_duration.year5" = "5A"; "chart.time_duration.all" = "TOUS"; @@ -1959,7 +2065,6 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "tron.send.fee.info" = "Le coût estimé pour envoyer une transaction donnée sur le réseau (sans exclure les coûts d'énergie, de bande passante et d'activation)"; "tron.send.resources_consumed.info" = "Bandwidth passante est l'unité qui mesure la taille des octets de transaction stockés dans la base de données blockchain. Plus la transaction est grande, plus la bande passante sera consommée.\n\nL'énergie est l'unité qui mesure le montant de calcul requis par la machine virtuelle TRON pour effectuer des opérations spécifiques sur le réseau TRON.\n\nÉtant donné que les transactions de contrats intelligents nécessitent des ressources informatiques pour être exécutées, chaque transaction de contrat intelligent nécessite le paiement de frais d'énergie."; "tron.send.activation_fee.info" = "Le transfert de jetons TRX ou TRC-10 vers une adresse de compte inactive activera ledit compte."; -"tron.send.inactive_address" = "Cette adresse n'est pas active"; // Cex Coin Select @@ -2025,7 +2130,7 @@ Allez dans Paramètres - > %@ et autorisez l'accès à la caméra."; "transaction_filter.blockchain" = "Blockchain"; "transaction_filter.all_blockchains" = "Toutes les blockchains"; -"transaction_filter.coin" = "Coin"; +"transaction_filter.coin" = "Pièce"; "transaction_filter.all_coins" = "Toutes les pièces"; "transaction_filter.contact" = "Contacter"; "transaction_filter.all_contacts" = "Tous les contacts"; diff --git a/UnstoppableWallet/UnstoppableWallet/ko.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/ko.lproj/Localizable.strings index fe7f55f968..d4f0f10269 100644 --- a/UnstoppableWallet/UnstoppableWallet/ko.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/ko.lproj/Localizable.strings @@ -226,6 +226,7 @@ "extended_key.purpose" = "목적"; "extended_key.blockchain" = "Blockchain"; "extended_key.account" = "계정"; +"extended_key.account.description" = "이 설정은 고급 사용자용입니다. 지갑을 가져오려면 (확장 개인 키를 통해) 또는 거래 목록을 가져오려면 (확장 공개 키를 통해) 계정 0이 필요합니다."; "extended_key.tap_to_show" = "탭하여 extended private key 표시"; // Backup @@ -316,7 +317,6 @@ "balance.downloading_blocks" = "블록 다운로드"; "balance.scanning_blocks" = "스캐닝 블록"; "balance.enhancing_transactions" = "거래 향상"; -"wait_for_synchronization" = "동기화 대기 중"; "balance.searching.count" = "%@ tx"; "balance.syncing_percent" = "동기화 중... %@"; @@ -349,6 +349,8 @@ "balance.token.frozen" = "잠김"; "balance.token.frozen.info.title" = "겨울왕국 제목"; "balance.token.frozen.info.description" = "고정된 설명 텍스트"; +"balance.token.account.inactive.title" = "계정이 활성화되지 않음"; +"balance.token.account.inactive.description" = "새로운 TRON 지갑은 활성화되기 위해 최소 1 TRX의 입금이 필요합니다. 비활성 지갑은 토큰을 보유하고 받을 수는 있지만, 활성화될 때까지 잔액이 정확히 표시되지 않습니다."; // Account switcher @@ -443,6 +445,8 @@ "send.transaction_inputs_outputs_info.shuffle.description" = "트랜잭션 출력의 순서는 모든 트랜잭션에서 무작위로 지정됩니다. 때로는 변화가 첫 번째 출력이 될 수도 있고 때로는 두 번째 출력이 될 수도 있습니다. 사용자가 앱 개발자를 신뢰하는 경우 권장 옵션으로 간주합니다."; "send.transaction_inputs_outputs_info.deterministic.title" = "2. Deterministic"; "send.transaction_inputs_outputs_info.deterministic.description" = "거래 출력을 정렬하는 데에는 일반적으로 합의된 표준이 있습니다(이것은 BIP69로 알려져 있습니다). 오픈 소스 지갑에서는 이 표준이 지갑 사용자가 앱 개발자가 출력 순서를 어떻게 구현하는지 신뢰할 필요가 없도록 보장합니다. 이 표준은 새로운 것이기 때문에 아직 많은 지갑이 이를 구현하지 않았습니다. 그 결과, 블록체인에서 특정 거래가 이 표준을 사용하는 지갑에서 보내졌는지 여부를 어느 정도 파악할 수 있습니다."; +"send.select_all" = "전체 선택"; +"send.unselect_all" = "전체 선택 해제"; "send.confirmation.title" = "확인"; "send.confirmation.you_send" = "보냅니다"; @@ -460,18 +464,20 @@ "send.confirmation.time_lock" = "TimeLock"; "send.confirmation.replace_by_fee" = "수수료로 대체"; "send.confirmation.replaced_transactions" = "대체된 거래"; - +"send.confirmation.input" = "입력"; + +"send.confirmation.sync_failed" = "동기화 실패"; +"send.confirmation.invalid_data" = "유효하지 않은 데이터"; +"send.confirmation.refresh" = "새로고침"; +"send.confirmation.please_wait" = "기다려 주십시오"; +"send.confirmation.expires_in" = "%@ 후 만료됩니다"; +"send.confirmation.expired" = "만료"; "send.confirmation.slide_to_send" = "슬라이드하여 보내기"; "send.confirmation.sending" = "전송중"; "send.confirmation.sent" = "전송됨"; "send.confirmation.slide_to_approve" = "승인하기 위해 슬라이드하십시오"; -"send.confirmation.approving" = "승인 중"; -"send.confirmation.approved" = "승인됨"; - "send.confirmation.slide_to_revoke" = "취소하기 위해 슬라이드하십시오"; -"send.confirmation.revoking" = "철회"; -"send.confirmation.revoked" = "철회됨"; "send.confirmation.slide_to_resend" = "재전송"; "send.confirmation.slide_to_cancel" = "거래 취소"; @@ -504,6 +510,7 @@ "send.lock_time" = "시간 자물쇠"; "send.unspent_outputs" = "UTxOs"; +"send.unspent_outputs.description" = "잔액에서 자금을 사용하기 위해 수동으로 UTxO를 선택하세요."; "send.unspent_outputs.send_to" = "보내기"; "send.unspent_outputs.change" = "변경"; @@ -511,6 +518,14 @@ "approve.confirmation.you_revoke" = "당신은 취소"; "approve.confirmation.spender" = "스펜더"; +"send.enter_amount" = "금액 입력"; +"send.enter_address" = "주소 입력"; +"send.invalid_address" = "잘못된 주소"; +"send.token_not_enabled" = "토큰 활성화되지 않음"; +"send.token_syncing" = "토큰 동기화 중"; +"send.token_not_synced" = "토큰 동기화되지 않음"; +"send.insufficient_balance" = "잔액 부족"; + // Donate "donate.list.title" = "기부하기"; @@ -593,7 +608,7 @@ "swap.price_impact.description" = "일반적으로 보여진 가격으로부터 예상되는 가격 편차는 스왑 금액에 따라 증가합니다."; "swap.high_price_impact" = "높은 가격 영향"; "swap.recipient" = "수령인"; -"swap.slippage" = "Slippage"; +"swap.slippage" = "미끄러짐"; "swap.affiliate_fee" = "제휴 수수료"; "swap.liquidity_fee" = "유동성 수수료"; @@ -641,7 +656,6 @@ "swap.confirmation.slide_to_swap" = "밀어서 결제하기"; "swap.confirmation.swapping" = "스와핑"; -"swap.confirmation.swapped" = "스왑됨"; "swap.confirmation.refresh" = "새로고침"; "swap.confirmation.impact_too_high" = "%@은 현재 유동성이 매우 낮아 매우 불리한 가격으로 거래되고 있으므로, 이 거래에 대한 스왑 조치를 비활성화했습니다.\n교환을 원하시면 %@ 웹사이트를 사용하십시오."; "swap.confirmation.impact_warning" = "중요한 사항입니다! 매우 불리한 가격을 받고 있습니다. 이는 유동성이 극도로 낮기 때문입니다."; @@ -684,12 +698,57 @@ "market.defi_cap" = "디파이(DeFi) 시가총액"; "market.defi_tvl" = "디파이(DeFi)에서의 총 예치 가치"; +"market.global.market_cap" = "시가 총액"; +"market.global.volume" = "24h 거래량"; +"market.global.btc_dominance" = "BTC 지배력"; +"market.global.etf_inflow" = "ETF 유입액"; +"market.global.tvl_in_defi" = "디파이(DeFi)에서의 총 예치 가치"; + +"market.tab.news" = "뉴스"; +"market.tab.coins" = "코인"; +"market.tab.watchlist" = "관심 목록"; +"market.tab.platforms" = "플랫폼"; +"market.tab.pairs" = "쌍"; +"market.tab.sectors" = "부문"; + +"market.sort_by.title" = "정렬 기준"; +"market.sort_by.manual" = "수동"; +"market.sort_by.highest_cap" = "가장 높은 시가총액"; +"market.sort_by.lowest_cap" = "가장 낮은 시가총액"; +"market.sort_by.gainers" = "이득자"; +"market.sort_by.losers" = "해자"; +"market.sort_by.highest_volume" = "가장 높은 거래량"; +"market.sort_by.lowest_volume" = "가장 낮은 거래량"; + +"market.top_coins.title" = "코인"; +"market.top_coins" = "상위 %@"; + +"market.time_period.title" = "기간"; +"market.time_period.1d" = "1 일"; +"market.time_period.1w" = "1일주"; +"market.time_period.2w" = "2이주"; +"market.time_period.1m" = "1개월"; +"market.time_period.3m" = "3 개월"; +"market.time_period.6m" = "6 개월"; +"market.time_period.1y" = "일년"; +"market.time_period.2y" = "2 년"; +"market.time_period.5y" = "5 년"; +"market.time_period.1d.short" = "1하루"; +"market.time_period.1w.short" = "1주일"; +"market.time_period.2w.short" = "2주일"; +"market.time_period.1m.short" = "1개월"; +"market.time_period.3m.short" = "3개월"; +"market.time_period.6m.short" = "6개월"; +"market.time_period.1y.short" = "1년"; +"market.time_period.2y.short" = "2년"; +"market.time_period.5y.short" = "5년"; + "market.project_has_no_coin" = "이 프로젝트는 코인이 없습니다"; "market.top.section.header.see_all" = "모두 보기"; "market.top.section.header.top_gainers" = "최고 승자"; "market.top.section.header.top_losers" = "최고 패자"; -"market.top.section.header.sectors" = "분야"; +"market.top.section.header.sectors" = "부문"; "market.top.section.header.news" = "뉴스"; "market.top.volume.title" = "거래량"; "market.top.market_cap.title" = "시가총액"; @@ -720,6 +779,7 @@ "market.top.top_platforms" = "주요 플랫폼"; "market.top.protocols" = "프로토콜"; +"market.pairs.volume" = "거래량"; "top_pairs.title" = "상위 시장 페어"; "top_pairs.description" = "각 거래소에서 거래량 상위 페어"; @@ -728,6 +788,8 @@ "top_platform.title" = "%@ 생태계"; "top_platform.description" = "%@ 체인상의 모든 프로토콜의 시가총액"; +"top_platform.total_cap" = "시가 총액"; + "market.search.recent" = "최근의"; "market.search.popular" = "인기있는"; @@ -739,10 +801,22 @@ "market_discovery.not_found" = "검색 결과가 없습니다"; "market_watchlist.empty.caption" = "관심 목록이 비어 있습니다."; +"market.watchlist.signals" = "신호"; +"market.watchlist.empty" = "관심 목록이 비어 있습니다"; +"market.watchlist.signals.description" = "아래 신호는 대략 최근 30일 동안의 볼린저 밴드와 RSI 기술적 가격 지표를 기반으로 합니다. 이러한 신호는 알고리즘에 기반하며 자주 변경될 수 있습니다."; +"market.watchlist.signals.strong_buy.description" = "가격 상승에 대한 높은 신뢰도"; +"market.watchlist.signals.buy.description" = "가까운 미래에 가능성 있는 가격 상승"; +"market.watchlist.signals.neutral.description" = "명확한 트렌드가 없으며, 시장은 균형 상태에 있습니다"; +"market.watchlist.signals.sell.description" = "가까운 미래에 가능성 있는 가격 하락"; +"market.watchlist.signals.strong_sell.description" = "가격 하락의 높은 확률"; +"market.watchlist.signals.risky.description" = "고위험 수준, 주의가 필요합니다"; +"market.watchlist.signals.warning" = "언제나 위험 관리를 적용하는 것을 기억하세요. 이것은 금융 조언이 아님을 유의하세요."; +"market.watchlist.signals.turn_on" = "켜기"; "market.advanced_search.title" = "필터"; "market.advanced_search.show_results" = "결과 보기"; "market.advanced_search.empty_results" = "결과 없음"; +"market.advanced_search.retry" = "재시도"; "market.advanced_search.dex_description" = "이 설정은 이더리움(Uniswap DEX) 과 바이낸스 스마트체인(Pancake DEX) 에서 거래되는 토큰에 적용됩니다."; "market.advanced_search.24h" = "24시간"; @@ -762,17 +836,10 @@ "market.advanced_search.liquidity" = "DEX 유동성"; "market.advanced_search.blockchains" = "블록체인"; -"market.advanced_search.technical_advice" = "전반적인 점수가 좋거나 우수합니다"; +"market.advanced_search.signal" = "거래 신호"; "market.advanced_search.price_period" = "가격 기간"; "market.advanced_search.price_change" = "가격 변경"; -"market.advanced_search.technical_advice.risk_trade" = "거래 위험"; -"market.advanced_search.technical_advice.strong_buy" = "강력한 매수"; -"market.advanced_search.technical_advice.buy" = "매수"; -"market.advanced_search.technical_advice.neutral" = "중립적"; -"market.advanced_search.technical_advice.sell" = "매도"; -"market.advanced_search.technical_advice.strong_sell" = "강력한 매도"; - "market.advanced_search.outperformed_btc" = "BTC를 능가했습니다"; "market.advanced_search.outperformed_eth" = "ETH를 능가했습니다"; "market.advanced_search.outperformed_bnb" = "BNB를 능가했습니다"; @@ -807,9 +874,9 @@ "market.advanced_search.more_500_b" = "> 5000억"; "market.advanced_search.day" = "1 일"; -"market.advanced_search.week" = "1 일주"; -"market.advanced_search.week2" = "이주"; -"market.advanced_search.month" = "1 개월"; +"market.advanced_search.week" = "1일주"; +"market.advanced_search.week2" = "2이주"; +"market.advanced_search.month" = "1개월"; "market.advanced_search.month6" = "6 개월"; "market.advanced_search.year" = "일년"; @@ -828,10 +895,36 @@ "market.global.defi_cap.title" = "디파이(DeFi) 시가총액"; "market.global.defi_cap.description" = "디파이 프로젝트의 총 시장 가치"; -"market.global.tvl_in_defi.title" = "디파이(DeFi)에서의 총 예치 가치"; -"market.global.tvl_in_defi.description" = "디파이 (DeFi)에서의 총 예치 가치 (TVL)"; -"market.global.tvl_in_defi.multi_chain" = "다중 체인"; -"market.global.tvl_in_defi.filter_by_chain" = "체인별 필터링"; +"market.etf.title" = "총 순유입액"; +"market.etf.description" = "ETF의 순유입액은 현금 유입에서 유출을 뺀 것과 같습니다."; +"market.etf.total_net_assets" = "총 순자산"; +"market.etf.sort_by.highest_assets" = "최고 자산"; +"market.etf.sort_by.lowest_assets" = "최저 자산"; +"market.etf.sort_by.inflow" = "유입"; +"market.etf.sort_by.outflow" = "유출"; +"market.etf.period.all" = "전체"; + +"market.market_cap.title" = "총 시장 자본금"; +"market.market_cap.description" = "모든 암호화폐의 총 시장 가치\" or \"모든 가상 화폐의 총 시장 가치"; +"market.market_cap.market_cap" = "총 시가 총액"; + +"market.tvl_in_defi.title" = "디파이(DeFi)에서의 총 예치 가치"; +"market.tvl_in_defi.description" = "디파이 (DeFi)에서의 총 예치 가치 (TVL)"; +"market.tvl_in_defi.tvl" = "TVL"; +"market.tvl_in_defi.multi_chain" = "다중 체인"; +"market.tvl_in_defi.filter_by_chain" = "체인별 필터링"; +"market.tvl_in_defi.filter.all" = "전체"; + +"market.volume.title" = "거래량"; +"market.volume.description" = "암호화폐 시장의 24시간 거래량\" or \"가상 화폐 시장의 24시간 거래량"; +"market.volume.volume" = "거래량"; + +"market.signal.risky" = "위험"; +"market.signal.strong_buy" = "강력한 매수"; +"market.signal.buy" = "매수"; +"market.signal.neutral" = "중립적"; +"market.signal.sell" = "매도"; +"market.signal.strong_sell" = "강력한 매도"; // Coin Page @@ -853,10 +946,11 @@ "coin_overview.genesis_date" = "창립일"; "coin_overview.trading_volume" = "거래량"; -"coin_overview.roi.hour24" = "1 일"; -"coin_overview.roi.day7" = "1 일주"; -"coin_overview.roi.day14" = "이주"; -"coin_overview.roi.day30" = "1 개월"; +"coin_overview.roi.hour24" = "24 시간"; +"coin_overview.roi.day1" = "1 일"; +"coin_overview.roi.day7" = "1일주"; +"coin_overview.roi.day14" = "2이주"; +"coin_overview.roi.day30" = "1개월"; "coin_overview.roi.day200" = "6개월"; "coin_overview.roi.year1" = "일년"; @@ -880,49 +974,49 @@ "technical_advice.up" = "위로"; "technical_advice.over.main" = "해당 자산과 관련된 행동은 위험합니다."; -"technical_advice.over.indicators.signal_date" = "Starting from the %@"; -"technical_advice.over.indicators" = "The asset is outside the Bollinger Band channel and %@."; -"technical_advice.over.rsi" = " RSI = %@, This also indicates that the asset is %@."; -"technical_advice.over.advice" = " There might be a strong %@ward movement, so it's better to wait for the asset price to return to the channel."; +"technical_advice.over.indicators.signal_date" = "%@부터 시작합니다"; +"technical_advice.over.indicators" = "자산이 볼린저 밴드 채널 밖에 있으며 %@입니다."; +"technical_advice.over.rsi" = " RSI = %@, 이는 자산이 %@임을 나타냅니다."; +"technical_advice.over.advice" = " 강한 %@ 방향 움직임이 있을 수 있으므로 자산 가격이 채널로 돌아오기를 기다리는 것이 좋습니다."; -"technical_advice.strong.indicators" = "The asset was %@, but now it has returned to the Bollinger Band channel. This indicates a possible trend reversal."; -"technical_advice.strong.rsi" = " Meanwhile, the RSI is %@, which still indicates that it is %@."; -"technical_advice.strong.advice" = " This could be a very strong signal to enter the market. Keep in mind that there may be several attempts of %@ward movement after returning to the channel, so do not forget about risk management."; +"technical_advice.strong.indicators" = "자산이 %@이었지만 이제 볼린저 밴드 채널로 돌아왔습니다. 이는 추세 반전의 가능성을 나타냅니다."; +"technical_advice.strong.rsi" = " 한편, RSI는 %@이며, 여전히 %@임을 나타냅니다."; +"technical_advice.strong.advice" = " 이는 시장에 진입하기 위한 매우 강력한 신호일 수 있습니다. 채널로 돌아온 후 여러 번의 %@ 방향 움직임이 있을 수 있으므로, 리스크 관리에 유의하시기 바랍니다."; -"technical_advice.stable.rsi" = " Meanwhile, the RSI is %@, which also indicates a trend reversal (RSI crossed the boundary at 70%)."; -"technical_advice.stable.advice" = "The price is returning to neutral levels, however, there is still potential for upward movement. Keep in mind that RSI = 50 and the middle of the Bollinger Bands are strong resistances and possible trend reversal points. Do not forget about risk management."; +"technical_advice.stable.rsi" = " 한편, RSI는 %@이며, 이는 추세 반전을 나타냅니다 (RSI가 경계선을 넘었습니다. 70%에서)."; +"technical_advice.stable.advice" = "가격이 중립 수준으로 돌아가고 있지만, 여전히 상승 가능성이 있습니다. RSI = 50과 볼린저 밴드의 중간은 강한 저항선이자 추세 반전 지점일 수 있다는 점을 기억하십시오. 리스크 관리에 유의하시기 바랍니다."; -"technical_advice.neutral.rsi" = "RSI = %@ also confirms the absence of a strong trend."; -"technical_advice.neutral.indicators" = "The asset was in the overbought/oversold zone, but at the moment the price has returned to the Bollinger Band channel in the neutral zone. The RSI is %@ also confirms the absence of a strong trend, so overall the asset price is moving towards averaging and further movement is possible in any direction."; -"technical_advice.neutral.advice" = " In general, the asset price is moving towards averaging and further movement is possible in any direction."; +"technical_advice.neutral.rsi" = "RSI = %@도 강한 추세의 부재를 확인합니다."; +"technical_advice.neutral.indicators" = "해당 자산은 과매수/과매도 구간에 있었지만, 현재 가격은 중립 구간의 볼린저 밴드 채널로 복귀했습니다. RSI가 %@이며 강한 추세의 부재를 확인합니다. 따라서 전반적으로 자산 가격은 평균화되며 더 이상의 이동은 어느 방향으로든 가능합니다."; +"technical_advice.neutral.advice" = " 일반적으로, 자산 가격은 평균화로 향하고 더 이상의 움직임은 어느 방향으로든 가능합니다."; -"technical_advice.other.title" = "Please note:"; +"technical_advice.other.title" = "주의하세요:"; -"technical_advice.ema.above" = "above"; -"technical_advice.ema.below" = "below"; -"technical_advice.ema.growth" = "growth"; -"technical_advice.ema.decrease" = "decrease"; -"technical_advice.ema.advice" = "EMA 200. Determines the overall sentiment and trend. The daily price of the asset is located %@ the EMA (%@). This means that globally the asset is set for %@."; +"technical_advice.ema.above" = "위"; +"technical_advice.ema.below" = "아래"; +"technical_advice.ema.growth" = "성장"; +"technical_advice.ema.decrease" = "감소"; +"technical_advice.ema.advice" = "EMA 200. 전반적인 센티먼트와 트렌드를 결정합니다. 자산의 일일 가격은 EMA(%@)에 %@ 위치합니다. 이는 전반적으로 자산이 %@으로 설정되어 있음을 의미합니다."; -"technical_advice.macd.positive" = "above"; -"technical_advice.macd.negative" = "below"; -"technical_advice.macd.advice" = "MACD. Assesses the strength of the trend considering the average price change. The daily value of the histogram is %@ (%@). The price of the asset globally may move %@."; +"technical_advice.macd.positive" = "위"; +"technical_advice.macd.negative" = "아래"; +"technical_advice.macd.advice" = "MACD. 평균 가격 변화를 고려하여 추세의 강도를 평가합니다. 히스토그램의 일일 값은 %@입니다 (%@). 전반적으로 자산의 가격이 %@ 움직일 수 있습니다."; "coin_analytics.indicators.title" = "기술적 지표"; -"coin_analytics.indicators.disclaimer" = "Always remember to apply risk management, and note that this is not financial advice."; -"coin_analytics.indicators.info.title" = "Technical Indicators"; -"coin_analytics.indicators.info.description" = "We use the Bollinger Bands + RSI strategy to determine trading signals. All calculations are based on daily candlesticks and provide advice for a moderately long term. The essence of the strategy is that the asset price should reach an extreme, breaking out of the Bollinger Bands channel, and the RSI should be in the overbought/oversold zone. After the price returns to the channel, there is a high probability of the price returning to the mean values or attempting to break the channel from the other side. Note that the strategy may give several false signals during strong market movements before a correct signal appears.\n\nPlease remember that it is very important to apply risk management to trading and remember to cut losses if the market situation changes! "; -"coin_analytics.indicators.hide_details" = "Hide Details"; -"coin_analytics.indicators.show_details" = "Show Details"; +"coin_analytics.indicators.disclaimer" = "언제나 위험 관리를 적용하는 것을 기억하세요. 이것은 금융 조언이 아님을 유의하세요."; +"coin_analytics.indicators.info.title" = "기술적 지표"; +"coin_analytics.indicators.info.description" = "우리는 볼린저 밴드 + RSI 전략을 사용하여 거래 신호를 결정합니다. 모든 계산은 일일 양봉을 기반으로 하며, 중장기적인 조언을 제공합니다. 이 전략의 본질은 자산 가격이 극단에 도달하여 볼린저 밴드 채널을 벗어나야 하며, RSI가 과매수/과매도 구간에 있어야 한다는 것입니다. 가격이 채널로 복귀한 후, 가격이 평균값으로 복귀하거나 다른 측면에서 채널을 뚫으려는 높은 확률이 있습니다. 강한 시장 움직임 중에는 올바른 신호가 나타나기 전에 여러 가질 수도 있음을 유의하십시오.\n\n거래에 위험 관리를 적용하는 것이 매우 중요하며, 시장 상황이 변할 경우 손실을 줄이는 것을 기억하세요! "; +"coin_analytics.indicators.hide_details" = "세부 사항 숨기기"; +"coin_analytics.indicators.show_details" = "세부 정보 보기"; "coin_analytics.indicators.summary" = "요약"; "coin_analytics.indicators.no_data" = "데이터 없음"; -"coin_analytics.indicators.oversold" = "Very Risky to Trade"; -"coin_analytics.indicators.strong_buy" = "강한 매수"; +"coin_analytics.indicators.oversold" = "위험"; +"coin_analytics.indicators.strong_buy" = "강력한 매수"; "coin_analytics.indicators.buy" = "매수"; "coin_analytics.indicators.neutral" = "중립적"; "coin_analytics.indicators.sell" = "매도"; -"coin_analytics.indicators.strong_sell" = "강한 매도"; -"coin_analytics.indicators.overbought" = "Very Risky to Trade"; +"coin_analytics.indicators.strong_sell" = "강력한 매도"; +"coin_analytics.indicators.overbought" = "위험"; "coin_analytics.period" = "기간"; "coin_analytics.period.select_title" = "기간 선택"; "coin_analytics.period.1h" = "1시간"; @@ -935,7 +1029,7 @@ "coin_analytics.not_available" = "이 프로젝트에는 분석 데이터가 없습니다."; -"coin_analytics.technical_indicators" = "Technical Indicators"; +"coin_analytics.technical_indicators" = "기술적 지표"; "coin_analytics.technical_indicators.info1" = "요약: 이것은 다양한 기술적 지표와 시간대를 고려하여 자산의 기술적 특성을 일반적으로 개요합니다. 이러한 지표를 기반으로 한 공통된 견해 (매수, 매도 또는 중립)를 제공합니다."; "coin_analytics.technical_indicators.info2" = "이동평균 (MA): 이것들은 추세를 따르는 지표를 만들기 위해 가격 데이터를 평활하게 하는 데 사용되는 일반적인 기술 지표입니다. 이것들은 특정 시간 기간 동안의 평균 가격을 보여줍니다. 다양한 종류의 이동평균이 있습니다:\n\n단순 이동평균 (SMA): 이것은 선택한 가격 범위, 보통 종가, 내의 기간 수로 평균을 계산합니다.\n\n지수 이동평균 (EMA): 이것은 최근 가격에 더 많은 가중치를 부여하여 최근 가격 변동에 더 빨리 반응합니다."; "coin_analytics.technical_indicators.info3" = "오실레이터(Oscillators): 이것들은 일정한 범위 내에서 시간에 따라 변동하는 기술 지표로 (중심선 위 및 아래 또는 설정된 수준 사이에서), 시장에서 과매수 및 과매도 상태를 식별하는 데 도움을 주도록 설계되었습니다. 여기 일반적인 오실레이터 중 일부가 있습니다:\n\n상대강도지수 (RSI): 이것은 가격 움직임의 속도와 변화를 측정합니다. 주로 과매수 또는 과매도 상태를 식별하는 데 사용됩니다.\n\n이동평균수렴확산 (MACD): 이것은 잠재적인 매수 및 매도 신호를 식별하는 데 사용됩니다. MACD는 신호선을 크로스할 때 (매수) 또는 (매도) 아래로 크로스할 때 기술적 신호를 생성합니다."; @@ -943,6 +1037,7 @@ "coin_analytics.cex_volume" = "CEX 거래량"; "coin_analytics.cex_volume_rank" = "CEX 거래량 순위"; "coin_analytics.cex_volume_rank.description" = "중앙화 거래소에서의 토큰 거래량을 기준으로 순위가 매겨진 토큰들."; +"coin_analytics.cex_volume_rank.sorting_field" = "거래량"; "coin_analytics.cex_volume.info1" = "주요 중앙화 거래소에서의 토큰의 30일 동안의 총 거래량."; "coin_analytics.cex_volume.info2" = "주요 중앙화 거래소에서의 토큰의 1년 동안 일일 거래량 변동을 보여주는 차트."; "coin_analytics.cex_volume.info3" = "Token's rank is based on trading volume on leading centralized exchanges over 30-day period."; @@ -951,6 +1046,7 @@ "coin_analytics.dex_volume" = "DEX 거래량"; "coin_analytics.dex_volume_rank" = "DEX 거래량 순위"; "coin_analytics.dex_volume_rank.description" = "탈중앙화 거래소에서의 토큰 거래량을 기준으로 순위가 매겨진 토큰들."; +"coin_analytics.dex_volume_rank.sorting_field" = "거래량"; "coin_analytics.dex_volume.info1" = "주요 탈중앙화 거래소에서의 토큰의 30일 동안의 총 거래량."; "coin_analytics.dex_volume.info2" = "1년 동안 주요 탈중앙화 거래소에서의 토큰의 일일 거래량 변동을 보여주는 차트."; "coin_analytics.dex_volume.info3" = "주요 탈중앙화 거래소에서 30일 동안의 거래량을 기반으로 한 토큰의 순위."; @@ -962,6 +1058,7 @@ "coin_analytics.dex_liquidity" = "DEX 유동성"; "coin_analytics.dex_liquidity_rank" = "DEX 유동성 순위"; "coin_analytics.dex_liquidity_rank.description" = "탈중앙화 거래소에서 사용 가능한 유동성을 기준으로 한 토큰들의 순위."; +"coin_analytics.dex_liquidity_rank.sorting_field" = "유동성"; "coin_analytics.dex_liquidity.info1" = "주요 탈중앙화 거래소에서 해당 토큰의 현재 사용 가능한 유동성 총액."; "coin_analytics.dex_liquidity.info2" = "주요 탈중앙화 거래소에서의 해당 토큰의 1년 동안 사용 가능한 유동성 변동을 보여주는 차트."; "coin_analytics.dex_liquidity.info3" = "주요 탈중앙화 거래소에서의 해당 토큰의 사용 가능한 유동성을 기준으로 순위 매겨진 모든 토큰의 목록."; @@ -973,6 +1070,7 @@ "coin_analytics.active_addresses.30_day_unique_addresses" = "30일 동안의 고유 주소"; "coin_analytics.active_addresses_rank" = "활성 주소 순위"; "coin_analytics.active_addresses_rank.description" = "토큰과 거래하는 고유 주소의 수에 따라 순위가 매겨진 토큰들."; +"coin_analytics.active_addresses_rank.sorting_field" = "활성"; "coin_analytics.active_addresses.info1" = "24시간 동안의 고유 일일 활성 주소 총 수."; "coin_analytics.active_addresses.info2" = "1년 동안 일일 활성 주소 수의 변동을 보여주는 차트."; "coin_analytics.active_addresses.info3" = "Total number of unique blockchain addresses transacting with token over 30-day period."; @@ -982,6 +1080,7 @@ "coin_analytics.transaction_count" = "트랜잭션 수"; "coin_analytics.transaction_count_rank" = "거래 수 순위"; "coin_analytics.transaction_count_rank.description" = "토큰은 블록체인에서의 거래 수에 따라 순위가 매겨집니다."; +"coin_analytics.transaction_count_rank.sorting_field" = "수량"; "coin_analytics.transaction_count.info1" = "Total number of unique blockchain transactions with token over 30-day period."; "coin_analytics.transaction_count.info2" = "1년 동안의 거래 수 변동을 보여주는 차트."; "coin_analytics.transaction_count.info3" = "토큰의 순위는 30일 동안의 토큰 거래 수를 기준으로 합니다."; @@ -991,6 +1090,7 @@ "coin_analytics.holders" = "소유주"; "coin_analytics.holders_rank" = "소유주 순위"; "coin_analytics.holders_rank.description" = "여러 블록체인에서 그들을 보유한 고유 주소에 의해 토큰을 순위 매김."; +"coin_analytics.holders_rank.sorting_field" = "소유주"; "coin_analytics.holders.info1" = "다양한 블록체인에서 토큰을 보유한 고유 주소의 총 수."; "coin_analytics.holders.info2" = "각 블록체인에서 토큰을 보유한 상위 10개 지갑."; "coin_analytics.holders.tracked_blockchains" = "추적되는 블록체인: 이더리움, 바이낸스 스마트 체인, 옵티미즘, 아비트럼, 셀로, 크로노스, 아발란체, 팬텀, 폴리곤"; @@ -1010,10 +1110,12 @@ "coin_analytics.project_fee" = "프로젝트 수수료"; "coin_analytics.project_fee_rank" = "프로젝트 수수료 순위"; "coin_analytics.project_fee_rank.description" = "Tokens ranked according to fees generated by respective projects. The way fees are collected varies from project to project."; +"coin_analytics.project_fee_rank.sorting_field" = "거래량"; "coin_analytics.project_revenue" = "프로젝트 수익"; "coin_analytics.project_revenue_rank" = "프로젝트 수익 순위"; "coin_analytics.project_revenue_rank.description" = "Tokens ranked by revenue generated for holders via mechanisms i.e. staking or token burns."; +"coin_analytics.project_revenue_rank.sorting_field" = "매출"; "coin_analytics.other_data" = "기타 데이터"; @@ -1199,6 +1301,8 @@ "settings.rate_us" = "앱 평가"; "settings.tell_friends" = "친구에게 추천하기"; "settings.contact_us" = "문의하기"; +"settings.social_networks.label" = "Be Unstoppable"; +"settings.social_networks.footer" = "독점 비디오로 암호화폐를 배우고 정복하세요. 비공식적으로 우리를 알아가세요. 우리가 작업하는 것들을 가장 먼저 보실 수 있습니다."; // Settings -> Base Currency @@ -1416,9 +1520,7 @@ // Settings -> About App "settings.about_app.title" = "앱 정보"; -"settings.about_app.app_name" = "%@ 지갑"; -"settings.about_app.description" = "%@ 지갑은 암호화폐를 개인적이고 독립적인 방식으로 투자하고 보관하려는 사람들을 위해 만들어졌습니다. \n\n이 지갑은 사용자만이 자금에 대한 통제를 갖는 비보관형 P2P 지갑입니다. 데이터를 수집하지 않으며 사용자의 자금을 특정 지갑 앱에 잠그지 않아 독립성을 유지합니다. \n\n%@ 지갑은 완전한 오픈소스이며 누구든지 앱이 주장하는 대로 정확하게 작동하는지 확인할 수 있습니다."; -"settings.about_app.whats_new" = "새로운 기능"; +"settings.about_app.app_version" = "앱버전"; "settings.about_app.website" = "웹사이트"; // Settings -> About App -> Contact @@ -1430,11 +1532,11 @@ // Settings -> Privacy "settings.privacy" = "개인정보"; -"settings.privacy.description" = "%@은 사용자에 대한 데이터를 노출할 수 있는 데이터를 수집하거나 분석 도구를 사용하지 않습니다. 지갑은 사용자에게 높은 수준의 개인 정보를 보장하도록 설계되었습니다."; -"settings.privacy.statement.user_data_storage" = "사용자 데이터는 항상 사용자의 장치에 남아 있습니다."; +"settings.privacy.description" = "%@는 코인 잔액이나 주소와 같은 개인 정보를 노출하는 개인 데이터를 수집하지 않습니다. UI 사용 통계를 수집하긴 하지만, 이는 사용자 기반 및 앱 사용 트렌드를 이해하기 위한 것입니다. 원하신다면 비활성화할 수 있습니다."; "settings.privacy.statement.data_usage" = "지갑은 사용자에 대한 데이터를 수집하지 않습니다."; -"settings.privacy.statement.data_privacy" = "지갑은 사용자에 대한 데이터를 공유하지 않습니다."; -"settings.privacy.statement.user_account" = "사용자 데이터를 다른 곳에 보관하는 사용자 계정이나 데이터베이스가 없습니다."; +"settings.privacy.statement.data_storage" = "사용자 계정이나 사용자 데이터를 저장하는 데이터베이스가 없습니다."; +"settings.privacy.statement.user_account" = "허용된 경우, 지갑은 앱 사용 습관을 Unstoppable 팀과 공유할 것입니다. 이는 사용자가 어떤 기능을 사용하고 있는지 (또는 사용하지 않는지) 이해하기 위한 것입니다. 개인 정보 보호를 중시하는 앱이지만, 우리의 노력을 평가할 방법이 필요합니다. 이를 통해 만든 기능이 사용되고 있는지 여부를 알 수 없기 때문입니다."; +"settings.privacy.allow" = "UI 데이터 공유"; // Settings -> Appearance @@ -1445,21 +1547,25 @@ "appearance.theme.dark" = "어두운"; "appearance.theme.light" = "밝은"; -"appearance.tab_settings" = "탭 설정"; "appearance.markets_tab" = "마켓 탭"; +"appearance.hide_markets" = "마켓 숨기기"; +"appearance.price_change" = "가격 변경"; +"appearance.price_change.24h" = "24시"; +"appearance.price_change.1d" = "UTC 자정"; + "appearance.launch_screen" = "시작 화면"; "appearance.launch_screen.auto" = "자동"; "appearance.launch_screen.balance" = "잔액"; "appearance.launch_screen.market_overview" = "시장 개요"; "appearance.launch_screen.watchlist" = "관심 목록"; -"appearance.app_icon" = "앱 아이콘"; - -"appearance.balance_conversion" = "잔액 변환"; - +"appearance.balance_tab" = "잔고 탭"; +"appearance.hide_buttons" = "숨김 버튼"; "appearance.balance_value" = "균형 가치"; -"appearance.balance_value.coin_value" = "코인 가치"; -"appearance.balance_value.fiat_value" = "법정 가치"; +"appearance.balance_value.coin_fiat" = "코인/피아트"; +"appearance.balance_value.fiat_coin" = "피아트/코인"; + +"appearance.app_icon" = "앱 아이콘"; // Settings -> Contacts @@ -1524,7 +1630,7 @@ "chart.time_duration.day" = "24시"; "chart.time_duration.week" = "7일"; -"chart.time_duration.week2" = "2주"; +"chart.time_duration.week2" = "2주일"; "chart.time_duration.month" = "1개월"; "chart.time_duration.month3" = "3개월"; "chart.time_duration.halfyear" = "6개월"; @@ -1618,7 +1724,7 @@ "add_token.contract_address_not_found" = "%@ 블록체인에서 계약 주소를 찾을 수 없습니다."; "add_token.bep2_symbol_not_found" = "BEP2 기호를 찾을 수 없음"; "add_token.input_placeholder.contract_address" = "계약 주소"; -"add_token.input_placeholder.bep2_symbol" = "BEP2 Symbol"; +"add_token.input_placeholder.bep2_symbol" = "BEP2 심볼"; "add_token.coin_name" = "코인 이름"; "add_token.symbol" = "기호"; "add_token.decimals" = "소수점"; @@ -1960,7 +2066,6 @@ "tron.send.fee.info" = "네트워크에서 거래하는 데 드는 예상 비용입니다. (energy, bandwidth 및 활성화 수수료 제외)"; "tron.send.resources_consumed.info" = "Bandwidth 은 블록체인 데이터베이스에 저장된 트랜잭션 바이트 크기를 측정하는 단위입니다. 거래가 클수록 대역폭 자원이 더 많이 사용됩니다.\n\nEnergy 는 TRON 가상 머신이 TRON 네트워크에서 특정 작업을 수행하기 위해 필요한 계산 양을 측정하는 단위입니다. \n\n스마트 계약 트랜잭션은 실행에 계산 자원이 필요하기 때문에 각 스마트 계약 트랜잭션에는 energy 수수료를 지불해야합니다."; "tron.send.activation_fee.info" = "TRX나 TRC-10 토큰을 비활성화된 계정 주소로 이전하면 해당 계정이 활성화됩니다."; -"tron.send.inactive_address" = "이 주소는 활성화되어 있지 않습니다"; // Cex Coin Select diff --git a/UnstoppableWallet/UnstoppableWallet/pt-BR.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/pt-BR.lproj/Localizable.strings index de6fc27246..44fb376248 100644 --- a/UnstoppableWallet/UnstoppableWallet/pt-BR.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/pt-BR.lproj/Localizable.strings @@ -228,6 +228,7 @@ Go to Settings - > %@ and allow access to the camera."; "extended_key.purpose" = "Propósito"; "extended_key.blockchain" = "Blockchain"; "extended_key.account" = "Conta"; +"extended_key.account.description" = "Esta é uma configuração para usuários avançados. Se estiver tentando importar uma carteira (através da chave privada estendida) ou uma lista de transações (através da chave pública estendida), você precisa da conta 0."; "extended_key.tap_to_show" = "Get ready for it, folks! A little tap, and you'll see that extended private key. It's like unlocking a world of possibilities!"; // Backup @@ -318,7 +319,6 @@ Go to Settings - > %@ and allow access to the camera."; "balance.downloading_blocks" = "Baixando Blocos"; "balance.scanning_blocks" = "Verificando Blocos"; "balance.enhancing_transactions" = "Aprimorando Transações"; -"wait_for_synchronization" = "Aguarde a sincronização"; "balance.searching.count" = "%@ tx"; "balance.syncing_percent" = "Sincronizando... %@"; @@ -351,6 +351,8 @@ Go to Settings - > %@ and allow access to the camera."; "balance.token.frozen" = "Travado"; "balance.token.frozen.info.title" = "Tempo de congelamento"; "balance.token.frozen.info.description" = "Texto de Descrição Congelada"; +"balance.token.account.inactive.title" = "Conta Não Ativa"; +"balance.token.account.inactive.description" = "Novas carteiras TRON requerem um depósito mínimo de 1 TRX para se tornarem ativas. Carteiras inativas podem armazenar e receber tokens, mas não atualizarão os saldos até serem ativadas."; // Account switcher @@ -445,6 +447,8 @@ Go to Settings - > %@ and allow access to the camera."; "send.transaction_inputs_outputs_info.shuffle.description" = "A ordem das saídas da transação é aleatória em cada transação. Às vezes, a mudança pode ser a primeira saída, às vezes pode ser a segunda. Se um usuário confiar no desenvolvedor do aplicativo, considere essa uma opção recomendada."; "send.transaction_inputs_outputs_info.deterministic.title" = "2. Determinístico"; "send.transaction_inputs_outputs_info.deterministic.description" = "Existe um padrão comumente aceitado para ordenar saídas de transações (conhecido como BIP69). Em carteiras de código aberto, esse padrão garante que os usuários de carteiras não precisem confiar em como os desenvolvedores do aplicativo implementam a ordem das saídas. Como esse padrão é novo, poucas carteiras o implementaram ainda. Como resultado, é possível ver no blockchain se uma transação foi enviada de uma carteira que usa esse padrão ou não."; +"send.select_all" = "Selecionar Tudo"; +"send.unselect_all" = "Desmarcar Tudo"; "send.confirmation.title" = "Confirmar"; "send.confirmation.you_send" = "Enviar"; @@ -462,18 +466,20 @@ Go to Settings - > %@ and allow access to the camera."; "send.confirmation.time_lock" = "TimeLock"; "send.confirmation.replace_by_fee" = "Substituir pela taxa"; "send.confirmation.replaced_transactions" = "Transações Substituídas"; - +"send.confirmation.input" = "Entrada"; + +"send.confirmation.sync_failed" = "Falha na Sincronização"; +"send.confirmation.invalid_data" = "Dados Inválidos"; +"send.confirmation.refresh" = "Atualizar"; +"send.confirmation.please_wait" = "Por Favor, Aguarde"; +"send.confirmation.expires_in" = "Expira em %@"; +"send.confirmation.expired" = "Expirado"; "send.confirmation.slide_to_send" = "Deslize para enviar"; "send.confirmation.sending" = "Enviando"; "send.confirmation.sent" = "Enviado"; "send.confirmation.slide_to_approve" = "Deslize para Aprovar"; -"send.confirmation.approving" = "Aprovando"; -"send.confirmation.approved" = "Aprovado"; - "send.confirmation.slide_to_revoke" = "Deslize para Revogar"; -"send.confirmation.revoking" = "Revogando"; -"send.confirmation.revoked" = "Revogado"; "send.confirmation.slide_to_resend" = "Enviar novamente"; "send.confirmation.slide_to_cancel" = "Cancelar transação"; @@ -506,6 +512,7 @@ Go to Settings - > %@ and allow access to the camera."; "send.lock_time" = "TravaTempo"; "send.unspent_outputs" = "UTxOs"; +"send.unspent_outputs.description" = "Selecionar manualmente UTxO para gastar os fundos no saldo"; "send.unspent_outputs.send_to" = "Enviar para"; "send.unspent_outputs.change" = "Alterar"; @@ -513,6 +520,14 @@ Go to Settings - > %@ and allow access to the camera."; "approve.confirmation.you_revoke" = "Você revoga"; "approve.confirmation.spender" = "Gastador"; +"send.enter_amount" = "Inserir Quantidade"; +"send.enter_address" = "Insira o Endereço"; +"send.invalid_address" = "Endereço Inválido"; +"send.token_not_enabled" = "Token não ativado"; +"send.token_syncing" = "Sincronização do Token"; +"send.token_not_synced" = "Token não sincronizado"; +"send.insufficient_balance" = "Saldo insuficiente"; + // Donate "donate.list.title" = "Doar com"; @@ -588,7 +603,7 @@ Go to Settings - > %@ and allow access to the camera."; "swap.unlock.title" = "Desbloquear Acesso"; "swap.unlock.subtitle" = "Permitir acesso ao seguinte valor"; -"swap.unlock.description" = "Grant permission to a smart contract for token trading on your behalf, specifying the allowed amount. No impact on your balance, but incurs a small fee for approval.\n\nPre-approving a higher amount for future trades is cost-effective compared to on-demand approvals."; +"swap.unlock.description" = "Conceda permissão a um contrato inteligente para negociar tokens em seu nome, especificando a quantidade permitida. Isso não afeta seu saldo, mas incorre em uma pequena taxa para aprovação.\n\nPré-aprovar uma quantidade maior para negociações futuras é mais econômico em comparação com aprovações sob demanda."; "swap.unlock.unlimited" = "Ilimitado"; "swap.price_impact" = "Impacto de preço"; @@ -636,14 +651,13 @@ Go to Settings - > %@ and allow access to the camera."; "swap.confirmation.title" = "Confirmar"; "swap.confirmation.quoting" = "Cotação..."; "swap.confirmation.invalid_quote" = "Cotação incorreta"; -"swap.confirmation.please_wait" = "Por favor, aguarde"; +"swap.confirmation.please_wait" = "Por Favor, Aguarde"; "swap.confirmation.quote_expires_in" = "O prazo da cotação expira em %@"; "swap.confirmation.quote_expired" = "A cotação expirou"; "swap.confirmation.quote_failed" = "Erro ao cotar"; "swap.confirmation.slide_to_swap" = "Deslize para trocar"; "swap.confirmation.swapping" = "Trocando"; -"swap.confirmation.swapped" = "Trocado"; "swap.confirmation.refresh" = "Atualizar"; "swap.confirmation.impact_too_high" = "%@ desativou a ação de troca para essa troca porque você está obtendo um preço extremamente desfavorável. Isso se deve a liquidez extremamente baixa.\nSe você ainda quiser trocar, use o site %@."; "swap.confirmation.impact_warning" = "Importante! Você está recebendo um preço extremamente desfavorável. Isso se deve a uma liquidez extremamente baixa."; @@ -686,6 +700,51 @@ Go to Settings - > %@ and allow access to the camera."; "market.defi_cap" = "Mercado de DeFi"; "market.defi_tvl" = "TVL no DeFi"; +"market.global.market_cap" = "Valor total de mercado"; +"market.global.volume" = "Volume em 24 horas"; +"market.global.btc_dominance" = "Dominação BTC"; +"market.global.etf_inflow" = "Entrada de ETF"; +"market.global.tvl_in_defi" = "TVL no DeFi"; + +"market.tab.news" = "Notícias"; +"market.tab.coins" = "Moedas"; +"market.tab.watchlist" = "Lista de observação"; +"market.tab.platforms" = "Plataformas"; +"market.tab.pairs" = "Pares"; +"market.tab.sectors" = "Setores"; + +"market.sort_by.title" = "Organizar por"; +"market.sort_by.manual" = "Manual"; +"market.sort_by.highest_cap" = "Maior capitalização"; +"market.sort_by.lowest_cap" = "Menor capitalização"; +"market.sort_by.gainers" = "Ganhadores"; +"market.sort_by.losers" = "Perdedores"; +"market.sort_by.highest_volume" = "Maior volume"; +"market.sort_by.lowest_volume" = "Menor volume"; + +"market.top_coins.title" = "Moedas"; +"market.top_coins" = "Primeiros %@"; + +"market.time_period.title" = "Período"; +"market.time_period.1d" = "1 Dia"; +"market.time_period.1w" = "1 Semana"; +"market.time_period.2w" = "2 Semanas"; +"market.time_period.1m" = "1 Meses"; +"market.time_period.3m" = "3 Meses"; +"market.time_period.6m" = "6 Meses"; +"market.time_period.1y" = "1 Ano"; +"market.time_period.2y" = "2 Anos"; +"market.time_period.5y" = "5 Anos"; +"market.time_period.1d.short" = "1D"; +"market.time_period.1w.short" = "1S"; +"market.time_period.2w.short" = "2S"; +"market.time_period.1m.short" = "1M"; +"market.time_period.3m.short" = "3M"; +"market.time_period.6m.short" = "6M"; +"market.time_period.1y.short" = "1A"; +"market.time_period.2y.short" = "2A"; +"market.time_period.5y.short" = "5A"; + "market.project_has_no_coin" = "Esse projeto não tem uma moeda"; "market.top.section.header.see_all" = "Ver tudo"; @@ -722,6 +781,7 @@ Go to Settings - > %@ and allow access to the camera."; "market.top.top_platforms" = "Melhores Plataformas"; "market.top.protocols" = "Protocolos"; +"market.pairs.volume" = "Volume"; "top_pairs.title" = "Principais Pares de Mercado"; "top_pairs.description" = "Principais pares de negociação por volume em todas as bolsas"; @@ -730,6 +790,8 @@ Go to Settings - > %@ and allow access to the camera."; "top_platform.title" = "%@ Ecossistema"; "top_platform.description" = "Valor de mercado de todos procolos na cadeia %@"; +"top_platform.total_cap" = "Valor total de mercado"; + "market.search.recent" = "Recentes"; "market.search.popular" = "Populares"; @@ -741,10 +803,22 @@ Go to Settings - > %@ and allow access to the camera."; "market_discovery.not_found" = "Nenhum resultado encontrado"; "market_watchlist.empty.caption" = "Seu catálogo está vazio."; +"market.watchlist.signals" = "Sinais"; +"market.watchlist.empty" = "Seu catálogo está vazio"; +"market.watchlist.signals.description" = "Os sinais abaixo são baseados nos indicadores técnicos de preço das Bandas de Bollinger e RSI ao longo dos últimos aproximadamente 30 dias. Esses sinais são algorítmicos e podem mudar com frequência."; +"market.watchlist.signals.strong_buy.description" = "Alta confiança em aumento de preço"; +"market.watchlist.signals.buy.description" = "Provável aumento de preço no futuro próximo"; +"market.watchlist.signals.neutral.description" = "Sem tendência clara, o mercado está em equilíbrio"; +"market.watchlist.signals.sell.description" = "Provável queda de preço no futuro próximo"; +"market.watchlist.signals.strong_sell.description" = "Alta probabilidade de redução de preço"; +"market.watchlist.signals.risky.description" = "Nível de risco elevado, requer cuidado"; +"market.watchlist.signals.warning" = "Sempre lembre-se de aplicar o gerenciamento de risco, e observe que isso não é um conselho financeiro."; +"market.watchlist.signals.turn_on" = "Ligar"; "market.advanced_search.title" = "Filtros"; "market.advanced_search.show_results" = "Mostrar Resultados"; "market.advanced_search.empty_results" = "Nenhum resultado"; +"market.advanced_search.retry" = "Tentar de Novo"; "market.advanced_search.dex_description" = "Esta configuração se aplica a tokens operados na Ethereum (Uniswap DEX) e na Binance Smart Chain (Pancake DEX)."; "market.advanced_search.24h" = "24h"; @@ -764,17 +838,10 @@ Go to Settings - > %@ and allow access to the camera."; "market.advanced_search.liquidity" = "Liquidez DEX"; "market.advanced_search.blockchains" = "Blockchains"; -"market.advanced_search.technical_advice" = "Sinais de negociação"; +"market.advanced_search.signal" = "Sinal de Negociação"; "market.advanced_search.price_period" = "Período de Preço"; "market.advanced_search.price_change" = "Mudança de Preço"; -"market.advanced_search.technical_advice.risk_trade" = "Risco de Negociação"; -"market.advanced_search.technical_advice.strong_buy" = "Compra forte"; -"market.advanced_search.technical_advice.buy" = "Comprar"; -"market.advanced_search.technical_advice.neutral" = "Neutra"; -"market.advanced_search.technical_advice.sell" = "Vender"; -"market.advanced_search.technical_advice.strong_sell" = "Venda Forte"; - "market.advanced_search.outperformed_btc" = "BTC superado"; "market.advanced_search.outperformed_eth" = "ETH superado"; "market.advanced_search.outperformed_bnb" = "BNB superado"; @@ -811,11 +878,11 @@ Go to Settings - > %@ and allow access to the camera."; "market.advanced_search.day" = "1 Dia"; "market.advanced_search.week" = "1 Semana"; "market.advanced_search.week2" = "2 Semanas"; -"market.advanced_search.month" = "1 Mês"; +"market.advanced_search.month" = "1 Meses"; "market.advanced_search.month6" = "6 Meses"; "market.advanced_search.year" = "1 Ano"; -"market.advanced_search.day.short" = "24h"; +"market.advanced_search.day.short" = "24H"; "market.advanced_search.week.short" = "7D"; "market.advanced_search.month.short" = "1M"; @@ -830,10 +897,36 @@ Go to Settings - > %@ and allow access to the camera."; "market.global.defi_cap.title" = "Mercado de DeFi"; "market.global.defi_cap.description" = "Valor total de mercado de projetos DeFi"; -"market.global.tvl_in_defi.title" = "TVL no DeFi"; -"market.global.tvl_in_defi.description" = "Valor total bloqueado (TVL) no DeFi"; -"market.global.tvl_in_defi.multi_chain" = "Multi-Cadeia"; -"market.global.tvl_in_defi.filter_by_chain" = "Filtrar por cadeia"; +"market.etf.title" = "Total de Despesa líquida"; +"market.etf.description" = "A entrada líquida de um ETF é igual a seus fluxos de caixa menos outflows."; +"market.etf.total_net_assets" = "Total de Ativos Líquidos"; +"market.etf.sort_by.highest_assets" = "Ativos mais altos"; +"market.etf.sort_by.lowest_assets" = "Menos Ativos"; +"market.etf.sort_by.inflow" = "Receita"; +"market.etf.sort_by.outflow" = "Despesa"; +"market.etf.period.all" = "Todos"; + +"market.market_cap.title" = "Capitalização Total de Mercado"; +"market.market_cap.description" = "Valor total de mercado de todas as criptomoedas"; +"market.market_cap.market_cap" = "Capitalização de Mercado"; + +"market.tvl_in_defi.title" = "TVL no DeFi"; +"market.tvl_in_defi.description" = "Valor total bloqueado (TVL) no DeFi"; +"market.tvl_in_defi.tvl" = "TVL"; +"market.tvl_in_defi.multi_chain" = "Multi-Cadeia"; +"market.tvl_in_defi.filter_by_chain" = "Filtrar por cadeia"; +"market.tvl_in_defi.filter.all" = "Todos"; + +"market.volume.title" = "Volume"; +"market.volume.description" = "O volume de operação em 24h do mercado de cripto"; +"market.volume.volume" = "Volume"; + +"market.signal.risky" = "Arriscado"; +"market.signal.strong_buy" = "Compra forte"; +"market.signal.buy" = "Comprar"; +"market.signal.neutral" = "Neutra"; +"market.signal.sell" = "Vender"; +"market.signal.strong_sell" = "Venda Forte"; // Coin Page @@ -854,10 +947,11 @@ Go to Settings - > %@ and allow access to the camera."; "coin_overview.genesis_date" = "Data de Início"; "coin_overview.trading_volume" = "Volume de Operações"; -"coin_overview.roi.hour24" = "1 Dia"; +"coin_overview.roi.hour24" = "24 Horas"; +"coin_overview.roi.day1" = "1 Dia"; "coin_overview.roi.day7" = "1 Semana"; "coin_overview.roi.day14" = "2 Semanas"; -"coin_overview.roi.day30" = "1 Mês"; +"coin_overview.roi.day30" = "1 Meses"; "coin_overview.roi.day200" = "6 Meses"; "coin_overview.roi.year1" = "1 Ano"; @@ -877,53 +971,53 @@ Go to Settings - > %@ and allow access to the camera."; // Coin Page -> Analytics "technical_advice.over.bought" = "sobrecomprado"; "technical_advice.over.sold" = "sobrevendido"; -"technical_advice.down" = "down"; -"technical_advice.up" = "up"; +"technical_advice.down" = "baixo"; +"technical_advice.up" = "alto"; -"technical_advice.over.main" = "The actions with the asset are risky."; -"technical_advice.over.indicators.signal_date" = "Starting from the %@"; -"technical_advice.over.indicators" = "The asset is outside the Bollinger Band channel and %@."; -"technical_advice.over.rsi" = " RSI = %@, This also indicates that the asset is %@."; -"technical_advice.over.advice" = " There might be a strong %@ward movement, so it's better to wait for the asset price to return to the channel."; +"technical_advice.over.main" = "As ações com o ativo são arriscadas."; +"technical_advice.over.indicators.signal_date" = "Começando a partir do %@"; +"technical_advice.over.indicators" = "O ativo está fora do canal das Bandas de Bollinger e %@."; +"technical_advice.over.rsi" = " RSI = %@. Isso também indica que o ativo está %@."; +"technical_advice.over.advice" = " Pode haver um forte movimento %@, então é melhor esperar que o preço do ativo retorne ao canal."; -"technical_advice.strong.indicators" = "The asset was %@, but now it has returned to the Bollinger Band channel. This indicates a possible trend reversal."; -"technical_advice.strong.rsi" = " Meanwhile, the RSI is %@, which still indicates that it is %@."; -"technical_advice.strong.advice" = " This could be a very strong signal to enter the market. Keep in mind that there may be several attempts of %@ward movement after returning to the channel, so do not forget about risk management."; +"technical_advice.strong.indicators" = "O ativo estava %@, mas agora retornou ao canal das Bandas de Bollinger. Isso indica uma possível reversão de tendência."; +"technical_advice.strong.rsi" = " Enquanto isso, o RSI está %@, o que ainda indica que está %@."; +"technical_advice.strong.advice" = " Este pode ser um sinal muito forte para entrar no mercado. Tenha em mente que pode haver várias tentativas de movimento em direção a %@ após o retorno ao canal, então não se esqueça do gerenciamento de risco."; -"technical_advice.stable.rsi" = " Meanwhile, the RSI is %@, which also indicates a trend reversal (RSI crossed the boundary at 70%)."; -"technical_advice.stable.advice" = "The price is returning to neutral levels, however, there is still potential for upward movement. Keep in mind that RSI = 50 and the middle of the Bollinger Bands are strong resistances and possible trend reversal points. Do not forget about risk management."; +"technical_advice.stable.rsi" = " Enquanto isso, o RSI está %@, o que também indica uma reversão de tendência (o RSI cruzou o limite em 70%)."; +"technical_advice.stable.advice" = "O preço está retornando aos níveis neutros; no entanto, ainda há potencial para movimentos ascendentes. Tenha em mente que o RSI = 50 e o meio das Bandas de Bollinger são resistências fortes e possíveis pontos de reversão de tendência. Não se esqueça do gerenciamento de risco."; -"technical_advice.neutral.rsi" = "RSI = %@ also confirms the absence of a strong trend."; -"technical_advice.neutral.indicators" = "The asset was in the overbought/oversold zone, but at the moment the price has returned to the Bollinger Band channel in the neutral zone. The RSI is %@ also confirms the absence of a strong trend, so overall the asset price is moving towards averaging and further movement is possible in any direction."; -"technical_advice.neutral.advice" = " In general, the asset price is moving towards averaging and further movement is possible in any direction."; +"technical_advice.neutral.rsi" = "RSI = %@ também confirma a ausência de uma tendência forte."; +"technical_advice.neutral.indicators" = "O ativo estava na zona de sobrecompra/sobrevenda, mas no momento o preço retornou ao canal das Bandas de Bollinger na zona neutra. O RSI está em %@, o que também confirma a ausência de uma tendência forte, então, no geral, o preço do ativo está se movendo em direção à média e movimentos adicionais são possíveis em qualquer direção."; +"technical_advice.neutral.advice" = " Em geral, o preço do ativo está se movendo em direção à média e movimentos adicionais são possíveis em qualquer direção."; -"technical_advice.other.title" = "Please note:"; +"technical_advice.other.title" = "Por favor, observe:"; -"technical_advice.ema.above" = "above"; -"technical_advice.ema.below" = "below"; -"technical_advice.ema.growth" = "growth"; -"technical_advice.ema.decrease" = "decrease"; -"technical_advice.ema.advice" = "EMA 200. Determines the overall sentiment and trend. The daily price of the asset is located %@ the EMA (%@). This means that globally the asset is set for %@."; +"technical_advice.ema.above" = "acima"; +"technical_advice.ema.below" = "abaixo"; +"technical_advice.ema.growth" = "crescimento"; +"technical_advice.ema.decrease" = "decrescente"; +"technical_advice.ema.advice" = "EMA 200. Determina o sentimento geral e a tendência. O preço diário do ativo está localizado %@ o EMA (%@). Isso significa que globalmente o ativo está configurado para %@."; -"technical_advice.macd.positive" = "above"; -"technical_advice.macd.negative" = "below"; -"technical_advice.macd.advice" = "MACD. Assesses the strength of the trend considering the average price change. The daily value of the histogram is %@ (%@). The price of the asset globally may move %@."; +"technical_advice.macd.positive" = "acima"; +"technical_advice.macd.negative" = "abaixo"; +"technical_advice.macd.advice" = "MACD. Avalia a força da tendência considerando a mudança média de preço. O valor diário do histograma é %@ (%@). O preço do ativo globalmente pode se mover %@."; "coin_analytics.indicators.title" = "Indicadores Técnicos"; -"coin_analytics.indicators.disclaimer" = "Always remember to apply risk management, and note that this is not financial advice."; +"coin_analytics.indicators.disclaimer" = "Sempre lembre-se de aplicar o gerenciamento de risco, e observe que isso não é um conselho financeiro."; "coin_analytics.indicators.info.title" = "Technical Indicators"; -"coin_analytics.indicators.info.description" = "We use the Bollinger Bands + RSI strategy to determine trading signals. All calculations are based on daily candlesticks and provide advice for a moderately long term. The essence of the strategy is that the asset price should reach an extreme, breaking out of the Bollinger Bands channel, and the RSI should be in the overbought/oversold zone. After the price returns to the channel, there is a high probability of the price returning to the mean values or attempting to break the channel from the other side. Note that the strategy may give several false signals during strong market movements before a correct signal appears.\n\nPlease remember that it is very important to apply risk management to trading and remember to cut losses if the market situation changes! "; -"coin_analytics.indicators.hide_details" = "Hide Details"; -"coin_analytics.indicators.show_details" = "Show Details"; +"coin_analytics.indicators.info.description" = "Nós utilizamos a estratégia Bollinger Bands + RSI para determinar sinais de negociação. Todas as análises são baseadas em candlesticks diários e fornecem orientação para um prazo moderadamente longo. A essência da estratégia é que o preço do ativo deve atingir um extremo, saindo do canal das Bandas de Bollinger, e o RSI deve estar na zona de sobrecompra/sobrevenda. Após o preço retornar ao canal, há uma alta probabilidade de o preço retornar aos valores médios ou tentar romper o canal pelo outro lado. Note que a estratégia pode fornecer vários sinais falsos durante movimentos fortes do mercado antes de um sinal correto aparecer.\n\nPor favor, lembre-se de que é muito importante aplicar gerenciamento de risco à negociação e lembrar-se de cortar as perdas se a situação de mercado mudar! "; +"coin_analytics.indicators.hide_details" = "Ocultar detalhes"; +"coin_analytics.indicators.show_details" = "Mostrar detalhes"; "coin_analytics.indicators.summary" = "Resumo"; "coin_analytics.indicators.no_data" = "Sem dados"; -"coin_analytics.indicators.oversold" = "Very Risky to Trade"; +"coin_analytics.indicators.oversold" = "Arriscado"; "coin_analytics.indicators.strong_buy" = "Compra forte"; "coin_analytics.indicators.buy" = "Comprar"; "coin_analytics.indicators.neutral" = "Neutra"; -"coin_analytics.indicators.sell" = "Irá vender"; -"coin_analytics.indicators.strong_sell" = "Venda forte"; -"coin_analytics.indicators.overbought" = "Very Risky to Trade"; +"coin_analytics.indicators.sell" = "Vender"; +"coin_analytics.indicators.strong_sell" = "Venda Forte"; +"coin_analytics.indicators.overbought" = "Arriscado"; "coin_analytics.period" = "Período"; "coin_analytics.period.select_title" = "Selecionar Período"; "coin_analytics.period.1h" = "1 hora"; @@ -944,6 +1038,7 @@ Go to Settings - > %@ and allow access to the camera."; "coin_analytics.cex_volume" = "Volume CEX"; "coin_analytics.cex_volume_rank" = "Ranking do Volume CEX"; "coin_analytics.cex_volume_rank.description" = "Tokens classificados por volume de negociação para o token em corretoras centralizadas."; +"coin_analytics.cex_volume_rank.sorting_field" = "Volume"; "coin_analytics.cex_volume.info1" = "Volume total de trading para o token em corretoras centralizadas durante um período de 30 dias."; "coin_analytics.cex_volume.info2" = "Gráfico mostrando a variação no volume diário de negociação do token nas principais corretoras centralizadas durante um período de 1 ano."; "coin_analytics.cex_volume.info3" = "Classificação do Token baseada no volume de trading nas principais corretoras centralizadas durante um período de 30 dias."; @@ -952,6 +1047,7 @@ Go to Settings - > %@ and allow access to the camera."; "coin_analytics.dex_volume" = "Volume DEX"; "coin_analytics.dex_volume_rank" = "Ranking do Volume DEX"; "coin_analytics.dex_volume_rank.description" = "Tokens classificados por volume de negociação para o token em corretoras descentralizadas."; +"coin_analytics.dex_volume_rank.sorting_field" = "Volume"; "coin_analytics.dex_volume.info1" = "Volume total de trading para o token em corretoras centralizadas durante um período de 30 dias."; "coin_analytics.dex_volume.info2" = "Gráfico mostrando a variação no volume diário de negociação do token nas principais corretoras centralizadas durante um período de 1 ano."; "coin_analytics.dex_volume.info3" = "Classificação do Token baseada no volume de trading em principais corretoras centralizadas durante um período de 30 dias."; @@ -963,6 +1059,7 @@ Go to Settings - > %@ and allow access to the camera."; "coin_analytics.dex_liquidity" = "Liquidez DEX"; "coin_analytics.dex_liquidity_rank" = "Ranking de Liquidez DEX"; "coin_analytics.dex_liquidity_rank.description" = "Tokens classificados por liquidez disponível em corretoras descentralizadas."; +"coin_analytics.dex_liquidity_rank.sorting_field" = "Liquidez"; "coin_analytics.dex_liquidity.info1" = "Quantidade total de liquidez disponível para o token em corretoras descentralizadas."; "coin_analytics.dex_liquidity.info2" = "Gráfico mostrando a variação na liquidez disponível para o token nas principais corretoras descentralizadas durante o período de 1 ano."; "coin_analytics.dex_liquidity.info3" = "Lista de todos os tokens com base na liquidez disponível para o token nas principais exchanges descentralizadas."; @@ -974,6 +1071,7 @@ Go to Settings - > %@ and allow access to the camera."; "coin_analytics.active_addresses.30_day_unique_addresses" = "30 Dias Endereços Únicos"; "coin_analytics.active_addresses_rank" = "Ranking de Endereços Ativos"; "coin_analytics.active_addresses_rank.description" = "Tokens classificados pelo número de endereços exclusivos que realizam transações com o token."; +"coin_analytics.active_addresses_rank.sorting_field" = "Ativo"; "coin_analytics.active_addresses.info1" = "Número total de endereços ativos diários únicos em um período de 24 horas."; "coin_analytics.active_addresses.info2" = "Gráfico mostrando a variação na contagem diária de endereços ativos durante o período de 1 ano."; "coin_analytics.active_addresses.info3" = "Número total de endereços de blockchain únicos realizando transações com token em um período de 30 dias."; @@ -983,15 +1081,17 @@ Go to Settings - > %@ and allow access to the camera."; "coin_analytics.transaction_count" = "Contagem de transações"; "coin_analytics.transaction_count_rank" = "Ranking de Contador Tx"; "coin_analytics.transaction_count_rank.description" = "Tokens classificados por número de transações em uma blockchain."; +"coin_analytics.transaction_count_rank.sorting_field" = "Contagem"; "coin_analytics.transaction_count.info1" = "Número total de transações únicas na blockchain com token em um período de 30 dias."; "coin_analytics.transaction_count.info2" = "Gráfico mostrando a variação na contagem de transações no período de 1 ano."; "coin_analytics.transaction_count.info3" = "Ranking do token com base no número de transações com o período de 30 dias do token."; "coin_analytics.transaction_count.info4" = "Lista de todos os tokens classificados com base no número de transações com o token em intervalos de 24h / 7D / 1M."; "coin_analytics.transaction_count.info5" = "O número total de tokens transferidos sobre a blockchain ao longo do período de 30 dias."; -"coin_analytics.holders" = "Holders"; +"coin_analytics.holders" = "Detentores"; "coin_analytics.holders_rank" = "Classificação dos Holders"; "coin_analytics.holders_rank.description" = "Raking de tokens por endereços únicos guardando-os em várias blockchains."; +"coin_analytics.holders_rank.sorting_field" = "Detentores"; "coin_analytics.holders.info1" = "Número total de endereços únicos que contêm o token em várias blockchains."; "coin_analytics.holders.info2" = "As 10 principais carteiras que possuem o token em cada blockchain."; "coin_analytics.holders.tracked_blockchains" = "Blockchains rastreadas: Ethereum, Binance Smart Chain, Optimism, Arbitrum, Celo, Cronos, Avalanche, Fantom, Polygon"; @@ -1011,10 +1111,12 @@ Go to Settings - > %@ and allow access to the camera."; "coin_analytics.project_fee" = "Taxa do Projeto"; "coin_analytics.project_fee_rank" = "Classificação da Taxa do Projeto"; "coin_analytics.project_fee_rank.description" = "Tokens ranqueados de acordo com as taxas geradas pelos respectivos projetos. A forma como as taxas são coletadas varia do projeto para o projeto."; +"coin_analytics.project_fee_rank.sorting_field" = "Volume"; "coin_analytics.project_revenue" = "Receita do Projeto"; "coin_analytics.project_revenue_rank" = "Ranking da Receita do Projeto"; "coin_analytics.project_revenue_rank.description" = "Tokens classificados por receitas geradas para os holders através de mecanismos, ou seja, burns por staking ou token."; +"coin_analytics.project_revenue_rank.sorting_field" = "Receita"; "coin_analytics.other_data" = "Outros dados"; @@ -1200,6 +1302,8 @@ Go to Settings - > %@ and allow access to the camera."; "settings.rate_us" = "Avalie-Nos"; "settings.tell_friends" = "Indique para amigos"; "settings.contact_us" = "Fale Conosco"; +"settings.social_networks.label" = "Seja Unstoppable"; +"settings.social_networks.footer" = "Aprenda e domine cripto através de vídeos exclusivos. Conheça-nos informalmente. Seja o primeiro a ver as coisas em que trabalhamos."; // Settings -> Base Currency @@ -1417,9 +1521,7 @@ Go to Settings - > %@ and allow access to the camera."; // Settings -> About App "settings.about_app.title" = "Sobre o App"; -"settings.about_app.app_name" = "Carteira %@"; -"settings.about_app.description" = "A carteira %@ foi construída para aqueles que buscam investir e armazenar criptomoedas de forma privada e independente.\n\nÉ uma carteira não-custódia, peer-to-peer onde apenas o usuário tem controle sobre os fundos. Não coleta nenhum dado e mantém o usuário independente ao não bloquear os fundos do usuário para um aplicativo específico de carteira.\n\nA carteira %@ é totalmente de código aberto e qualquer um pode confirmar que o aplicativo funciona exatamente como afirma."; -"settings.about_app.whats_new" = "O que há de novo"; +"settings.about_app.app_version" = "Versão da Aplicação"; "settings.about_app.website" = "Site"; // Settings -> About App -> Contact @@ -1431,11 +1533,11 @@ Go to Settings - > %@ and allow access to the camera."; // Settings -> Privacy "settings.privacy" = "Privacidade"; -"settings.privacy.description" = "%@ não coleta nenhum dado ou usa ferramentas de análise que podem expor quaisquer dados sobre seus usuários. A carteira foi projetada para garantir um alto nível de privacidade para seus usuários."; -"settings.privacy.statement.user_data_storage" = "Os dados do usuário sempre permanecem em seu dispositivo."; +"settings.privacy.description" = "%@ não coleta dados pessoais que exponham suas informações privadas, como saldos de moedas ou endereços. Embora coletemos algumas estatísticas de uso da interface do usuário, é exclusivamente para entender nossa base de usuários e tendências de uso do aplicativo. Isso pode ser desativado se você desejar."; "settings.privacy.statement.data_usage" = "A carteira não coleta nenhum dado sobre os usuários."; -"settings.privacy.statement.data_privacy" = "A carteira não coleta nenhum dado sobre os usuários."; -"settings.privacy.statement.user_account" = "Não há nenhuma conta de usuário ou banco de dados mantendo os dados do usuário em algum lugar."; +"settings.privacy.statement.data_storage" = "Não há contas de usuário ou bancos de dados armazenando dados do usuário."; +"settings.privacy.statement.user_account" = "Se permitido, a carteira compartilhará os hábitos de uso do aplicativo com a equipe do Unstoppable. Isso é para entender quais recursos estão sendo usados (ou não) pelos nossos usuários. Sendo um aplicativo focado em privacidade, precisamos de alguma maneira de avaliar nossos esforços e sem isso não temos ideia se os recursos que construímos estão sendo utilizados ou não."; +"settings.privacy.allow" = "Compartilhar Dados de UI"; // Settings -> Appearance @@ -1446,21 +1548,25 @@ Go to Settings - > %@ and allow access to the camera."; "appearance.theme.dark" = "Escuro"; "appearance.theme.light" = "Claro"; -"appearance.tab_settings" = "Configurações de Abas"; "appearance.markets_tab" = "Guia de Mercados"; +"appearance.hide_markets" = "Esconder Mercados"; +"appearance.price_change" = "Mudança de Preço"; +"appearance.price_change.24h" = "24H"; +"appearance.price_change.1d" = "Meia-noite UTC"; + "appearance.launch_screen" = "Tela Inicial"; "appearance.launch_screen.auto" = "Automático"; "appearance.launch_screen.balance" = "Saldo"; "appearance.launch_screen.market_overview" = "Visão geral do mercado"; "appearance.launch_screen.watchlist" = "Lista de observação"; -"appearance.app_icon" = "Ícone do Aplicativo"; - -"appearance.balance_conversion" = "Conversão de saldo"; - +"appearance.balance_tab" = "Aba de Saldo"; +"appearance.hide_buttons" = "Ocultar Botões"; "appearance.balance_value" = "Valor Patrimonial"; -"appearance.balance_value.coin_value" = "Valor da Moeda"; -"appearance.balance_value.fiat_value" = "Valor Fiat"; +"appearance.balance_value.coin_fiat" = "Criptomoeda/Moeda Fiat"; +"appearance.balance_value.fiat_coin" = "Moeda Fiat / Criptomoeda"; + +"appearance.app_icon" = "Ícone do Aplicativo"; // Settings -> Contacts @@ -1523,7 +1629,7 @@ Go to Settings - > %@ and allow access to the camera."; // Key Types -"chart.time_duration.day" = "24h"; +"chart.time_duration.day" = "24H"; "chart.time_duration.week" = "7D"; "chart.time_duration.week2" = "2S"; "chart.time_duration.month" = "1M"; @@ -1959,7 +2065,6 @@ Go to Settings - > %@ and allow access to the camera."; "tron.send.fee.info" = "O custo estimado de envio de determinada transação na rede. (Sem excluir Energy, Bandwidth e Taxa de Ativação)"; "tron.send.resources_consumed.info" = "A bandwidth é a unidade que mede o tamanho dos bytes de transação armazenados no banco de dados blockchain. Quanto maior a transação, mais recursos de largura de banda serão consumidos.\n\nEnergy é a unidade que mede a quantidade de computação necessária pela máquina virtual TRON para realizar operações específicas na rede TRON.\n\nComo as transações de contrato inteligente exigem recursos de computação para executar, cada transação de contrato inteligente requer o pagamento da taxa de energy."; "tron.send.activation_fee.info" = "Transferir tokens TRX ou TRC-10 para um endereço de conta inativo ativará a conta."; -"tron.send.inactive_address" = "Este endereço não está ativo"; // Cex Coin Select diff --git a/UnstoppableWallet/UnstoppableWallet/ru.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/ru.lproj/Localizable.strings index 740bdd3ba6..bc6dbf4c2f 100644 --- a/UnstoppableWallet/UnstoppableWallet/ru.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/ru.lproj/Localizable.strings @@ -192,7 +192,7 @@ "coin_settings.title" = "Настройки блокчейна"; "coin_settings.bitcoin_cash_coin_type.title.type0" = "Legacy"; -"coin_settings.bitcoin_cash_coin_type.title.type145" = "CashAddress"; +"coin_settings.bitcoin_cash_coin_type.title.type145" = "CashAddress (рекомендованный)"; "sync_mode.hybrid" = "Hybrid"; "sync_mode.from_blockchain" = "Из блокчейна"; "blockchain_settings.description" = "Выберите формат адреса для получения средств. Правильный формат должен быть выбран при восстановлении существующего кошелька."; @@ -228,8 +228,9 @@ "extended_key.account_extended_private_key" = "Account Extended Private Key"; "extended_key.account_extended_public_key" = "Account Extended Public Key"; "extended_key.purpose" = "Purpose"; -"extended_key.blockchain" = "Blockchain"; +"extended_key.blockchain" = "Блокчейн"; "extended_key.account" = "Account"; +"extended_key.account.description" = "Это настройка для опытных пользователей. Если вы пытаетесь импортировать кошелек (через расширенный приватный ключ) или список транзакций (через расширенный публичный ключ), вам нужен аккаунт 0."; "extended_key.tap_to_show" = "Нажмите, чтобы показать extended private key"; // Backup @@ -320,7 +321,6 @@ "balance.downloading_blocks" = "Загрузка блоков"; "balance.scanning_blocks" = "Сканирование блоков"; "balance.enhancing_transactions" = "Улучшение транзакций"; -"wait_for_synchronization" = "Дождитесь синхронизации"; "balance.searching.count" = "%@ tx"; "balance.syncing_percent" = "Идет синхронизация... %@"; @@ -353,6 +353,8 @@ "balance.token.frozen" = "Frozen"; "balance.token.frozen.info.title" = "Frozen title"; "balance.token.frozen.info.description" = "Frozen Description Text"; +"balance.token.account.inactive.title" = "Аккаунт не активен"; +"balance.token.account.inactive.description" = "Новым кошелькам TRON требуется депозит как минимум 1 TRX для активации. Неактивные кошельки могут принимать и хранить токены, но не будут корректировать балансы до момента активации."; // Account switcher @@ -447,6 +449,8 @@ "send.transaction_inputs_outputs_info.shuffle.description" = "Порядок выхода транзакций меняется случайным образом в каждой транзакции. Иногда изменение может быть первым выводом, иногда - вторым. Если пользователь доверяет разработчику приложения, то рекомендуем использовать этот вариант."; "send.transaction_inputs_outputs_info.deterministic.title" = "2. Deterministic"; "send.transaction_inputs_outputs_info.deterministic.description" = "Существует широко признанный стандарт для упорядочивания выходов транзакции, известный как BIP69. В открытых кошельках этот стандарт обеспечивает, чтобы пользователи кошелька не должны были полагаться на то, как разработчики приложения реализуют упорядочивание выходов. Поскольку этот стандарт относительно новый, не многие кошельки его уже реализовали. В результате, на блокчейне в некоторых случаях можно узнать, отправлена ли транзакция из кошелька, который использует этот стандарт или нет."; +"send.select_all" = "Выбрать все"; +"send.unselect_all" = "Снять выделение"; "send.confirmation.title" = "Подтвердить"; "send.confirmation.you_send" = "Вы отправляете"; @@ -464,18 +468,20 @@ "send.confirmation.time_lock" = "TimeLock"; "send.confirmation.replace_by_fee" = "Изменить комиссию"; "send.confirmation.replaced_transactions" = "Замененные транзакции"; - +"send.confirmation.input" = "Ввод"; + +"send.confirmation.sync_failed" = "Ошибка синхронизации"; +"send.confirmation.invalid_data" = "Неверные данные"; +"send.confirmation.refresh" = "Обновить"; +"send.confirmation.please_wait" = "Пожалуйста, подождите"; +"send.confirmation.expires_in" = "Истекает через %@"; +"send.confirmation.expired" = "Истёк"; "send.confirmation.slide_to_send" = "Проведите для отправки"; "send.confirmation.sending" = "Отправка"; "send.confirmation.sent" = "Отправлено"; "send.confirmation.slide_to_approve" = "Проведите для разрешения"; -"send.confirmation.approving" = "Разрешение"; -"send.confirmation.approved" = "Разрешено"; - "send.confirmation.slide_to_revoke" = "Проведите чтобы отозвать"; -"send.confirmation.revoking" = "Отмена"; -"send.confirmation.revoked" = "Отменен"; "send.confirmation.slide_to_resend" = "Отправить повторно"; "send.confirmation.slide_to_cancel" = "Отменить транзакцию"; @@ -508,6 +514,7 @@ "send.lock_time" = "TimeLock"; "send.unspent_outputs" = "UTxOs"; +"send.unspent_outputs.description" = "Вручную выбрать UTxO для траты средств"; "send.unspent_outputs.send_to" = "Отправить"; "send.unspent_outputs.change" = "Изменить"; @@ -515,6 +522,14 @@ "approve.confirmation.you_revoke" = "Вы отменяете"; "approve.confirmation.spender" = "Покупатель"; +"send.enter_amount" = "Введите сумму"; +"send.enter_address" = "Введите адрес"; +"send.invalid_address" = "Неверный адрес"; +"send.token_not_enabled" = "Токен не включен"; +"send.token_syncing" = "Синхронизация токенов"; +"send.token_not_synced" = "Токен не синхронизирован"; +"send.insufficient_balance" = "Недостаточный баланс"; + // Donate "donate.list.title" = "Пожертвовать с помощью"; @@ -645,7 +660,6 @@ "swap.confirmation.slide_to_swap" = "Проведите для обмена"; "swap.confirmation.swapping" = "Обмен"; -"swap.confirmation.swapped" = "Обмен выполнен"; "swap.confirmation.refresh" = "Обновить"; "swap.confirmation.impact_too_high" = "%@ отключил действие обмена для этой сделки, потому что вы получаете крайне невыгодную цену. Это связано с крайне низкой ликвидностью.Если вы все равно хотите выполнить обмен, используйте вместо этого веб-сайт %@."; "swap.confirmation.impact_warning" = "Важно! Вы получаете крайне невыгодную цену из-за низкой ликвидности."; @@ -688,6 +702,51 @@ "market.defi_cap" = "Капитализация DeFi"; "market.defi_tvl" = "TVL в DeFi"; +"market.global.market_cap" = "Общая капитализация"; +"market.global.volume" = "24ч объем"; +"market.global.btc_dominance" = "Доминирование BTC"; +"market.global.etf_inflow" = "ETF приток"; +"market.global.tvl_in_defi" = "TVL в DeFi"; + +"market.tab.news" = "Новости"; +"market.tab.coins" = "Монеты"; +"market.tab.watchlist" = "Избранное"; +"market.tab.platforms" = "Платформы"; +"market.tab.pairs" = "Пары"; +"market.tab.sectors" = "Секторы"; + +"market.sort_by.title" = "Сортировать"; +"market.sort_by.manual" = "Вручную"; +"market.sort_by.highest_cap" = "Наивысшей кап."; +"market.sort_by.lowest_cap" = "Наименьшей кап."; +"market.sort_by.gainers" = "Gainers"; +"market.sort_by.losers" = "Losers"; +"market.sort_by.highest_volume" = "Наивысшему обьему"; +"market.sort_by.lowest_volume" = "Наименьшему объему"; + +"market.top_coins.title" = "Монеты"; +"market.top_coins" = "Топ %@"; + +"market.time_period.title" = "Период"; +"market.time_period.1d" = "1 День"; +"market.time_period.1w" = "1 Неделя"; +"market.time_period.2w" = "2 Недели"; +"market.time_period.1m" = "1 Месяц"; +"market.time_period.3m" = "3 Месяца"; +"market.time_period.6m" = "6 Месяцев"; +"market.time_period.1y" = "1 Год"; +"market.time_period.2y" = "2 Года"; +"market.time_period.5y" = "5 Лет"; +"market.time_period.1d.short" = "1Д"; +"market.time_period.1w.short" = "1Н"; +"market.time_period.2w.short" = "2Н"; +"market.time_period.1m.short" = "1M"; +"market.time_period.3m.short" = "3M"; +"market.time_period.6m.short" = "6M"; +"market.time_period.1y.short" = "1Г"; +"market.time_period.2y.short" = "2Г"; +"market.time_period.5y.short" = "5Г"; + "market.project_has_no_coin" = "У этого проекта нет токена"; "market.top.section.header.see_all" = "Посмотреть всё"; @@ -712,10 +771,10 @@ "market.top.title" = "Лучшие токены"; "market.top.description" = "Топ токенов по рыночной капитализации"; -"market.top.highest_cap" = "Наивысшая кап."; -"market.top.lowest_cap" = "Наименьшая кап."; -"market.top.highest_volume" = "Наивысший объем"; -"market.top.lowest_volume" = "Наименьший объем"; +"market.top.highest_cap" = "Наивысшей кап."; +"market.top.lowest_cap" = "Наименьшей кап."; +"market.top.highest_volume" = "Наивысшему обьему"; +"market.top.lowest_volume" = "Наименьшему объему"; "market.top.top_gainers" = "Показывают рост"; "market.top.top_losers" = "Теряют в цене"; "market.top.top_collections" = "Топ NFT коллекции"; @@ -724,6 +783,7 @@ "market.top.top_platforms" = "Топ платформы"; "market.top.protocols" = "Протоколы"; +"market.pairs.volume" = "Объем"; "top_pairs.title" = "Лучшие рыночные пары"; "top_pairs.description" = "Лучшие торговые пары по объему на каждой бирже"; @@ -732,6 +792,8 @@ "top_platform.title" = "%@ Экосистема"; "top_platform.description" = "Капитализация рынка всех протоколов на блокчейне %@ "; +"top_platform.total_cap" = "Общая капитализация"; + "market.search.recent" = "Недавние"; "market.search.popular" = "Популярные"; @@ -743,10 +805,22 @@ "market_discovery.not_found" = "Ничего не найдено"; "market_watchlist.empty.caption" = "У вас нет токенов в избранном."; +"market.watchlist.signals" = "Сигналы"; +"market.watchlist.empty" = "Список избранного пуст"; +"market.watchlist.signals.description" = "Нижеследующие сигналы основаны на технических индикаторах ценового диапазона Боллинджера и RSI за последние приблизительно 30 дней. Эти сигналы алгоритмические и могут часто изменяться."; +"market.watchlist.signals.strong_buy.description" = "Высокая уверенность в повышении цен"; +"market.watchlist.signals.buy.description" = "Вероятное увеличение цены в ближайшем будущем"; +"market.watchlist.signals.neutral.description" = "Нет чёткого тренда, рынок находится в равновесии"; +"market.watchlist.signals.sell.description" = "Вероятное снижение цен в ближайшем будущем"; +"market.watchlist.signals.strong_sell.description" = "Высокая вероятность снижения цены"; +"market.watchlist.signals.risky.description" = "Повышенный уровень риска, требует осторожности"; +"market.watchlist.signals.warning" = "Всегда помните о применении управления рисками, и обратите внимание, что это не финансовый совет."; +"market.watchlist.signals.turn_on" = "Включить"; "market.advanced_search.title" = "Фильтры"; "market.advanced_search.show_results" = "Показать результаты"; "market.advanced_search.empty_results" = "Сбросить результаты"; +"market.advanced_search.retry" = "Повторить"; "market.advanced_search.dex_description" = "Эта настройка применяется к токенам, торгуемым на Ethereum (Uniswap DEX) и Binance Smart Chain (Pancake DEX)."; "market.advanced_search.24h" = "24ч"; @@ -766,17 +840,10 @@ "market.advanced_search.liquidity" = "Ликвидность DEX"; "market.advanced_search.blockchains" = "Блокчейны"; -"market.advanced_search.technical_advice" = "Торговые сигналы"; +"market.advanced_search.signal" = "Торговый сигнал"; "market.advanced_search.price_period" = "Ценовой период"; "market.advanced_search.price_change" = "по изменению цены (%)"; -"market.advanced_search.technical_advice.risk_trade" = "Риск для торговли"; -"market.advanced_search.technical_advice.strong_buy" = "Активно покупать"; -"market.advanced_search.technical_advice.buy" = "Покупать"; -"market.advanced_search.technical_advice.neutral" = "Нейтрально"; -"market.advanced_search.technical_advice.sell" = "Продавать"; -"market.advanced_search.technical_advice.strong_sell" = "Активно продавать"; - "market.advanced_search.outperformed_btc" = "Обошел BTC"; "market.advanced_search.outperformed_eth" = "Обошел ETH"; "market.advanced_search.outperformed_bnb" = "Обошел BNB"; @@ -810,12 +877,12 @@ "market.advanced_search.more_50_b" = "> 50млрд"; "market.advanced_search.more_500_b" = "> 500млрд"; -"market.advanced_search.day" = "1 день"; -"market.advanced_search.week" = "1 неделя"; -"market.advanced_search.week2" = "2 недели"; -"market.advanced_search.month" = "1 месяц"; -"market.advanced_search.month6" = "6 месяцев"; -"market.advanced_search.year" = "1 год"; +"market.advanced_search.day" = "1 День"; +"market.advanced_search.week" = "1 Неделя"; +"market.advanced_search.week2" = "2 Недели"; +"market.advanced_search.month" = "1 Месяц"; +"market.advanced_search.month6" = "6 Месяцев"; +"market.advanced_search.year" = "1 Год"; "market.advanced_search.day.short" = "24ч"; "market.advanced_search.week.short" = "7Д"; @@ -832,10 +899,36 @@ "market.global.defi_cap.title" = "Капитализация DeFi"; "market.global.defi_cap.description" = "Общая рыночная стоимость проектов DeFi"; -"market.global.tvl_in_defi.title" = "TVL в DeFi"; -"market.global.tvl_in_defi.description" = "Всего заблокировано (TVL) в DeFi"; -"market.global.tvl_in_defi.multi_chain" = "Мультичейн"; -"market.global.tvl_in_defi.filter_by_chain" = "Сортировать по блокчейну"; +"market.etf.title" = "Общий чистый приток"; +"market.etf.description" = "Чистый приток (net inflow) ETF равен разнице между поступлениями и оттоками наличных средств."; +"market.etf.total_net_assets" = "Всего чистых активов"; +"market.etf.sort_by.highest_assets" = "Наивысшие активы"; +"market.etf.sort_by.lowest_assets" = "Наименьшие активы"; +"market.etf.sort_by.inflow" = "Приток"; +"market.etf.sort_by.outflow" = "Расход"; +"market.etf.period.all" = "Все"; + +"market.market_cap.title" = "Полная рын. кап."; +"market.market_cap.description" = "Общая рыночная стоимость всех криптовалют"; +"market.market_cap.market_cap" = "Рын. капитализация"; + +"market.tvl_in_defi.title" = "TVL в DeFi"; +"market.tvl_in_defi.description" = "Всего заблокировано (TVL) в DeFi"; +"market.tvl_in_defi.tvl" = "TVL"; +"market.tvl_in_defi.multi_chain" = "Мультичейн"; +"market.tvl_in_defi.filter_by_chain" = "Сортировать по блокчейну"; +"market.tvl_in_defi.filter.all" = "Все"; + +"market.volume.title" = "Объем"; +"market.volume.description" = "24-часовой объем крипторынка"; +"market.volume.volume" = "Объем"; + +"market.signal.risky" = "Рискованно"; +"market.signal.strong_buy" = "Активно покупать"; +"market.signal.buy" = "Покупать"; +"market.signal.neutral" = "Нейтрально"; +"market.signal.sell" = "Продавать"; +"market.signal.strong_sell" = "Активно продавать"; // Coin Page @@ -856,12 +949,13 @@ "coin_overview.genesis_date" = "Дата старта"; "coin_overview.trading_volume" = "Объем торговли"; -"coin_overview.roi.hour24" = "1 день"; -"coin_overview.roi.day7" = "1 неделя"; -"coin_overview.roi.day14" = "2 недели"; -"coin_overview.roi.day30" = "1 месяц"; +"coin_overview.roi.hour24" = "24 Часа"; +"coin_overview.roi.day1" = "1 День"; +"coin_overview.roi.day7" = "1 Неделя"; +"coin_overview.roi.day14" = "2 Недели"; +"coin_overview.roi.day30" = "1 Месяц"; "coin_overview.roi.day200" = "6 месяцев"; -"coin_overview.roi.year1" = "1 год"; +"coin_overview.roi.year1" = "1 Год"; "coin_overview.overview" = "Обзор"; "coin_overview.description_warning" = "Это описание, сгенерированное искусственным интеллектом на основе предоставленного справочного материала для данной криптовалюты. Оно может содержать ошибки."; @@ -919,13 +1013,13 @@ "coin_analytics.indicators.show_details" = "Показать детали"; "coin_analytics.indicators.summary" = "Общая оценка"; "coin_analytics.indicators.no_data" = "Нет данных"; -"coin_analytics.indicators.oversold" = "Очень рискованная торговля"; +"coin_analytics.indicators.oversold" = "Рискованно"; "coin_analytics.indicators.strong_buy" = "Активно покупать"; "coin_analytics.indicators.buy" = "Покупать"; "coin_analytics.indicators.neutral" = "Нейтрально"; "coin_analytics.indicators.sell" = "Продавать"; "coin_analytics.indicators.strong_sell" = "Активно продавать"; -"coin_analytics.indicators.overbought" = "Очень рискованная торговля"; +"coin_analytics.indicators.overbought" = "Рискованно"; "coin_analytics.period" = "Период"; "coin_analytics.period.select_title" = "Выберите период"; "coin_analytics.period.1h" = "1 час"; @@ -946,6 +1040,7 @@ "coin_analytics.cex_volume" = "Объем CEX"; "coin_analytics.cex_volume_rank" = "Рейтинг объема CEX"; "coin_analytics.cex_volume_rank.description" = "Торговый объем токена на централизованных биржах."; +"coin_analytics.cex_volume_rank.sorting_field" = "Объем"; "coin_analytics.cex_volume.info1" = "Общий объем торгов по токену на ведущих централизованных биржах за 30-дневный период."; "coin_analytics.cex_volume.info2" = "График, показывающий колебания дневного объема торговли токеном на ведущих централизованных биржах за 1 год."; "coin_analytics.cex_volume.info3" = "Рейтинг токена основан на объеме торговли на ведущих централизованных биржах за 30-дневный период."; @@ -954,6 +1049,7 @@ "coin_analytics.dex_volume" = "Объем DEX"; "coin_analytics.dex_volume_rank" = "Рейтинг объема DEX"; "coin_analytics.dex_volume_rank.description" = "Торговый объем токена на децентрализованных биржах."; +"coin_analytics.dex_volume_rank.sorting_field" = "Объем"; "coin_analytics.dex_volume.info1" = "Общий объем торгов по токену на ведущих децентрализованных биржах за 30-дневный период."; "coin_analytics.dex_volume.info2" = "График, показывающий колебания дневного объема торговли токеном на ведущих децентрализованных биржах за 1 год."; "coin_analytics.dex_volume.info3" = "Рейтинг токена основан на объеме торговли на ведущих децентрализованных биржах за 30-дневный период."; @@ -965,6 +1061,7 @@ "coin_analytics.dex_liquidity" = "Ликвидность DEX"; "coin_analytics.dex_liquidity_rank" = "Рейтинг ликвидности DEX"; "coin_analytics.dex_liquidity_rank.description" = "Рейтинг токенов основан по доступной ликвидности на децентрализованных биржах."; +"coin_analytics.dex_liquidity_rank.sorting_field" = "Ликвидность"; "coin_analytics.dex_liquidity.info1" = "Общая ликвидность, доступная в настоящий момент для токена на ведущих децентрализованных биржах."; "coin_analytics.dex_liquidity.info2" = "График, отражающий колебания доступной ликвидности для токена на ведущих децентрализованных биржах за 1 год."; "coin_analytics.dex_liquidity.info3" = "Список всех токенов, ранжированных по доступной ликвидности на ведущих децентрализованных биржах."; @@ -976,6 +1073,7 @@ "coin_analytics.active_addresses.30_day_unique_addresses" = "Уникальные адреса за 30 дней"; "coin_analytics.active_addresses_rank" = "Рейтинг активных адресов"; "coin_analytics.active_addresses_rank.description" = "Список токенов, ранжированных по количеству уникальных адресов транзакций с токеном."; +"coin_analytics.active_addresses_rank.sorting_field" = "Активно"; "coin_analytics.active_addresses.info1" = "Общее количество уникальных ежедневных активных адресов за 24 часа."; "coin_analytics.active_addresses.info2" = "График, показывающий вариацию количества ежедневных активных адресов в течение 1 года."; "coin_analytics.active_addresses.info3" = "Общее количество уникальных адресов блокчейна, с которых проводились транзакции с токеном за 30 дней."; @@ -985,6 +1083,7 @@ "coin_analytics.transaction_count" = "Количество транзакций"; "coin_analytics.transaction_count_rank" = "Рейтинг кол-ва транзакций"; "coin_analytics.transaction_count_rank.description" = "Токены ранжируются по количеству транзакций в блокчейне."; +"coin_analytics.transaction_count_rank.sorting_field" = "Кол-во"; "coin_analytics.transaction_count.info1" = "Общее количество уникальных транзакций блокчейна с токеном более 30 дней."; "coin_analytics.transaction_count.info2" = "График, отражающий колебания количества транзакций за 1 год."; "coin_analytics.transaction_count.info3" = "Рейтинг токена основан на количестве транзакций с токеном за 30-дневный период."; @@ -994,6 +1093,7 @@ "coin_analytics.holders" = "Держатели"; "coin_analytics.holders_rank" = "Рейтинг держателей"; "coin_analytics.holders_rank.description" = "Рейтинг токенов по уникальным адресам, содержащим их в нескольких блокчейнах."; +"coin_analytics.holders_rank.sorting_field" = "Держатели"; "coin_analytics.holders.info1" = "Общее количество уникальных адресов с токенами в различных блокчейнах."; "coin_analytics.holders.info2" = "Топ-10 кошельков с токенами в каждом блокчейне."; "coin_analytics.holders.tracked_blockchains" = "Отслеживаемые блокчейны: Ethereum, Binance Smart Chain, Optimism, Arbitrum, Celo, Cronos, Avalanche, Fantom, Polygon"; @@ -1013,10 +1113,12 @@ "coin_analytics.project_fee" = "Комиссия проекта"; "coin_analytics.project_fee_rank" = "Рейтинг оплаты за проект"; "coin_analytics.project_fee_rank.description" = "Токены ранжируются в соответствии с платами, полученными в рамках соответствующих проектов."; +"coin_analytics.project_fee_rank.sorting_field" = "Объем"; "coin_analytics.project_revenue" = "Доход от проекта"; "coin_analytics.project_revenue_rank" = "Рейтинг доходов"; "coin_analytics.project_revenue_rank.description" = "Токены ранжируются по пассивному доходу владельцев, получаемому через стекинг или сжигание токенов."; +"coin_analytics.project_revenue_rank.sorting_field" = "Доход"; "coin_analytics.other_data" = "Другие данные"; @@ -1202,6 +1304,8 @@ "settings.rate_us" = "Оцените нас"; "settings.tell_friends" = "Расскажите друзьям"; "settings.contact_us" = "Свяжитесь с нами"; +"settings.social_networks.label" = "Be Unstoppable"; +"settings.social_networks.footer" = "Изучайте и осваивайте криптовалюты с помощью эксклюзивных видео. Узнавайте о нас неформально. Будьте первыми, кто увидит то, над чем мы работаем."; // Settings -> Base Currency @@ -1419,9 +1523,7 @@ // Settings -> About App "settings.about_app.title" = "О приложении"; -"settings.about_app.app_name" = "%@ кошелек"; -"settings.about_app.description" = "Кошелек %@ создан для тех, кто ищет возможность инвестировать и хранить криптовалюты в частном и независимом режиме.\n\nЭто некастодиальный кошелек с принципом равноправия, в котором только пользователь имеет полный контроль над своими средствами. Он не собирает никаких данных и сохраняет независимость пользователя, не привязывая средства пользователя к конкретному приложению кошелька.\n\nКошелек %@ полностью открытого исходного кода, и любой может подтвердить, что приложение работает именно так, как утверждается."; -"settings.about_app.whats_new" = "Что нового"; +"settings.about_app.app_version" = "Версия приложения"; "settings.about_app.website" = "Веб-сайт"; // Settings -> About App -> Contact @@ -1433,11 +1535,11 @@ // Settings -> Privacy "settings.privacy" = "Приватность"; -"settings.privacy.description" = "%@ не собирает данные и не использует инструменты аналитики, которые могут раскрывать любые данные о своих пользователях. Кошелек разработан для обеспечения высокого уровня конфиденциальности для своих пользователей."; -"settings.privacy.statement.user_data_storage" = "Данные пользователя всегда остаются на устройстве пользователя."; +"settings.privacy.description" = "%@ не собирает персональные данные, которые могут раскрывать вашу частную информацию, такую как балансы монет или адреса. Мы собираем некоторую статистику использования интерфейса исключительно для понимания нашей пользовательской базы и тенденций использования приложения. Эта функция может быть отключена по вашему желанию."; "settings.privacy.statement.data_usage" = "Кошелек не собирает никаких данных о пользователях."; -"settings.privacy.statement.data_privacy" = "Кошелек не передает никаких данных о пользователях."; -"settings.privacy.statement.user_account" = "Нет учетных записей или баз данных, хранящих данные пользователя в другом месте."; +"settings.privacy.statement.data_storage" = "Здесь нет пользовательских аккаунтов или баз данных для хранения данных пользователей."; +"settings.privacy.statement.user_account" = "Если разрешено, кошелек будет делиться с командой Unstoppable информацией о привычках использования приложения. Это необходимо для понимания, какие функции используются (или нет) нашими пользователями. В качестве приложения, ориентированного на конфиденциальность, нам нужен способ оценить наши усилия, и без этого мы не сможем определить, используются ли построенные нами функции или нет."; +"settings.privacy.allow" = "Поделиться данными UI"; // Settings -> Appearance @@ -1448,21 +1550,25 @@ "appearance.theme.dark" = "Тёмная"; "appearance.theme.light" = "Светлая"; -"appearance.tab_settings" = "Настройки вкладки"; "appearance.markets_tab" = "Вкладка рынки"; +"appearance.hide_markets" = "Скрыть рынки"; +"appearance.price_change" = "по изменению цены (%)"; +"appearance.price_change.24h" = "24ч"; +"appearance.price_change.1d" = "Полночь по UTC"; + "appearance.launch_screen" = "Запуск экрана"; "appearance.launch_screen.auto" = "По умолчанию"; "appearance.launch_screen.balance" = "Баланс"; "appearance.launch_screen.market_overview" = "Обзор рынка"; "appearance.launch_screen.watchlist" = "Избранное"; -"appearance.app_icon" = "Иконка приложения"; - -"appearance.balance_conversion" = "Конвертация баланса"; - +"appearance.balance_tab" = "Вкладка баланс"; +"appearance.hide_buttons" = "Скрыть кнопки"; "appearance.balance_value" = "Значение баланса"; -"appearance.balance_value.coin_value" = "Токен значение"; -"appearance.balance_value.fiat_value" = "Фиатное значение"; +"appearance.balance_value.coin_fiat" = "Монета / Фиат"; +"appearance.balance_value.fiat_coin" = "Фиат / Монета"; + +"appearance.app_icon" = "Иконка приложения"; // Settings -> Contacts @@ -1480,7 +1586,7 @@ "contacts.contact.add_address" = "Добавить адрес"; "contacts.contact.delete" = "Удалить контакт"; "contacts.contact.address.blockchains" = "Блокчейны"; -"contacts.contact.address.blockchain" = "Blockchain"; +"contacts.contact.address.blockchain" = "Блокчейн"; "contacts.contact.address.delete_address" = "Удалить адрес"; "contacts.restore.restored" = "Восстановлено"; @@ -1529,8 +1635,8 @@ "chart.time_duration.week" = "7Д"; "chart.time_duration.week2" = "2Н"; "chart.time_duration.month" = "1M"; -"chart.time_duration.month3" = "3М"; -"chart.time_duration.halfyear" = "6М"; +"chart.time_duration.month3" = "3M"; +"chart.time_duration.halfyear" = "6M"; "chart.time_duration.year" = "1Г"; "chart.time_duration.year2" = "2Г"; "chart.time_duration.year5" = "5Г"; @@ -1612,7 +1718,7 @@ // Add Token "add_token.title" = "Добавить токен"; -"add_token.blockchain" = "Blockchain"; +"add_token.blockchain" = "Блокчейн"; "add_token.already_added" = "Этот токен уже есть в списке"; "add_token.invalid_contract_address" = "Недействительный адрес контракта"; "add_token.invalid_bep2_symbol" = "Неверный символ BEP2"; @@ -1833,7 +1939,7 @@ "fee_settings.errors.transaction_underpriced" = "Низкая комиссия за транзакцию"; "fee_settings.errors.tips_higher_than_max_fee" = "Слишком низкая макс. комиссия"; "fee_settings.errors.zero_amount.info" = "Невозможно отправить 0 TRX"; -"fee_settings.warning.risk_of_getting_stuck" = "Риски"; +"fee_settings.warning.risk_of_getting_stuck" = "Рискованно"; "fee_settings.warning.risk_of_getting_stuck.info" = "Обработка транзакции может отложиться на некоторое время, или транзакцию могут полностью отклонить."; "fee_settings.warning.overpricing" = "Слишком высокая комиссия"; "fee_settings.warning.overpricing.info" = "Установленная комиссия за транзакцию больше чем требуется для обработки этой транзакции."; @@ -1884,7 +1990,7 @@ "nft_asset.details.contract_address" = "Адрес контракта"; "nft_asset.details.token_id" = "ID токена"; "nft_asset.details.token_standard" = "Стандарт Токена"; -"nft_asset.details.blockchain" = "Blockchain"; +"nft_asset.details.blockchain" = "Блокчейн"; "nft_asset.links" = "Ссылки"; "nft_asset.links.website" = "Веб-сайт"; "nft_asset.options.save_to_photos" = "Сохранить в фото"; @@ -1962,7 +2068,6 @@ "tron.send.fee.info" = "Ориентировочные затраты на отправку данной транзакции по сети (без учёта energy, Bandwidth и комиссии за активацию) предполагаются"; "tron.send.resources_consumed.info" = "Bandwidth - это единица, которая измеряет размер байт транзакции, хранящихся в базе данных блокчейна. Чем больше транзакция, тем больше bandwidth будет потребляться.\n\nEnergy — это единица, измеряющая количество вычислений, требуемых виртуальной машиной TRON для выполнения конкретных операций в сети TRON.\n\nПоскольку транзакции смарт-контракта требуют выполнения вычислительных ресурсов, каждая сделка по контракту требует оплаты energy комиссии."; "tron.send.activation_fee.info" = "Передача токенов TRX или TRC-10 на адрес неактивного аккаунта активирует его."; -"tron.send.inactive_address" = "Этот адрес не активен"; // Cex Coin Select @@ -2026,7 +2131,7 @@ "transaction_filter.title" = "Фильтр"; "transaction_filter.description" = "Фильтрация по контактам работает на %@ блокчейнах. Ниже показаны подходящие контакты"; -"transaction_filter.blockchain" = "Blockchain"; +"transaction_filter.blockchain" = "Блокчейн"; "transaction_filter.all_blockchains" = "Все блокчейны"; "transaction_filter.coin" = "Монета"; "transaction_filter.all_coins" = "Все токены"; diff --git a/UnstoppableWallet/UnstoppableWallet/tr.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/tr.lproj/Localizable.strings index 92a3d24b2a..f144fec5ce 100644 --- a/UnstoppableWallet/UnstoppableWallet/tr.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/tr.lproj/Localizable.strings @@ -228,6 +228,7 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "extended_key.purpose" = "Purpose"; "extended_key.blockchain" = "Blockchain"; "extended_key.account" = "Account"; +"extended_key.account.description" = "Bu, ileri düzey kullanıcılar için bir ayar. Eğer cüzdanı (genişletilmiş özel anahtar aracılığıyla) veya işlem listesini (genişletilmiş genel anahtar aracılığıyla) içe aktarmaya çalışıyorsanız, hesap 0'a ihtiyacınız var."; "extended_key.tap_to_show" = "Extended private key göstermek için dokunun"; // Backup @@ -318,7 +319,6 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "balance.downloading_blocks" = "Blokları İndirme"; "balance.scanning_blocks" = "Tarama Blokları"; "balance.enhancing_transactions" = "İşlemleri Geliştirme"; -"wait_for_synchronization" = "Senkronizasyonu bekleyin"; "balance.searching.count" = "bakiye.araması.miktar"; "balance.syncing_percent" = "Senkronize ediliyor... %@"; @@ -339,8 +339,8 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "balance.token.locked" = "Kilitli"; "balance.token.locked.info.title" = "TimeLock"; "balance.token.locked.info.description" = "Gönderici bu fonları belirtilen tarihte sona erecek şekilde bir harcama kilidiyle gönderdi.\n\nEndişelenmeyin, alınan Bitcoin'ler zaten sizindir ancak kilitleme süresi sona erene kadar bunları Bitcoin ağında harcayamazsınız."; -"balance.token.not_relayed" = "Broadcasting"; -"balance.token.not_relayed.info.title" = "Broadcast"; +"balance.token.not_relayed" = "Yayınlama"; +"balance.token.not_relayed.info.title" = "Yayın"; "balance.token.not_relayed.info.description" = "Bu miktarı içeren işlem yayınlandı ancak henüz ağ tarafından kabul edilmedi"; "balance.token.processing" = "İşleniyor"; "balance.token.processing.info.title" = "İşlem Sayısı"; @@ -351,6 +351,8 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "balance.token.frozen" = "Frozen"; "balance.token.frozen.info.title" = "Frozen title"; "balance.token.frozen.info.description" = "Frozen Description Text"; +"balance.token.account.inactive.title" = "Hesap aktif değil"; +"balance.token.account.inactive.description" = "Yeni TRON cüzdanları aktif hale gelmek için en az 1 TRX yatırılmasını gerektirir. Aktif olmayan cüzdanlar, tokenları alabilir ve saklayabilir, ancak etkinleştirilene kadar bakiyeleri güncellemez."; // Account switcher @@ -445,10 +447,12 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "send.transaction_inputs_outputs_info.shuffle.description" = "İşlem çıktılarının sırası her işlemde rastgele belirlenir. Bazen değişim ilk çıktı olabilir, bazen ikinci olabilir. Bir kullanıcı uygulamanın geliştiricisine güveniyorsa bunu önerilen bir seçenek olarak değerlendirin."; "send.transaction_inputs_outputs_info.deterministic.title" = "2. Deterministic"; "send.transaction_inputs_outputs_info.deterministic.description" = "İşlem çıktılarını sipariş etmek için yaygın olarak kabul edilen bir standart vardır (BIP69 olarak bilinir). Açık kaynaklı (open-source) cüzdanlarda, bu standart, cüzdan kullanıcılarının uygulamanın geliştiricilerinin çıktıların sıralanmasını nasıl uyguladıklarına güvenmelerine gerek kalmamasını sağlar. Bu standart yeni olduğundan, henüz pek çok cüzdan bunu uygulamamıştır. Sonuç olarak, bir işlemin bu standardı kullanan bir cüzdandan gönderilip gönderilmediğini blok zincirinde görmek mümkündür."; +"send.select_all" = "Hepsini Seç"; +"send.unselect_all" = "Tüm Seçimleri Kaldır"; "send.confirmation.title" = "Onayla"; "send.confirmation.you_send" = "Gönderdiğiniz"; -"send.confirmation.transfer" = "Transfer"; +"send.confirmation.transfer" = "Aktar"; "send.confirmation.to" = "Alan"; "send.confirmation.own" = "Kendi"; "send.confirmation.contact_name" = "Kişi Adı"; @@ -462,18 +466,20 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "send.confirmation.time_lock" = "TimeLock"; "send.confirmation.replace_by_fee" = "Ücretle Değiştir"; "send.confirmation.replaced_transactions" = "Değiştirilmiş İşlemler"; - +"send.confirmation.input" = "Giriş"; + +"send.confirmation.sync_failed" = "Senkronizasyon Başarısız"; +"send.confirmation.invalid_data" = "Geçersiz Veri"; +"send.confirmation.refresh" = "Yenile"; +"send.confirmation.please_wait" = "Lütfen Bekleyin"; +"send.confirmation.expires_in" = "%@ içinde sona erer"; +"send.confirmation.expired" = "Süresi Dolmuş"; "send.confirmation.slide_to_send" = "Göndermek için Kaydırın"; "send.confirmation.sending" = "Gönderiliyor"; "send.confirmation.sent" = "Gönderildi"; "send.confirmation.slide_to_approve" = "Onaylamak İçin Kaydır"; -"send.confirmation.approving" = "Onaylayan"; -"send.confirmation.approved" = "Onayla"; - "send.confirmation.slide_to_revoke" = "İptal Etmek İçin Kaydır"; -"send.confirmation.revoking" = "İptal ediliyor"; -"send.confirmation.revoked" = "İptal et"; "send.confirmation.slide_to_resend" = "Yeniden Gönder"; "send.confirmation.slide_to_cancel" = "İşlemi İptal Et"; @@ -506,6 +512,7 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "send.lock_time" = "TimeLock"; "send.unspent_outputs" = "UTxOs"; +"send.unspent_outputs.description" = "Bakiyedeki fonları harcamak için UTxO'yu manuel olarak seçin"; "send.unspent_outputs.send_to" = "Göndermek için"; "send.unspent_outputs.change" = "Değiştir"; @@ -513,6 +520,14 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "approve.confirmation.you_revoke" = "Sen iptal et"; "approve.confirmation.spender" = "Harcayan"; +"send.enter_amount" = "Miktarı Girin"; +"send.enter_address" = "Adresini Giriniz"; +"send.invalid_address" = "Geçersiz Adres"; +"send.token_not_enabled" = "Jeton Etkin Değil"; +"send.token_syncing" = "Token Senkronizasyonu"; +"send.token_not_synced" = "Jeton Senkronize Edilmedi"; +"send.insufficient_balance" = "Yetersiz Bakiye"; + // Donate "donate.list.title" = "Bağış yapın"; @@ -643,7 +658,6 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "swap.confirmation.slide_to_swap" = "Değiştirmek için kaydır"; "swap.confirmation.swapping" = "Takas"; -"swap.confirmation.swapped" = "Takas Edildi"; "swap.confirmation.refresh" = "Yenile"; "swap.confirmation.impact_too_high" = "%@, son derece elverişsiz bir fiyat aldığınız için bu ticaret için takas işlemini devre dışı bıraktı. Bunun nedeni son derece düşük likiditedir.\nHala takas yapmak istiyorsanız lütfen bunun yerine %@ web sitesini kullanın."; "swap.confirmation.impact_warning" = "Önemli! Son derece olumsuz bir fiyat alıyorsunuz. Bunun nedeni son derece düşük likiditedir."; @@ -684,7 +698,52 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "market.total_market_cap" = "Total Cap"; "market.24h_volume" = "24 saat hacim"; "market.defi_cap" = "DeFi Cap"; -"market.defi_tvl" = "TVL in DeFi"; +"market.defi_tvl" = "DeFi'de TVL"; + +"market.global.market_cap" = "Toplam Piyasa Değeri"; +"market.global.volume" = "24s Hacim"; +"market.global.btc_dominance" = "BTC Hakimiyeti"; +"market.global.etf_inflow" = "ETF Girişi"; +"market.global.tvl_in_defi" = "DeFi'de TVL"; + +"market.tab.news" = "Haberler"; +"market.tab.coins" = "Coin'ler"; +"market.tab.watchlist" = "İzleme Listesi"; +"market.tab.platforms" = "Platformlar"; +"market.tab.pairs" = "Çiftler"; +"market.tab.sectors" = "Sektörler"; + +"market.sort_by.title" = "Sırala"; +"market.sort_by.manual" = "Manuel"; +"market.sort_by.highest_cap" = "En Yüksek Piyasa Değeri"; +"market.sort_by.lowest_cap" = "En Düşük Piyasa Değeri"; +"market.sort_by.gainers" = "Kazanlar"; +"market.sort_by.losers" = "Kaybedenler"; +"market.sort_by.highest_volume" = "En Yüksek Hacim"; +"market.sort_by.lowest_volume" = "En Düşük Hacim"; + +"market.top_coins.title" = "Coin'ler"; +"market.top_coins" = "En İyiler %@"; + +"market.time_period.title" = "Periyot"; +"market.time_period.1d" = "1 Gün"; +"market.time_period.1w" = "1 Hafta"; +"market.time_period.2w" = "2 Hafta"; +"market.time_period.1m" = "1 Ay"; +"market.time_period.3m" = "3 Ay"; +"market.time_period.6m" = "6 Ay"; +"market.time_period.1y" = "1 Yıl"; +"market.time_period.2y" = "2 Yıl"; +"market.time_period.5y" = "5 Yıl"; +"market.time_period.1d.short" = "1G"; +"market.time_period.1w.short" = "1H"; +"market.time_period.2w.short" = "2H"; +"market.time_period.1m.short" = "1A"; +"market.time_period.3m.short" = "3A"; +"market.time_period.6m.short" = "6A"; +"market.time_period.1y.short" = "1Y"; +"market.time_period.2y.short" = "2Y"; +"market.time_period.5y.short" = "5Y"; "market.project_has_no_coin" = "Bu proje bir koin içermiyor"; @@ -710,19 +769,20 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "market.top.title" = "En iyi Coinler"; "market.top.description" = "P. Değerine Göre En Iyi Coinler"; -"market.top.highest_cap" = "En Yüksek"; -"market.top.lowest_cap" = "En Düşük"; -"market.top.highest_volume" = "En Yüksek HacIm"; -"market.top.lowest_volume" = "En Düşük HacIm"; +"market.top.highest_cap" = "En Yüksek Piyasa Değeri"; +"market.top.lowest_cap" = "En Düşük Piyasa Değeri"; +"market.top.highest_volume" = "En Yüksek Hacim"; +"market.top.lowest_volume" = "En Düşük Hacim"; "market.top.top_gainers" = "En Kazançlılar"; "market.top.top_losers" = "Çok Kaybedenler"; "market.top.top_collections" = "En İyi NFT Koleksiyonlar"; "market.top.floor_price" = "Taban:"; -"market.top.top_market_pairs" = "Top Market Pairs"; +"market.top.top_market_pairs" = "En İyi Piyasa Çiftleri"; "market.top.top_platforms" = "En İyi Platformlar"; "market.top.protocols" = "Protokoller"; -"top_pairs.title" = "Top Market Pairs"; +"market.pairs.volume" = "Hacim"; +"top_pairs.title" = "En İyi Piyasa Çiftleri"; "top_pairs.description" = "Her borsada hacme göre en üstte olan işlem çiftleri"; "top_platforms.title" = "Platformlar Sıralaması"; @@ -730,6 +790,8 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "top_platform.title" = "%@ Ekosistemi"; "top_platform.description" = "%@ zincirindeki tüm protokollerin piyasa değeri"; +"top_platform.total_cap" = "Toplam Piyasa Değeri"; + "market.search.recent" = "Son"; "market.search.popular" = "Popüler"; @@ -741,10 +803,22 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "market_discovery.not_found" = "Sonuç bulunamadı"; "market_watchlist.empty.caption" = "İzleme listeniz boş."; +"market.watchlist.signals" = "Sinyaller"; +"market.watchlist.empty" = "İzleme listeniz boş"; +"market.watchlist.signals.description" = "Aşağıdaki sinyaller yaklaşık olarak son 30 gün boyunca Bollinger Bantları ve RSI teknik fiyat göstergelerine dayanmaktadır. Bu sinyaller algoritmiktir ve sık sık değişebilir."; +"market.watchlist.signals.strong_buy.description" = "Fiyat artışında yüksek güven"; +"market.watchlist.signals.buy.description" = "Yakın gelecekte muhtemel fiyat artışı"; +"market.watchlist.signals.neutral.description" = "Net bir trend yok, piyasa denge durumunda"; +"market.watchlist.signals.sell.description" = "Yakın gelecekte muhtemel fiyat düşüşü"; +"market.watchlist.signals.strong_sell.description" = "Fiyatın yüksek olasılıkla düşeceği"; +"market.watchlist.signals.risky.description" = "Yüksek risk seviyesi, dikkat gerektirir"; +"market.watchlist.signals.warning" = "Her zaman risk yönetimini uygulamayı unutmayın ve bu finansal tavsiye değildir."; +"market.watchlist.signals.turn_on" = "Açık Konum"; "market.advanced_search.title" = "Filtreler"; "market.advanced_search.show_results" = "Sonuçları Göster"; "market.advanced_search.empty_results" = "Boş Sonuçlar"; +"market.advanced_search.retry" = "Yeniden Dene"; "market.advanced_search.dex_description" = "Bu ayar, Ethereum (Uniswap DEX) ve Binance Smart Chain (Pancake DEX) üzerinde işlem gören tokenlar için geçerlidir."; "market.advanced_search.24h" = "24 sa"; @@ -764,24 +838,17 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "market.advanced_search.liquidity" = "DEX Likiditesi"; "market.advanced_search.blockchains" = "Blockchain'ler"; -"market.advanced_search.technical_advice" = "Ticaret Sinyalleri"; +"market.advanced_search.signal" = "Alım Satım Sinyali"; "market.advanced_search.price_period" = "Fiyat Periyodu"; "market.advanced_search.price_change" = "Fiyat Değişimi"; -"market.advanced_search.technical_advice.risk_trade" = "Ticaret Riski"; -"market.advanced_search.technical_advice.strong_buy" = "Kuvvetli Al"; -"market.advanced_search.technical_advice.buy" = "Al"; -"market.advanced_search.technical_advice.neutral" = "Nötr"; -"market.advanced_search.technical_advice.sell" = "Sat"; -"market.advanced_search.technical_advice.strong_sell" = "Kuvvetli Satış"; - "market.advanced_search.outperformed_btc" = "BTC'yi Geride Bıraktı"; "market.advanced_search.outperformed_eth" = "ETH'yi Geride Bıraktı"; "market.advanced_search.outperformed_bnb" = "BNB'yi Geride Bıraktı"; "market.advanced_search.price_close_to_ath" = "ATH'ye Yakın Fiyat"; "market.advanced_search.price_close_to_atl" = "ATL'ye Yakın Fiyat"; -"market.advanced_search.top" = "Top %d"; +"market.advanced_search.top" = "En İyiler %d"; "market.advanced_search.reset_all" = "Sıfırla"; "market.advanced_search.less_5_m" = "< 5M"; @@ -830,10 +897,36 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "market.global.defi_cap.title" = "DeFi Cap"; "market.global.defi_cap.description" = "DeFi projelerinin toplam piyasa değeri"; -"market.global.tvl_in_defi.title" = "TVL in DeFi"; -"market.global.tvl_in_defi.description" = "DeFi'deki Toplam Kilit Değer (TVL)"; -"market.global.tvl_in_defi.multi_chain" = "Multi-Chain"; -"market.global.tvl_in_defi.filter_by_chain" = "Zincir ile filtrele"; +"market.etf.title" = "Toplam Net Giriş"; +"market.etf.description" = "Bir ETF'nin net girişi, nakit girişlerinin çıkışlardan çıkarılmasıyla eşittir."; +"market.etf.total_net_assets" = "Toplam Net Varlıklar"; +"market.etf.sort_by.highest_assets" = "En Yüksek Varlıklar"; +"market.etf.sort_by.lowest_assets" = "En Düşük Varlıklar"; +"market.etf.sort_by.inflow" = "Giriş"; +"market.etf.sort_by.outflow" = "Çıkış"; +"market.etf.period.all" = "Hepsi"; + +"market.market_cap.title" = "Toplam Piyasa Değeri"; +"market.market_cap.description" = "Tüm kripto paraların toplam piyasa değeri"; +"market.market_cap.market_cap" = "Piyasa Değeri"; + +"market.tvl_in_defi.title" = "DeFi'de TVL"; +"market.tvl_in_defi.description" = "DeFi'deki Toplam Kilit Değer (TVL)"; +"market.tvl_in_defi.tvl" = "TVL"; +"market.tvl_in_defi.multi_chain" = "Çoklu Zincir"; +"market.tvl_in_defi.filter_by_chain" = "Zincir ile filtrele"; +"market.tvl_in_defi.filter.all" = "Hepsi"; + +"market.volume.title" = "Hacim"; +"market.volume.description" = "Kripto piyasa 24 saatlik işlem hacmi"; +"market.volume.volume" = "Hacim"; + +"market.signal.risky" = "Riskli"; +"market.signal.strong_buy" = "Kuvvetli Al"; +"market.signal.buy" = "Al"; +"market.signal.neutral" = "Tarafsız"; +"market.signal.sell" = "Sat"; +"market.signal.strong_sell" = "Kuvvetli Satış"; // Coin Page @@ -854,7 +947,8 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "coin_overview.genesis_date" = "Başlangıç ​​tarihi"; "coin_overview.trading_volume" = "İşlem Hacmi"; -"coin_overview.roi.hour24" = "1 Gün"; +"coin_overview.roi.hour24" = "24 Saat"; +"coin_overview.roi.day1" = "1 Gün"; "coin_overview.roi.day7" = "1 Hafta"; "coin_overview.roi.day14" = "2 Hafta"; "coin_overview.roi.day30" = "1 Ay"; @@ -875,56 +969,56 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "coin_overview.whitepaper" = "Whitepaper"; // Coin Page -> Analytics -"technical_advice.over.bought" = "overbought"; -"technical_advice.over.sold" = "oversold"; -"technical_advice.down" = "down"; -"technical_advice.up" = "up"; +"technical_advice.over.bought" = "aşırı alım"; +"technical_advice.over.sold" = "aşırı satım"; +"technical_advice.down" = "aşağı"; +"technical_advice.up" = "yukarı"; -"technical_advice.over.main" = "The actions with the asset are risky."; +"technical_advice.over.main" = "Varlık ile yapılan işlemler risklidir."; "technical_advice.over.indicators.signal_date" = "%@'den başlayarak"; -"technical_advice.over.indicators" = "The asset is outside the Bollinger Band channel and %@."; -"technical_advice.over.rsi" = " RSI = %@, This also indicates that the asset is %@."; -"technical_advice.over.advice" = " There might be a strong %@ward movement, so it's better to wait for the asset price to return to the channel."; +"technical_advice.over.indicators" = "Varlık, Bollinger Band kanalı dışında ve %@."; +"technical_advice.over.rsi" = " RSI = %@, Bu aynı zamanda varlığın %@ olduğunu gösterir."; +"technical_advice.over.advice" = " Güçlü bir %@ yönlü hareket olabilir, bu yüzden varlık fiyatının kanala dönmesini beklemek daha iyi olur."; -"technical_advice.strong.indicators" = "The asset was %@, but now it has returned to the Bollinger Band channel. This indicates a possible trend reversal."; -"technical_advice.strong.rsi" = " Meanwhile, the RSI is %@, which still indicates that it is %@."; -"technical_advice.strong.advice" = " This could be a very strong signal to enter the market. Keep in mind that there may be several attempts of %@ward movement after returning to the channel, so do not forget about risk management."; +"technical_advice.strong.indicators" = "Varlık %@ idi, ancak şimdi Bollinger Band kanalına geri döndü. Bu, olası bir trend dönüşünü gösterir."; +"technical_advice.strong.rsi" = " Bu arada, RSI %@, hala %@ olduğunu gösteriyor."; +"technical_advice.strong.advice" = " Bu piyasaya giriş için çok güçlü bir sinyal olabilir. Kanala dönüşten sonra %@ yönlü hareketin birkaç denemesi olabileceğini unutmayın, bu yüzden risk yönetimini unutmayın."; -"technical_advice.stable.rsi" = " Meanwhile, the RSI is %@, which also indicates a trend reversal (RSI crossed the boundary at 70%)."; -"technical_advice.stable.advice" = "The price is returning to neutral levels, however, there is still potential for upward movement. Keep in mind that RSI = 50 and the middle of the Bollinger Bands are strong resistances and possible trend reversal points. Do not forget about risk management."; +"technical_advice.stable.rsi" = " Bu arada, RSI %@, bu da bir trend dönüşünü gösteriyor (RSI 70 sınırını geçti)."; +"technical_advice.stable.advice" = "Fiyat nötr seviyelere geri dönüyor, ancak hala yukarı yönlü hareket potansiyeli var. RSI = 50 ve Bollinger Bantlarının ortası güçlü dirençler ve olası trend dönüş noktalarıdır. Risk yönetimini unutmayın."; -"technical_advice.neutral.rsi" = "RSI = %@ also confirms the absence of a strong trend."; -"technical_advice.neutral.indicators" = "The asset was in the overbought/oversold zone, but at the moment the price has returned to the Bollinger Band channel in the neutral zone. The RSI is %@ also confirms the absence of a strong trend, so overall the asset price is moving towards averaging and further movement is possible in any direction."; -"technical_advice.neutral.advice" = " In general, the asset price is moving towards averaging and further movement is possible in any direction."; +"technical_advice.neutral.rsi" = "RSI = %@ ayrıca güçlü bir trendin olmadığını doğruluyor."; +"technical_advice.neutral.indicators" = "Varlık aşırı alım/aşırı satım bölgesindeydi, ancak şu anda fiyat nötr bölgedeki Bollinger Band kanalına geri döndü. RSI = %@ ayrıca güçlü bir trendin olmadığını doğruluyor, bu nedenle genel olarak varlık fiyatı ortalama yapmaya doğru hareket ediyor ve daha fazla hareket herhangi bir yönde mümkün olabilir."; +"technical_advice.neutral.advice" = " Genel olarak, varlık fiyatı ortalama hareket ediyor ve daha fazla hareket herhangi bir yönde mümkün olabilir."; -"technical_advice.other.title" = "Please note:"; +"technical_advice.other.title" = "Lütfen dikkate alın:"; -"technical_advice.ema.above" = "above"; -"technical_advice.ema.below" = "below"; -"technical_advice.ema.growth" = "growth"; -"technical_advice.ema.decrease" = "decrease"; -"technical_advice.ema.advice" = "EMA 200. Determines the overall sentiment and trend. The daily price of the asset is located %@ the EMA (%@). This means that globally the asset is set for %@."; +"technical_advice.ema.above" = "yukarıda"; +"technical_advice.ema.below" = "aşağıda"; +"technical_advice.ema.growth" = "büyüme"; +"technical_advice.ema.decrease" = "azalış"; +"technical_advice.ema.advice" = "EMA 200. Genel duygu ve trendi belirler. Varlığın günlük fiyatı EMA (%@) 'nin %@'den geçtiği yerde bulunuyor. Bu, genel olarak varlığın %@ olduğu anlamına gelir."; -"technical_advice.macd.positive" = "above"; -"technical_advice.macd.negative" = "below"; -"technical_advice.macd.advice" = "MACD. Assesses the strength of the trend considering the average price change. The daily value of the histogram is %@ (%@). The price of the asset globally may move %@."; +"technical_advice.macd.positive" = "yukarıda"; +"technical_advice.macd.negative" = "aşağıda"; +"technical_advice.macd.advice" = "MACD. Ortalama fiyat değişimini dikkate alarak trendin gücünü değerlendirir. Histogramın günlük değeri %@ (%@). Varlığın fiyatı genel olarak %@ hareket edebilir."; "coin_analytics.indicators.title" = "Teknik Göstergeleri"; -"coin_analytics.indicators.disclaimer" = "Always remember to apply risk management, and note that this is not financial advice."; -"coin_analytics.indicators.info.title" = "Technical Indicators"; -"coin_analytics.indicators.info.description" = "We use the Bollinger Bands + RSI strategy to determine trading signals. All calculations are based on daily candlesticks and provide advice for a moderately long term. The essence of the strategy is that the asset price should reach an extreme, breaking out of the Bollinger Bands channel, and the RSI should be in the overbought/oversold zone. After the price returns to the channel, there is a high probability of the price returning to the mean values or attempting to break the channel from the other side. Note that the strategy may give several false signals during strong market movements before a correct signal appears.\n\nPlease remember that it is very important to apply risk management to trading and remember to cut losses if the market situation changes! "; -"coin_analytics.indicators.hide_details" = "Hide Details"; -"coin_analytics.indicators.show_details" = "Show Details"; +"coin_analytics.indicators.disclaimer" = "Her zaman risk yönetimini uygulamayı unutmayın ve bu finansal tavsiye değildir."; +"coin_analytics.indicators.info.title" = "Teknik Göstergeleri"; +"coin_analytics.indicators.info.description" = "Bollinger Bantları + RSI stratejisini kullanarak işlem sinyallerini belirliyoruz. Tüm hesaplamalar günlük mum grafiklerine dayanmakta ve ortalama uzun vadeli bir strateji için tavsiyeler sunmaktadır. Stratejinin özü, varlık fiyatının Bollinger Bantları kanalından çıkarak aşırı seviyelere ulaşması ve RSI'nin aşırı alım/aşırı satım bölgesinde olması gerektiğidir. Fiyat kanala geri döndükten sonra, fiyatın ortalama değerlere geri dönme veya kanalı diğer taraftan kırmaya yönelik yüksek bir olasılığı vardır. Stratejinin güçlü piyasa hareketleri sırasında birkaç yanlış sinyal verebileceğini unutmayın, doğru bir sinyal ortaya çıkmadan önce.\n\nLütfen ticarette risk yönetimini uygulamanın çok önemli olduğunu ve piyasa durumu değişirse kayıpları kesmeyi unutmayın! "; +"coin_analytics.indicators.hide_details" = "Detayları Gizle"; +"coin_analytics.indicators.show_details" = "Detayları Göster"; "coin_analytics.indicators.summary" = "Kısa özet"; "coin_analytics.indicators.no_data" = "Veri Yok"; -"coin_analytics.indicators.oversold" = "Very Risky to Trade"; -"coin_analytics.indicators.strong_buy" = "Strong Buy"; -"coin_analytics.indicators.buy" = "Buy"; -"coin_analytics.indicators.neutral" = "Nötr"; -"coin_analytics.indicators.sell" = "Sell"; -"coin_analytics.indicators.strong_sell" = "Strong Sell"; -"coin_analytics.indicators.overbought" = "Very Risky to Trade"; -"coin_analytics.period" = "Period"; +"coin_analytics.indicators.oversold" = "Riskli"; +"coin_analytics.indicators.strong_buy" = "Kuvvetli Al"; +"coin_analytics.indicators.buy" = "Al"; +"coin_analytics.indicators.neutral" = "Tarafsız"; +"coin_analytics.indicators.sell" = "Sat"; +"coin_analytics.indicators.strong_sell" = "Kuvvetli Satış"; +"coin_analytics.indicators.overbought" = "Riskli"; +"coin_analytics.period" = "Periyot"; "coin_analytics.period.select_title" = "Periyot Seç"; "coin_analytics.period.1h" = "1 saat"; "coin_analytics.period.4h" = "4 saat"; @@ -936,7 +1030,7 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "coin_analytics.not_available" = "This project has no analytical data"; -"coin_analytics.technical_indicators" = "Technical Indicators"; +"coin_analytics.technical_indicators" = "Teknik Göstergeleri"; "coin_analytics.technical_indicators.info1" = "Özet: Bu, bir varlığın teknikleri ile ilgili çeşitli teknik göstergeleri ve zaman dilimlerini göz önünde bulunduran genel bir bakıştır. Bu göstergelere dayalı olarak bir uzlaşı görüşü sunar (Al, Sat veya Tarafsız)."; "coin_analytics.technical_indicators.info2" = "Hareketli Ortalamalar (MA): Bunlar, eğilimi takip eden bir gösterge oluşturmak için fiyat verilerini düzleştiren yaygın olarak kullanılan teknik göstergelerdir. Belirli bir zaman dilimi içinde ortalama fiyatı gösterirler. Birden fazla MA türü bulunur:\n\nBasit Hareketli Ortalama (SMA): Bu, genellikle kapanış fiyatları gibi belirli bir fiyat aralığının ortalamasını, o aralıktaki dönem sayısına göre hesaplar.\n\nÜstel Hareketli Ortalama (EMA): Bu, daha yeni fiyatlara daha fazla ağırlık verir ve bu nedenle daha yeni fiyat değişikliklerine daha hızlı tepki verir."; "coin_analytics.technical_indicators.info3" = "Osilatörler: Bunlar, zaman içinde bir bant içinde (bir merkez çizginin üstünde ve altında veya belirlenen seviyeler arasında) dalgalanan teknik göstergelerdir. Piyasada aşırı alım ve aşırı satım koşullarını belirlemeye yardımcı olmak için tasarlanmışlardır. İşte bazı yaygın osilatörler:\n\nGüç Endeksi (RSI - Relative Strength Index): Bu, fiyat hareketlerinin hızını ve değişimini ölçer. Genellikle aşırı alım veya aşırı satım koşullarını belirlemek için kullanılır.\n\nHareketli Ortalama Yakınsama Sapma (MACD - Moving Average Convergence Divergence): Bu, potansiyel alım ve satış sinyallerini belirlemek için kullanılır. Teknik sinyalleri tetiklerken sinyal çizgisinin üzerinden (alış) veya altından (satış) geçtiğinde çalışır."; @@ -944,6 +1038,7 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "coin_analytics.cex_volume" = "CEX Hacmi"; "coin_analytics.cex_volume_rank" = "CEX Hacim Sıralaması"; "coin_analytics.cex_volume_rank.description" = "Merkezi borsalarda token için işlem hacmine göre sıralanan tokenlar."; +"coin_analytics.cex_volume_rank.sorting_field" = "Hacim"; "coin_analytics.cex_volume.info1" = "Total trading volume for the token on leading centralized exchanges over 30-day period."; "coin_analytics.cex_volume.info2" = "Önde gelen merkezi borsalarda token için günlük işlem hacmindeki değişimi gösteren 1 yıllık döneme ait grafik."; "coin_analytics.cex_volume.info3" = "Token's rank based on trading volume on leading centralized exchanges over 30-day period."; @@ -952,6 +1047,7 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "coin_analytics.dex_volume" = "DEX Hacmi"; "coin_analytics.dex_volume_rank" = "DEX Hacim Sıralaması"; "coin_analytics.dex_volume_rank.description" = "Merkezi olmayan borsalarda token için işlem hacmine göre sıralanan tokenlar."; +"coin_analytics.dex_volume_rank.sorting_field" = "Hacim"; "coin_analytics.dex_volume.info1" = "Total trading volume for the token on leading centralized exchanges over 30-day period."; "coin_analytics.dex_volume.info2" = "Bir yıllık süre zarfında önde gelen merkezi olmayan borsalarda token için günlük işlem hacmindeki değişikliği gösteren grafik."; "coin_analytics.dex_volume.info3" = "30 günlük süre içinde önde gelen merkezi olmayan borsalarda işlem hacmine göre token sıralaması."; @@ -963,6 +1059,7 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "coin_analytics.dex_liquidity" = "DEX Likiditesi"; "coin_analytics.dex_liquidity_rank" = "DEX Likidite Sıralaması"; "coin_analytics.dex_liquidity_rank.description" = "Merkezi olmayan borsalarda mevcut likiditeye göre sıralanan tokenlar."; +"coin_analytics.dex_liquidity_rank.sorting_field" = "Likidite"; "coin_analytics.dex_liquidity.info1" = "Önde gelen merkezi olmayan borsalarda token için şu anda mevcut olan toplam likidite."; "coin_analytics.dex_liquidity.info2" = "Bir yıllık süre zarfında önde gelen merkezi olmayan borsalarda token için mevcut likiditedeki değişikliği gösteren grafik."; "coin_analytics.dex_liquidity.info3" = "Önde gelen merkezi olmayan borsalarda token için mevcut likiditeye göre sıralanan tüm tokenlerin listesi."; @@ -974,6 +1071,7 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "coin_analytics.active_addresses.30_day_unique_addresses" = "30 Günlük Benzersiz Adresler"; "coin_analytics.active_addresses_rank" = "Aktif Adresler Sıralaması"; "coin_analytics.active_addresses_rank.description" = "Token ile işlem yapan benzersiz adreslerin sayısına göre sıralanan tokenlar."; +"coin_analytics.active_addresses_rank.sorting_field" = "Aktif"; "coin_analytics.active_addresses.info1" = "Total number of unique daily active addresses over 24-hour period."; "coin_analytics.active_addresses.info2" = "Bir yıllık süre zarfında günlük aktif adres sayısındaki değişimi gösteren grafik."; "coin_analytics.active_addresses.info3" = "Total number of unique blockchain addresses transacting with token over 30-day period."; @@ -983,6 +1081,7 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "coin_analytics.transaction_count" = "İşlem Sayısı"; "coin_analytics.transaction_count_rank" = "İşlem Sayısı Sıralaması"; "coin_analytics.transaction_count_rank.description" = "Tokens ranked by number of transactions on a blockchain."; +"coin_analytics.transaction_count_rank.sorting_field" = "Sayı"; "coin_analytics.transaction_count.info1" = "Total number of unique blockchain transactions with token over 30-day period."; "coin_analytics.transaction_count.info2" = "Bir yıllık süre zarfında işlem sayısındaki değişimi gösteren grafik."; "coin_analytics.transaction_count.info3" = "Token's rank based on the number of transactions with the token 30-day period."; @@ -992,6 +1091,7 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "coin_analytics.holders" = "Sahipler"; "coin_analytics.holders_rank" = "Sahipler Sıralaması"; "coin_analytics.holders_rank.description" = "Çoklu blockchain'lerde onları tutan benzersiz adreslere göre tokenleri sıralama."; +"coin_analytics.holders_rank.sorting_field" = "Sahipler"; "coin_analytics.holders.info1" = "Çeşitli blockchain'lerde tokeni tutan benzersiz adreslerin toplam sayısı."; "coin_analytics.holders.info2" = "Her blok zincirinde tokenı tutan en iyi 10 cüzdan."; "coin_analytics.holders.tracked_blockchains" = "İzlenen blok zincirleri: Ethereum, Binance Smart Chain, İyimserlik, Arbitrum, Celo, Cronos, Avalanche, Fantom, Polygon"; @@ -1000,21 +1100,23 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "coin_analytics.holders.see_all" = "Hepsini Gör"; "coin_analytics.project_tvl" = "Proje TVL"; -"coin_analytics.tvl_ratio" = "M.Cap / TVL Ratio"; +"coin_analytics.tvl_ratio" = "Piyasa Değeri / TVL Oranı"; "coin_analytics.project_tvl.info_title" = "Proje TVL (Toplam Değer)"; "coin_analytics.project_tvl.info1" = "Total-Value-Locked (or Assets Under Management) in the project's smart contracts."; "coin_analytics.project_tvl.info2" = "Projenin akıllı sözleşmelerindeki Toplam Kilit Değerdeki değişimi gösteren 1 yıllık döneme ait grafik."; "coin_analytics.project_tvl.info3" = "Token's rank based on current Total-Value-Locked."; "coin_analytics.project_tvl.info4" = "Mevcut Toplam Kilit Değere göre sıralanan tüm tokenların listesi."; -"coin_analytics.project_tvl.info5" = "Market Cap / TVL ratio for the project."; +"coin_analytics.project_tvl.info5" = "Projenin Piyasa Değeri / TVL oranı."; "coin_analytics.project_fee" = "Proje Ücreti"; "coin_analytics.project_fee_rank" = "Proje Ücret Sıralaması"; "coin_analytics.project_fee_rank.description" = "Tokens ranked according to fees generated by respective projects. The way fees are collected varies from project to project."; +"coin_analytics.project_fee_rank.sorting_field" = "Hacim"; "coin_analytics.project_revenue" = "Proje Geliri"; "coin_analytics.project_revenue_rank" = "Proje Gelir Sıralaması"; "coin_analytics.project_revenue_rank.description" = "Tokens ranked by revenue generated for holders via mechanisms i.e. staking or token burns."; +"coin_analytics.project_revenue_rank.sorting_field" = "Gelir"; "coin_analytics.other_data" = "Diğer Veriler"; @@ -1054,14 +1156,14 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "coin_analytics.30_day_rank" = "Son 30 Gün Sıralaması"; "coin_analytics.30_day_volume" = "Son 30 Gün Hacmi"; -"coin_analytics.analysis.title" = "Smart Contract Analysis"; +"coin_analytics.analysis.title" = "Akıllı Kontrat Analizi"; "coin_analytics.analysis.footer" = "De.Fi tarafından desteklenmektedir"; -"coin_analytics.analysis.high_risk_items" = "High Risk Items"; -"coin_analytics.analysis.medium_risk_items" = "Medium Risk Items"; -"coin_analytics.analysis.attention_required" = "Attention Required"; -"coin_analytics.analysis.token_detectors" = "Token Detectors"; -"coin_analytics.analysis.general_detectors" = "General Detectors"; -"coin_analytics.analysis.issues" = "Issues: %@"; +"coin_analytics.analysis.high_risk_items" = "Yüksek Riskli Ögeler"; +"coin_analytics.analysis.medium_risk_items" = "Orta Riskli Ögeler"; +"coin_analytics.analysis.attention_required" = "İlgilenmeniz Gerekiyor"; +"coin_analytics.analysis.token_detectors" = "Token Tespit Ediciler"; +"coin_analytics.analysis.general_detectors" = "Genel Tespit Ediciler"; +"coin_analytics.analysis.issues" = "Sorunlar: %@"; // Coin Page -> Markets @@ -1090,7 +1192,7 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "chart_indicators.settings.ma.description" = "EMA, SMA ve WMA, teknik analizde kullanılan hareketli ortalamalardır:\n\nEMA, daha hızlı tepkiler için yakın dönem fiyatlarına vurgu yapar.\nSMA, genel bir trend görünümü için fiyat verilerini ortalayır.\nWMA, yakın dönem verilerini lineer olarak ağırlıklandırarak hassasiyeti ve gürültü azaltmayı dengelemektedir"; "chart_indicators.settings.ma.type_title" = "Tür"; -"chart_indicators.settings.ma.period_title" = "Period"; +"chart_indicators.settings.ma.period_title" = "Periyot"; "chart_indicators.settings.rsi.title" = "RSI"; "chart_indicators.settings.rsi.description" = "Göreceli Güç Endeksi (RSI), fiyat hızını ve değişimini ölçen, aşırı alım (70 üzeri) veya aşırı satım (30 altı) piyasa koşullarını tanımlayan bir momentum osilatörüdür. Ayrıca fiyat ters dönüşlerini değişimler yoluyla tespit edebilir."; @@ -1204,6 +1306,8 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "settings.rate_us" = "Bizi Değerlendirin"; "settings.tell_friends" = "Arkadaşlarına bahset"; "settings.contact_us" = "Bize Ulaşın"; +"settings.social_networks.label" = "Be Unstoppable"; +"settings.social_networks.footer" = "Özel videolar aracılığıyla kripto para birimini öğrenin ve ustalaşın. Bizi resmi olmayan bir şekilde tanıyın. Üzerinde çalıştığımız şeyleri ilk gören siz olun."; // Settings -> Base Currency @@ -1421,9 +1525,7 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; // Settings -> About App "settings.about_app.title" = "Uygulama Hakkında"; -"settings.about_app.app_name" = "%@ Cüzdanı"; -"settings.about_app.description" = "%@ cüzdan, kripto para birimlerine özel ve bağımsız bir şekilde yatırım yapmak ve depolamak isteyenler için tasarlanmıştır.\n\nBu, fonlar üzerinde yalnızca kullanıcının kontrolüne sahip olduğu, velayet gerektirmeyen, eşler arası bir cüzdandır. Herhangi bir veri toplamaz ve kullanıcının fonlarını belirli bir cüzdan uygulamasına kilitlemeyerek kullanıcıyı bağımsız tutar.\n\n%@ cüzdan tamamen açık kaynaklıdır ve herkes uygulamanın tam olarak iddia ettiği gibi çalıştığını onaylayabilir."; -"settings.about_app.whats_new" = "Yenilikler"; +"settings.about_app.app_version" = "Uygulama Sürümü"; "settings.about_app.website" = "Website"; // Settings -> About App -> Contact @@ -1435,11 +1537,11 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; // Settings -> Privacy "settings.privacy" = "Gizlilik"; -"settings.privacy.description" = "%@ herhangi bir veri toplamaz veya kullanıcıları hakkında herhangi bir veriyi ifşa edebilecek analiz araçları kullanmaz. Сüzdanı kullanıcıları için yüksek düzeyde gizlilik sağlamak üzere tasarlanmıştır."; -"settings.privacy.statement.user_data_storage" = "Kullanıcı verileri her zaman kullanıcının cihazında kalır."; +"settings.privacy.description" = "%@, coin bakiyelerinizi veya adreslerinizi açığa çıkaran kişisel verileri toplamaz. Biraz kullanıcı arayüzü kullanım istatistikleri topluyoruz, ancak bunlar yalnızca kullanıcı tabanımızı ve uygulama kullanım eğilimlerini anlamak içindir. İsterseniz bunu devre dışı bırakabilirsiniz."; "settings.privacy.statement.data_usage" = "Bu cüzdan, kullanıcılar hakkında herhangi bir veri toplamaz."; -"settings.privacy.statement.data_privacy" = "Bu cüzdan, kullanıcılar hakkında herhangi bir veri paylaşmaz."; -"settings.privacy.statement.user_account" = "Kullanıcı verilerini herhangi bir yerde kaida alan hesaplar veya veritabanları yoktur."; +"settings.privacy.statement.data_storage" = "Kullanıcı hesapları veya kullanıcı verilerini depolayan veritabanları yoktur."; +"settings.privacy.statement.user_account" = "İzin verilirse, cüzdan, Unstoppable ekibiyle uygulama kullanım alışkanlıklarını paylaşacak. Bu, kullanıcılarımızın hangi özellikleri kullandığını (veya kullanmadığını) anlamak için. Gizliliğe odaklanan bir uygulama olarak, çabalarımızı değerlendirmenin bir yolu olmalı ve bu olmadan, oluşturduğumuz özelliklerin kullanılıp kullanılmadığını bilmiyoruz."; +"settings.privacy.allow" = "UI Verilerini Paylaş"; // Settings -> Appearance @@ -1450,21 +1552,25 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "appearance.theme.dark" = "Karanlık"; "appearance.theme.light" = "Açık"; -"appearance.tab_settings" = "Tab Ayarları"; "appearance.markets_tab" = "Piyasalar Sekmesi"; +"appearance.hide_markets" = "Piyasaları Gizle"; +"appearance.price_change" = "Fiyat Değişimi"; +"appearance.price_change.24h" = "24S"; +"appearance.price_change.1d" = "Gece Yarısı UTC"; + "appearance.launch_screen" = "Başlangıç Ekranı"; "appearance.launch_screen.auto" = "Oto"; "appearance.launch_screen.balance" = "Bakiye"; "appearance.launch_screen.market_overview" = "Piyasaya Genel Bakış"; "appearance.launch_screen.watchlist" = "İzleme Listesi"; -"appearance.app_icon" = "Uygulama İkonları"; - -"appearance.balance_conversion" = "Bakiye çevrimi"; - +"appearance.balance_tab" = "Bakiye Sekmesi"; +"appearance.hide_buttons" = "Düğmeleri Gizle"; "appearance.balance_value" = "Bakiye değeri"; -"appearance.balance_value.coin_value" = "Para değeri"; -"appearance.balance_value.fiat_value" = "Fiat Değeri"; +"appearance.balance_value.coin_fiat" = "Coin/Fiat"; +"appearance.balance_value.fiat_coin" = "Fiat/Coin"; + +"appearance.app_icon" = "Uygulama İkonları"; // Settings -> Contacts @@ -1624,7 +1730,7 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "add_token.input_placeholder.bep2_symbol" = "BEP2 sembolü"; "add_token.coin_name" = "Kripto Para Adı"; "add_token.symbol" = "Sembol"; -"add_token.decimals" = "Decimals"; +"add_token.decimals" = "Ondalıklar"; // Wallet Connect @@ -1918,7 +2024,7 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "nft.activity.event_types" = "Etkinlik Türleri"; "nft.activity.event_type.all" = "Tüm Etkinlikler"; "nft.activity.event_type.sale" = "İndirim"; -"nft.activity.event_type.transfer" = "Transfer"; +"nft.activity.event_type.transfer" = "Aktar"; "nft.activity.event_type.mint" = "Mint"; "nft.activity.event_type.list" = "Liste"; "nft.activity.event_type.listCancel" = "Liste İptal"; @@ -1967,7 +2073,6 @@ Ayarlar - > %@ ekranına giderek kamera erişimine izin verin."; "tron.send.fee.info" = "Ağda belirli bir işlemi göndermenin tahmini maliyeti. (Enerji, Bant Genişliği ve Aktivasyon Ücreti hariç)"; "tron.send.resources_consumed.info" = "Bandwidth, blockchain veritabanında saklanan işlem baytlarının boyutunu ölçen birimdir. İşlem ne kadar büyük olursa, o kadar fazla bant genişliği kaynağı tüketilir.\n\nEnergy, TRON sanal makinesinin TRON ağında belirli işlemleri gerçekleştirmek için ihtiyaç duyduğu hesaplama miktarını ölçen birimdir.\n\nAkıllı sözleşmeden beri işlemlerin yürütülmesi için bilgi işlem kaynakları gerekir, her akıllı sözleşme işleminin energy ücretini ödemesi gerekir."; "tron.send.activation_fee.info" = "TRX veya TRC-10 jetonlarını etkin olmayan bir hesap adresine aktarmak, hesabı etkinleştirir."; -"tron.send.inactive_address" = "Bu adres aktif değil"; // Cex Coin Select diff --git a/UnstoppableWallet/UnstoppableWallet/zh.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/zh.lproj/Localizable.strings index 2e014028d7..cc91e8bc3a 100644 --- a/UnstoppableWallet/UnstoppableWallet/zh.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/zh.lproj/Localizable.strings @@ -228,6 +228,7 @@ "extended_key.purpose" = "目的"; "extended_key.blockchain" = "Blockchain"; "extended_key.account" = "账户"; +"extended_key.account.description" = "这是高级用户的设置。 如果您试图导入钱包(通过扩展私钥)或交易列表(通过扩展公钥),您需要账户 0。"; "extended_key.tap_to_show" = "点击點擊顯示 extended private key 显示扩展私钥"; // Backup @@ -318,7 +319,6 @@ "balance.downloading_blocks" = "正在下载块"; "balance.scanning_blocks" = "扫描块"; "balance.enhancing_transactions" = "加强交易"; -"wait_for_synchronization" = "等待同步"; "balance.searching.count" = "%@ tx"; "balance.syncing_percent" = "正在同步... %@"; @@ -351,6 +351,8 @@ "balance.token.frozen" = "冰雪奇缘"; "balance.token.frozen.info.title" = "冰冻标题"; "balance.token.frozen.info.description" = "冻结描述文本"; +"balance.token.account.inactive.title" = "帐户未激活"; +"balance.token.account.inactive.description" = "新的TRON钱包需要至少1TRX的存款才能激活。 非活动钱包可以持有并接收代币,但在激活之前不会纠正余额。"; // Account switcher @@ -445,6 +447,8 @@ "send.transaction_inputs_outputs_info.shuffle.description" = "交易输出的顺序是随机的每次交易。有时更改可以是第一次输出,有时可以是第二次。 如果用户信任应用程序的开发者,则考虑这个推荐选项。"; "send.transaction_inputs_outputs_info.deterministic.title" = "2. Deterministic"; "send.transaction_inputs_outputs_info.deterministic.description" = "存在一个公认的交易输出排序标准(称为BIP69)。在开源钱包中,该标准确保钱包用户不需要信任应用开发者如何实现输出的排序。由于这个标准是新的,目前还没有多少钱包实施它。因此,在区块链上有可能某种程度上看出交易是否来自使用该标准的钱包。"; +"send.select_all" = "选择所有"; +"send.unselect_all" = "取消选择所有"; "send.confirmation.title" = "确认"; "send.confirmation.you_send" = "您发送"; @@ -462,18 +466,20 @@ "send.confirmation.time_lock" = "TimeLock"; "send.confirmation.replace_by_fee" = "替换交易"; "send.confirmation.replaced_transactions" = "已替换的交易"; - +"send.confirmation.input" = "输入"; + +"send.confirmation.sync_failed" = "同步失败"; +"send.confirmation.invalid_data" = "无效数据"; +"send.confirmation.refresh" = "刷新"; +"send.confirmation.please_wait" = "请稍候"; +"send.confirmation.expires_in" = "将在%@后过期"; +"send.confirmation.expired" = "过期"; "send.confirmation.slide_to_send" = "滑动发送"; "send.confirmation.sending" = "发送中"; "send.confirmation.sent" = "已发送"; "send.confirmation.slide_to_approve" = "滑动以批准"; -"send.confirmation.approving" = "审批中"; -"send.confirmation.approved" = "批准"; - "send.confirmation.slide_to_revoke" = "滑动以撤销"; -"send.confirmation.revoking" = "正在取消"; -"send.confirmation.revoked" = "已注销"; "send.confirmation.slide_to_resend" = "重新发送"; "send.confirmation.slide_to_cancel" = "取消交易"; @@ -506,6 +512,7 @@ "send.lock_time" = "TimeLock"; "send.unspent_outputs" = "UTxOs"; +"send.unspent_outputs.description" = "手动选择 UTxO 将资金花在余额中"; "send.unspent_outputs.send_to" = "发送至"; "send.unspent_outputs.change" = "更改"; @@ -513,6 +520,14 @@ "approve.confirmation.you_revoke" = "您已取消"; "approve.confirmation.spender" = "消费人"; +"send.enter_amount" = "输入金额"; +"send.enter_address" = "输入地址"; +"send.invalid_address" = "无效地址"; +"send.token_not_enabled" = "代币未启用"; +"send.token_syncing" = "代币同步"; +"send.token_not_synced" = "代币未同步"; +"send.insufficient_balance" = "可用额度不足"; + // Donate "donate.list.title" = "捐赠方式"; @@ -615,7 +630,7 @@ "swap.advanced_settings.service_fee_description" = "典型平台上交换动作的服务费:0.3% 或 0.6%"; "swap.advanced_settings.error.lower_slippage" = "您的交易可能失败。"; "swap.advanced_settings.error.higher_slippage" = "下跌容忍不得高于%@%%"; -"swap.advanced_settings.error.invalid_address" = "错误地址"; +"swap.advanced_settings.error.invalid_address" = "无效地址"; "swap.advanced_settings.error.invalid_slippage" = "无效延误"; "swap.advanced_settings.error.invalid_deadline" = "无效截止日期"; @@ -643,7 +658,6 @@ "swap.confirmation.slide_to_swap" = "滑动切换"; "swap.confirmation.swapping" = "交易"; -"swap.confirmation.swapped" = "已交换"; "swap.confirmation.refresh" = "刷新"; "swap.confirmation.impact_too_high" = "%@ 已禁用此交易的掉期操作,因为您得到的价格极其不利。 这是由于流动性极低造成的。\n如果您仍想兑换,请使用 %@ 网站。"; "swap.confirmation.impact_warning" = "重要提示!您正在以极其不利的价格进行交易。这是因为流动性非常低。"; @@ -686,6 +700,51 @@ "market.defi_cap" = "DeFi市场价值"; "market.defi_tvl" = "DeFi中的TVL"; +"market.global.market_cap" = "总市值"; +"market.global.volume" = "24小时交易量"; +"market.global.btc_dominance" = "BTC市场占比"; +"market.global.etf_inflow" = "ETF 输入"; +"market.global.tvl_in_defi" = "DeFi中的TVL"; + +"market.tab.news" = "发现"; +"market.tab.coins" = "币"; +"market.tab.watchlist" = "关注"; +"market.tab.platforms" = "平台"; +"market.tab.pairs" = "配对标点"; +"market.tab.sectors" = "行业"; + +"market.sort_by.title" = "排序方式为"; +"market.sort_by.manual" = "手册"; +"market.sort_by.highest_cap" = "市值由高到低"; +"market.sort_by.lowest_cap" = "市值由低到高"; +"market.sort_by.gainers" = "涨幅最大者"; +"market.sort_by.losers" = "跌幅最大者"; +"market.sort_by.highest_volume" = "成交量由高到低"; +"market.sort_by.lowest_volume" = "成交量由低到高"; + +"market.top_coins.title" = "币"; +"market.top_coins" = "前 %@"; + +"market.time_period.title" = "句号"; +"market.time_period.1d" = "1天"; +"market.time_period.1w" = "1周"; +"market.time_period.2w" = "2周"; +"market.time_period.1m" = "1月"; +"market.time_period.3m" = "3月"; +"market.time_period.6m" = "6 Months"; +"market.time_period.1y" = "1 Year"; +"market.time_period.2y" = "2 年"; +"market.time_period.5y" = "5 年"; +"market.time_period.1d.short" = "1天"; +"market.time_period.1w.short" = "1周"; +"market.time_period.2w.short" = "2周"; +"market.time_period.1m.short" = "1个月"; +"market.time_period.3m.short" = "3个月"; +"market.time_period.6m.short" = "6个月"; +"market.time_period.1y.short" = "1年"; +"market.time_period.2y.short" = "2年"; +"market.time_period.5y.short" = "5年"; + "market.project_has_no_coin" = "此项目没有硬币"; "market.top.section.header.see_all" = "查看全部"; @@ -722,6 +781,7 @@ "market.top.top_platforms" = "前列平台"; "market.top.protocols" = "协议"; +"market.pairs.volume" = "量"; "top_pairs.title" = "热门市场交易对"; "top_pairs.description" = "每个交易所的交易量最高的交易对"; @@ -730,6 +790,8 @@ "top_platform.title" = "%@ 星系"; "top_platform.description" = "%@ 链上所有协议的市场上限"; +"top_platform.total_cap" = "总市值"; + "market.search.recent" = "最近"; "market.search.popular" = "流行"; @@ -741,10 +803,22 @@ "market_discovery.not_found" = "未发现结果"; "market_watchlist.empty.caption" = "您的观察列表为空。"; +"market.watchlist.signals" = "信号"; +"market.watchlist.empty" = "您的观察列表为空"; +"market.watchlist.signals.description" = "下面的信号是基于Bollinger Bands 和 RSI 技术价格指标的近似值。 最近30天。这些信号是算法,可以经常改变。"; +"market.watchlist.signals.strong_buy.description" = "对价格上涨信心高"; +"market.watchlist.signals.buy.description" = "在不久的将来可能的价格提高"; +"market.watchlist.signals.neutral.description" = "没有明确的趋势,市场处于平衡状态"; +"market.watchlist.signals.sell.description" = "未来近期可能价格下跌"; +"market.watchlist.signals.strong_sell.description" = "价格下跌的高概率"; +"market.watchlist.signals.risky.description" = "风险水平升高,需要谨慎"; +"market.watchlist.signals.warning" = "请记住始终要进行风险管理,并注意这不是财务建议"; +"market.watchlist.signals.turn_on" = "打开"; "market.advanced_search.title" = "过滤器"; "market.advanced_search.show_results" = "显示结果"; "market.advanced_search.empty_results" = "空结果"; +"market.advanced_search.retry" = "重试"; "market.advanced_search.dex_description" = "此设置将应用于在以太坊(Uniswap DEX)和币安智能链(Pancake DEX)上交易的代币。"; "market.advanced_search.24h" = "24小时"; @@ -764,17 +838,10 @@ "market.advanced_search.liquidity" = "DEX流动性"; "market.advanced_search.blockchains" = "区块链"; -"market.advanced_search.technical_advice" = "交易信号"; +"market.advanced_search.signal" = "交易信号"; "market.advanced_search.price_period" = "价格区间"; "market.advanced_search.price_change" = "价格变动"; -"market.advanced_search.technical_advice.risk_trade" = "交易风险"; -"market.advanced_search.technical_advice.strong_buy" = "强烈建议买入"; -"market.advanced_search.technical_advice.buy" = "购买"; -"market.advanced_search.technical_advice.neutral" = "中性"; -"market.advanced_search.technical_advice.sell" = "出售"; -"market.advanced_search.technical_advice.strong_sell" = "强烈建议卖出"; - "market.advanced_search.outperformed_btc" = "跑赢BTC"; "market.advanced_search.outperformed_eth" = "跑赢ETH"; "market.advanced_search.outperformed_bnb" = "跑赢BNB"; @@ -812,12 +879,12 @@ "market.advanced_search.week" = "1周"; "market.advanced_search.week2" = "2周"; "market.advanced_search.month" = "1月"; -"market.advanced_search.month6" = "6 个月"; -"market.advanced_search.year" = "1 年"; +"market.advanced_search.month6" = "6 Months"; +"market.advanced_search.year" = "1 Year"; "market.advanced_search.day.short" = "24时"; "market.advanced_search.week.short" = "7天"; -"market.advanced_search.month.short" = "1月"; +"market.advanced_search.month.short" = "1个月"; "market.advanced_search_results.title" = "搜索结果"; @@ -830,10 +897,36 @@ "market.global.defi_cap.title" = "DeFi市场价值"; "market.global.defi_cap.description" = "去中心化金融(DeFi)项目的总市场价值"; -"market.global.tvl_in_defi.title" = "DeFi中的TVL"; -"market.global.tvl_in_defi.description" = "去中心金融(DeFi)中的锁定总价值(TVL)"; -"market.global.tvl_in_defi.multi_chain" = "多链"; -"market.global.tvl_in_defi.filter_by_chain" = "通过区块链过滤"; +"market.etf.title" = "网络总流量"; +"market.etf.description" = "ETF的净流入等于其现金流入减去流出"; +"market.etf.total_net_assets" = "净资产总额"; +"market.etf.sort_by.highest_assets" = "最高资产"; +"market.etf.sort_by.lowest_assets" = "最低资产"; +"market.etf.sort_by.inflow" = "流入"; +"market.etf.sort_by.outflow" = "外向流"; +"market.etf.period.all" = "所有"; + +"market.market_cap.title" = "总市场价值"; +"market.market_cap.description" = "所有加密货币的总市场价值"; +"market.market_cap.market_cap" = "市场价值"; + +"market.tvl_in_defi.title" = "DeFi中的TVL"; +"market.tvl_in_defi.description" = "去中心金融(DeFi)中的锁定总价值(TVL)"; +"market.tvl_in_defi.tvl" = "TVL"; +"market.tvl_in_defi.multi_chain" = "多链"; +"market.tvl_in_defi.filter_by_chain" = "通过区块链过滤"; +"market.tvl_in_defi.filter.all" = "所有"; + +"market.volume.title" = "量"; +"market.volume.description" = "加密货币市场24小时交易量"; +"market.volume.volume" = "量"; + +"market.signal.risky" = "有风险"; +"market.signal.strong_buy" = "强烈建议买入"; +"market.signal.buy" = "购买"; +"market.signal.neutral" = "中性"; +"market.signal.sell" = "出售"; +"market.signal.strong_sell" = "强烈建议卖出"; // Coin Page @@ -854,12 +947,13 @@ "coin_overview.genesis_date" = "起始日期"; "coin_overview.trading_volume" = "成交量"; -"coin_overview.roi.hour24" = "1天"; +"coin_overview.roi.hour24" = "24 小时"; +"coin_overview.roi.day1" = "1天"; "coin_overview.roi.day7" = "1周"; "coin_overview.roi.day14" = "2周"; "coin_overview.roi.day30" = "1月"; "coin_overview.roi.day200" = "6月"; -"coin_overview.roi.year1" = "1 年"; +"coin_overview.roi.year1" = "1 Year"; "coin_overview.overview" = "行情"; "coin_overview.description_warning" = "这是一个基于给定的加密货币所提供的参考材料生成的 AI 描述。它可能包含错误。"; @@ -875,55 +969,55 @@ "coin_overview.whitepaper" = "白皮书"; // Coin Page -> Analytics -"technical_advice.over.bought" = "overbought"; -"technical_advice.over.sold" = "oversold"; -"technical_advice.down" = "down"; -"technical_advice.up" = "up"; +"technical_advice.over.bought" = "已购买过期的"; +"technical_advice.over.sold" = "超价"; +"technical_advice.down" = "下"; +"technical_advice.up" = "上"; -"technical_advice.over.main" = "The actions with the asset are risky."; -"technical_advice.over.indicators.signal_date" = "Starting from the %@"; -"technical_advice.over.indicators" = "The asset is outside the Bollinger Band channel and %@."; -"technical_advice.over.rsi" = " RSI = %@, This also indicates that the asset is %@."; -"technical_advice.over.advice" = " There might be a strong %@ward movement, so it's better to wait for the asset price to return to the channel."; +"technical_advice.over.main" = "资产的行动是有风险的。"; +"technical_advice.over.indicators.signal_date" = "从 %@ 开始"; +"technical_advice.over.indicators" = "资产不属于Bollinger频道和 %@。"; +"technical_advice.over.rsi" = " RSI = %@,这也表明该资产为%@."; +"technical_advice.over.advice" = " 可能会有强烈的%@向动,因此最好等待资产价格回到通道中。"; -"technical_advice.strong.indicators" = "The asset was %@, but now it has returned to the Bollinger Band channel. This indicates a possible trend reversal."; -"technical_advice.strong.rsi" = " Meanwhile, the RSI is %@, which still indicates that it is %@."; -"technical_advice.strong.advice" = " This could be a very strong signal to enter the market. Keep in mind that there may be several attempts of %@ward movement after returning to the channel, so do not forget about risk management."; +"technical_advice.strong.indicators" = "资产是 %@,但现在它已经返回Bollinger Band 通道。这表明可能出现趋势逆转。"; +"technical_advice.strong.rsi" = " 与此同时,RSI 是 %@, 它仍然表示它是 %@。"; +"technical_advice.strong.advice" = " 这可能是一个非常强有力的进入市场的信号。请记住,在回到通道后,可能会有多次尝试%@向运动,所以不要忽视风险管理。"; -"technical_advice.stable.rsi" = " Meanwhile, the RSI is %@, which also indicates a trend reversal (RSI crossed the boundary at 70%)."; -"technical_advice.stable.advice" = "The price is returning to neutral levels, however, there is still potential for upward movement. Keep in mind that RSI = 50 and the middle of the Bollinger Bands are strong resistances and possible trend reversal points. Do not forget about risk management."; +"technical_advice.stable.rsi" = " 与此同时,RSI为 %@,这也表明了一种扭转趋势(RSI跨越边界,占70%)。"; +"technical_advice.stable.advice" = "然而,价格正在回到中度水平,但仍有上升的潜力。 牢记RSI=50和Bollinger区中部是强大的抗药性和可能的趋势逆转点。 不要忘记风险管理。"; -"technical_advice.neutral.rsi" = "RSI = %@ also confirms the absence of a strong trend."; -"technical_advice.neutral.indicators" = "The asset was in the overbought/oversold zone, but at the moment the price has returned to the Bollinger Band channel in the neutral zone. The RSI is %@ also confirms the absence of a strong trend, so overall the asset price is moving towards averaging and further movement is possible in any direction."; -"technical_advice.neutral.advice" = " In general, the asset price is moving towards averaging and further movement is possible in any direction."; +"technical_advice.neutral.rsi" = "RSI = %@ 也证实没有强有力的趋势。"; +"technical_advice.neutral.indicators" = "该资产处于被推销/出售区内,但此时价格已回到中立区内的Bollinger频道。 RSI 是 %@ 也证实了没有强大的趋势。 因此,整体资产价格正在走向平均值,任何方向都有可能进一步发展。"; +"technical_advice.neutral.advice" = " 一般而言,资产价格正在走向平均值,任何方向都有进一步变动的可能。"; -"technical_advice.other.title" = "Please note:"; +"technical_advice.other.title" = "请注意:"; -"technical_advice.ema.above" = "above"; -"technical_advice.ema.below" = "below"; -"technical_advice.ema.growth" = "growth"; -"technical_advice.ema.decrease" = "decrease"; -"technical_advice.ema.advice" = "EMA 200. Determines the overall sentiment and trend. The daily price of the asset is located %@ the EMA (%@). This means that globally the asset is set for %@."; +"technical_advice.ema.above" = "以上"; +"technical_advice.ema.below" = "下面"; +"technical_advice.ema.growth" = "发展"; +"technical_advice.ema.decrease" = "减少"; +"technical_advice.ema.advice" = "EMA 200。确定总体情绪和趋势。资产的日价格位于 EMA (%@) %@。这意味着全球资产已设定为 %@。"; -"technical_advice.macd.positive" = "above"; -"technical_advice.macd.negative" = "below"; -"technical_advice.macd.advice" = "MACD. Assesses the strength of the trend considering the average price change. The daily value of the histogram is %@ (%@). The price of the asset globally may move %@."; +"technical_advice.macd.positive" = "以上"; +"technical_advice.macd.negative" = "下面"; +"technical_advice.macd.advice" = "MACD。考虑平均价格变动来评估趋势的强度。直方图的每日值为%@(%@)。该资产的价格全球可能会移动%@。"; "coin_analytics.indicators.title" = "技术指标"; -"coin_analytics.indicators.disclaimer" = "Always remember to apply risk management, and note that this is not financial advice."; -"coin_analytics.indicators.info.title" = "Technical Indicators"; -"coin_analytics.indicators.info.description" = "We use the Bollinger Bands + RSI strategy to determine trading signals. All calculations are based on daily candlesticks and provide advice for a moderately long term. The essence of the strategy is that the asset price should reach an extreme, breaking out of the Bollinger Bands channel, and the RSI should be in the overbought/oversold zone. After the price returns to the channel, there is a high probability of the price returning to the mean values or attempting to break the channel from the other side. Note that the strategy may give several false signals during strong market movements before a correct signal appears.\n\nPlease remember that it is very important to apply risk management to trading and remember to cut losses if the market situation changes! "; -"coin_analytics.indicators.hide_details" = "Hide Details"; -"coin_analytics.indicators.show_details" = "Show Details"; +"coin_analytics.indicators.disclaimer" = "请记住始终要进行风险管理,并注意这不是财务建议"; +"coin_analytics.indicators.info.title" = "技术指标"; +"coin_analytics.indicators.info.description" = "我们使用布林带 + RSI 策略来确定交易信号。所有计算都基于日线图,并为中长期提供建议。该策略的本质是资产价格应该达到一个极端点,突破布林带通道,并且 RSI 应该处于超买/超卖区域。在价格回到通道后,有很高的概率价格回归到平均值或尝试从另一侧突破通道。请注意,在正确信号出现之前,该策略在市场强劲波动期间可能会出现多个虚假信号。\n\n请记住,在交易中应用风险管理非常重要,如果市场情况发生变化,请记得及时止损! "; +"coin_analytics.indicators.hide_details" = "隐藏细节"; +"coin_analytics.indicators.show_details" = "显示详细信息"; "coin_analytics.indicators.summary" = "总结"; "coin_analytics.indicators.no_data" = "无数据"; -"coin_analytics.indicators.oversold" = "Very Risky to Trade"; +"coin_analytics.indicators.oversold" = "有风险"; "coin_analytics.indicators.strong_buy" = "强烈建议买入"; "coin_analytics.indicators.buy" = "购买"; "coin_analytics.indicators.neutral" = "中性"; "coin_analytics.indicators.sell" = "出售"; "coin_analytics.indicators.strong_sell" = "强烈建议卖出"; -"coin_analytics.indicators.overbought" = "Very Risky to Trade"; +"coin_analytics.indicators.overbought" = "有风险"; "coin_analytics.period" = "句号"; "coin_analytics.period.select_title" = "选择时间段"; "coin_analytics.period.1h" = "1 小时"; @@ -936,7 +1030,7 @@ "coin_analytics.not_available" = "此项目没有分析数据"; -"coin_analytics.technical_indicators" = "Technical Indicators"; +"coin_analytics.technical_indicators" = "技术指标"; "coin_analytics.technical_indicators.info1" = "概述:这是对资产技术手段的一般概述,考虑到各种技术指标和时间框架。 它根据这些指标提供了一个协商一致的观点(购买、出售或中立)。"; "coin_analytics.technical_indicators.info2" = "移动平均值:这些是通用的技术指标,可使价格数据平滑使用,从而产生一个趋势跟踪指标。 它们显示了特定时期内的平均价格。 多个类型的MA:\n\n简单移动平均值(SMA):这个计算了选定的价格范围的平均值。 通常按该范围内的周期数结算价格。\n\n指数移动平均值(EMA):这使得对最近的价格更加权重,从而对最近的价格变化作出更快的反应。"; "coin_analytics.technical_indicators.info3" = "Oscillators:这些是技术指标,随着时间的推移在波段内(在中心线以下或在设定的层次之间)波动。 它们的目的是帮助查明市场上买卖过多和销售过剩的条件。 这里有一些常见的顾问:\n\n相对力量指数(RSI):这将衡量价格变动的速度和变化。 它通常被用来查明买入或卖出过多的情况。\n\n移动平均汇合差(MACD):用于识别潜在的买卖信号。 它在其信号线上方(购买)或下方(出售)时,会触发技术信号。"; @@ -944,6 +1038,7 @@ "coin_analytics.cex_volume" = "CEX交易量"; "coin_analytics.cex_volume_rank" = "CEX交易量排名"; "coin_analytics.cex_volume_rank.description" = "对中心化交易所的代币按交易量排名的代币。"; +"coin_analytics.cex_volume_rank.sorting_field" = "量"; "coin_analytics.cex_volume.info1" = "Total trading volume for the token on leading centralized exchanges over 30-day period."; "coin_analytics.cex_volume.info2" = "图表显示了过去一年内该代币在主要中心化交易所的日交易量变化。"; "coin_analytics.cex_volume.info3" = "代币的排名基于过去30天内在主要中心化交易所的交易量。"; @@ -952,6 +1047,7 @@ "coin_analytics.dex_volume" = "DEX交易量"; "coin_analytics.dex_volume_rank" = "DEX交易量排名"; "coin_analytics.dex_volume_rank.description" = "对去中心化交易所的代币按交易量排名的代币。"; +"coin_analytics.dex_volume_rank.sorting_field" = "量"; "coin_analytics.dex_volume.info1" = "Total trading volume for the token on leading centralized exchanges over 30-day period."; "coin_analytics.dex_volume.info2" = "图表显示了过去一年内该代币在主要去中心化交易所的日交易量变化。"; "coin_analytics.dex_volume.info3" = "代币的排名基于在主要去中心化交易所过去30天的交易量。"; @@ -963,6 +1059,7 @@ "coin_analytics.dex_liquidity" = "DEX流动性"; "coin_analytics.dex_liquidity_rank" = "DEX流通性排名"; "coin_analytics.dex_liquidity_rank.description" = "对去中心化交易所上按可用流通性排名的代币。"; +"coin_analytics.dex_liquidity_rank.sorting_field" = "流动性"; "coin_analytics.dex_liquidity.info1" = "该代币在主要去中心化交易所当前可用的总流动性。"; "coin_analytics.dex_liquidity.info2" = "图表显示了过去一年内该代币在主要去中心化交易所可用流动性的变化。"; "coin_analytics.dex_liquidity.info3" = "根据在主要去中心化交易所的代币可用流动性排名的所有代币列表。"; @@ -974,6 +1071,7 @@ "coin_analytics.active_addresses.30_day_unique_addresses" = "30天唯一地址"; "coin_analytics.active_addresses_rank" = "活跃地址排名"; "coin_analytics.active_addresses_rank.description" = "按与代币交易的唯一地址的数量排名的代币。"; +"coin_analytics.active_addresses_rank.sorting_field" = "已启用"; "coin_analytics.active_addresses.info1" = "Total number of unique daily active addresses over 24-hour period."; "coin_analytics.active_addresses.info2" = "图表显示了过去一年内每日活跃地址数量的变化。"; "coin_analytics.active_addresses.info3" = "过去30天内与代币进行交易的独特区块链地址的总数。"; @@ -983,6 +1081,7 @@ "coin_analytics.transaction_count" = "交易数"; "coin_analytics.transaction_count_rank" = "Tx计数排名"; "coin_analytics.transaction_count_rank.description" = "代币按区块链上的交易数量排名。"; +"coin_analytics.transaction_count_rank.sorting_field" = "计数"; "coin_analytics.transaction_count.info1" = "30天内代币独特区块链交易的总数。"; "coin_analytics.transaction_count.info2" = "显示1年周期的交易计数变化的图表。"; "coin_analytics.transaction_count.info3" = "代币的排名基于30天内代币交易的数量。"; @@ -992,6 +1091,7 @@ "coin_analytics.holders" = "持有者"; "coin_analytics.holders_rank" = "Classement des titulaires"; "coin_analytics.holders_rank.description" = "通过在多个区块链上持有它们的独特地址来排名令牌。"; +"coin_analytics.holders_rank.sorting_field" = "持有者"; "coin_analytics.holders.info1" = "在各种区块链上持有代币的唯一地址的总数。"; "coin_analytics.holders.info2" = "在每一个区块链上持有代币的前10大钱包。"; "coin_analytics.holders.tracked_blockchains" = "已追踪区块链:以太坊、币安智能链、Optimism、Arbitrum、Celo、Cronos、Avalanche、Fantom、Polygon"; @@ -1011,10 +1111,12 @@ "coin_analytics.project_fee" = "项目费用"; "coin_analytics.project_fee_rank" = "项目收费等级"; "coin_analytics.project_fee_rank.description" = "代币根据各自项目产生的费用进行排名。收取费用的方式因项目而异。"; +"coin_analytics.project_fee_rank.sorting_field" = "量"; "coin_analytics.project_revenue" = "项目收入"; "coin_analytics.project_revenue_rank" = "项目收入排名"; "coin_analytics.project_revenue_rank.description" = "代币按照通过机制(如质押或代币销毁)为持有者产生的收入进行排名。"; +"coin_analytics.project_revenue_rank.sorting_field" = "收入"; "coin_analytics.other_data" = "其它数据"; @@ -1200,6 +1302,8 @@ "settings.rate_us" = "评价我们"; "settings.tell_friends" = "分享给好友"; "settings.contact_us" = "联系我们"; +"settings.social_networks.label" = "Be Unstoppable"; +"settings.social_networks.footer" = "Learn and master crypto via exclusive videos. Get to know us informally. Be the first to see things we work on."; // Settings -> Base Currency @@ -1417,9 +1521,7 @@ // Settings -> About App "settings.about_app.title" = "关于应用"; -"settings.about_app.app_name" = "%@钱包"; -"settings.about_app.description" = "%@ 钱包是为那些寻求投资和以私人和独立方式存储加密货币的人建造的。\n\n它是一个非保管的、对等的钱包,在那里只有用户能够控制资金。 它不收集任何数据,并且通过不将用户的资金锁定到特定的钱包应用程序来保持用户独立。\n\n %@ 钱包完全开源,任何人都可以确认应用程序的工作完全如其所声称的那样正常。"; -"settings.about_app.whats_new" = "新增内容"; +"settings.about_app.app_version" = "应用程序版本"; "settings.about_app.website" = "网站"; // Settings -> About App -> Contact @@ -1431,11 +1533,11 @@ // Settings -> Privacy "settings.privacy" = "隐私保护"; -"settings.privacy.description" = "%@ 不收集任何数据或使用分析工具来揭露任何用户数据。 钱包的设计是为了确保用户高度的隐私。"; -"settings.privacy.statement.user_data_storage" = "用户数据总是保留在用户的设备上。"; +"settings.privacy.description" = "%@不收集暴露您私人信息的个人数据,例如币余额或地址。虽然我们收集了一些用户界面使用统计数据,但这仅用于了解我们的用户群和应用程序使用趋势。如果您希望,可以禁用此功能。"; "settings.privacy.statement.data_usage" = "钱包没有收集任何有关用户的数据。"; -"settings.privacy.statement.data_privacy" = "钱包没有收集任何有关用户的数据。"; -"settings.privacy.statement.user_account" = "没有用户帐户或数据库存放其他地方的用户数据。"; +"settings.privacy.statement.data_storage" = "我们没有用户账户或存储用户数据的数据库。"; +"settings.privacy.statement.user_account" = "如果允许,钱包将与不可阻挡团队分享应用程序使用习惯。这是为了了解我们的用户正在使用哪些功能(或未使用)。作为一个关注隐私的应用程序,我们需要一种评估我们努力的方式,如果没有这个,我们就无法知道我们构建的功能是否被使用。"; +"settings.privacy.allow" = "共享界面数据"; // Settings -> Appearance @@ -1446,21 +1548,25 @@ "appearance.theme.dark" = "深色"; "appearance.theme.light" = "浅色"; -"appearance.tab_settings" = "标签设置"; "appearance.markets_tab" = "市场选项卡"; +"appearance.hide_markets" = "隐藏市场"; +"appearance.price_change" = "价格变动"; +"appearance.price_change.24h" = "24时"; +"appearance.price_change.1d" = "午夜UTC"; + "appearance.launch_screen" = "启动屏幕"; "appearance.launch_screen.auto" = "自动"; "appearance.launch_screen.balance" = "余额"; "appearance.launch_screen.market_overview" = "行情"; "appearance.launch_screen.watchlist" = "关注"; -"appearance.app_icon" = "应用图标"; - -"appearance.balance_conversion" = "余额转换"; - +"appearance.balance_tab" = "余额标签"; +"appearance.hide_buttons" = "隐藏按钮"; "appearance.balance_value" = "余额值"; -"appearance.balance_value.coin_value" = "币值"; -"appearance.balance_value.fiat_value" = "纤维值"; +"appearance.balance_value.coin_fiat" = "加密货币 / 法定货币"; +"appearance.balance_value.fiat_coin" = "法定货币 / 加密货币"; + +"appearance.app_icon" = "应用图标"; // Settings -> Contacts @@ -1525,8 +1631,8 @@ "chart.time_duration.day" = "24时"; "chart.time_duration.week" = "7天"; -"chart.time_duration.week2" = "2星期"; -"chart.time_duration.month" = "1月"; +"chart.time_duration.week2" = "2周"; +"chart.time_duration.month" = "1个月"; "chart.time_duration.month3" = "3个月"; "chart.time_duration.halfyear" = "6个月"; "chart.time_duration.year" = "1年"; @@ -1959,7 +2065,6 @@ "tron.send.fee.info" = "估算在网络上发送特定交易所需的成本,包括能源、带宽和激活费"; "tron.send.resources_consumed.info" = "Bandwidth 是衡量存储在区块链数据库中的交易字节大小的单位。 交易越大,消耗的带宽资源就越多。\n\nEnergy 是衡量波场虚拟机在波场网络上执行特定操作所需计算量的单位。\n\n自智能合约 交易需要计算资源来执行,每笔智能合约交易都需要支付 energy费用。"; "tron.send.activation_fee.info" = "正在将TRX或 TRC-10令牌传输到一个非活动帐户地址将激活帐户。"; -"tron.send.inactive_address" = "此地址未激活"; // Cex Coin Select diff --git a/UnstoppableWallet/Widget/AppWidgetConstants.swift b/UnstoppableWallet/Widget/AppWidgetConstants.swift index b5ad23b417..5a6a1ec87e 100644 --- a/UnstoppableWallet/Widget/AppWidgetConstants.swift +++ b/UnstoppableWallet/Widget/AppWidgetConstants.swift @@ -4,6 +4,4 @@ enum AppWidgetConstants { static let singleCoinPriceWidgetKind: String = "io.horizontalsystems.unstoppable.SingleCoinPriceWidget" static let topCoinsWidgetKind: String = "io.horizontalsystems.unstoppable.TopCoinsWidget" static let watchlistWidgetKind: String = "io.horizontalsystems.unstoppable.WatchlistWidget" - - static let keyFavoriteCoinUids = "favorite_coin_uids" } diff --git a/UnstoppableWallet/Widget/Base.lproj/AppWidget.intentdefinition b/UnstoppableWallet/Widget/Base.lproj/AppWidget.intentdefinition index 47aca06421..3c68ada273 100644 --- a/UnstoppableWallet/Widget/Base.lproj/AppWidget.intentdefinition +++ b/UnstoppableWallet/Widget/Base.lproj/AppWidget.intentdefinition @@ -47,43 +47,23 @@ INEnumValueDisplayName - Highest Volume - INEnumValueDisplayNameID - t6AKQ5 - INEnumValueIndex - 3 - INEnumValueName - highestVolume - - - INEnumValueDisplayName - Lowest Volume - INEnumValueDisplayNameID - C1LmDj - INEnumValueIndex - 4 - INEnumValueName - lowestVolume - - - INEnumValueDisplayName - Top Gainers + Gainers INEnumValueDisplayNameID Rl3a0T INEnumValueIndex - 5 + 3 INEnumValueName - topGainers + gainers INEnumValueDisplayName - Top Losers + Losers INEnumValueDisplayNameID x1kK4T INEnumValueIndex - 6 + 4 INEnumValueName - topLosers + losers @@ -93,11 +73,11 @@ INIntentDefinitionNamespace 5N3KPh INIntentDefinitionSystemVersion - 23B81 + 23E224 INIntentDefinitionToolsBuildVersion - 15A240d + 15E204a INIntentDefinitionToolsVersion - 15.0 + 15.3 INIntents diff --git a/UnstoppableWallet/Widget/CoinPriceList/CoinPriceListView.swift b/UnstoppableWallet/Widget/CoinListView.swift similarity index 54% rename from UnstoppableWallet/Widget/CoinPriceList/CoinPriceListView.swift rename to UnstoppableWallet/Widget/CoinListView.swift index 9575d201e6..87227b526b 100644 --- a/UnstoppableWallet/Widget/CoinPriceList/CoinPriceListView.swift +++ b/UnstoppableWallet/Widget/CoinListView.swift @@ -2,8 +2,11 @@ import Charts import SwiftUI import WidgetKit -struct CoinPriceListView: View { - var entry: CoinPriceListProvider.Entry +struct CoinListView: View { + let items: [CoinItem] + let maxItemCount: Int + let title: LocalizedStringKey + let subtitle: LocalizedStringKey @Environment(\.widgetFamily) private var family @@ -43,14 +46,14 @@ struct CoinPriceListView: View { @ViewBuilder private func largeView() -> some View { VStack(spacing: 0) { HStack(spacing: .margin16) { - Text(entry.mode.title) + Text(title) .lineLimit(1) .font(.themeSubhead1) .foregroundColor(.themeLeah) Spacer() - Text(title(sortType: entry.sortType)) + Text(subtitle) .lineLimit(1) .font(.themeSubhead2) .foregroundColor(.themeGray) @@ -66,56 +69,30 @@ struct CoinPriceListView: View { .padding(.vertical, .margin4) } - @ViewBuilder private func list(verticalPadding: CGFloat, rowBuilder: @escaping (CoinPriceListEntry.Item) -> some View) -> some View { - if entry.mode.isWatchlist, entry.items.isEmpty { - VStack(spacing: .margin16) { - switch family { - case .systemLarge: - ZStack { - Circle() - .fill(Color.themeRaina) - .frame(width: 100, height: 100) - - Image("rate_48") - .renderingMode(.template) - .foregroundColor(.themeGray) + @ViewBuilder private func list(verticalPadding: CGFloat, rowBuilder: @escaping (CoinItem) -> some View) -> some View { + GeometryReader { proxy in + ListSection { + ForEach(items, id: \.uid) { item in + Link(destination: URL(string: "unstoppable.money://coin/\(item.uid)")!) { + rowBuilder(item) + .padding(.horizontal, .margin16) + .frame(maxHeight: .infinity) + .frame(maxHeight: proxy.size.height / CGFloat(maxItemCount)) } - default: - EmptyView() + .buttonStyle(PlainButtonStyle()) } - Text("watchlist.empty") - .multilineTextAlignment(.center) - .font(.themeSubhead2) - .foregroundColor(.themeGray) - } - .frame(maxHeight: .infinity) - .padding(.margin16) - } else { - GeometryReader { proxy in - ListSection { - ForEach(entry.items, id: \.uid) { item in - Link(destination: URL(string: "unstoppable.money://coin/\(item.uid)")!) { - rowBuilder(item) - .padding(.horizontal, .margin16) - .frame(maxHeight: .infinity) - .frame(maxHeight: proxy.size.height / CGFloat(entry.maxItemCount)) - } - .buttonStyle(PlainButtonStyle()) - } - - if entry.items.count < entry.maxItemCount { - Spacer() - } + if items.count < maxItemCount { + Spacer() } - .themeListStyle(.transparentInline) } - .frame(maxHeight: .infinity) - .padding(.vertical, verticalPadding) + .themeListStyle(.transparentInline) } + .frame(maxHeight: .infinity) + .padding(.vertical, verticalPadding) } - @ViewBuilder private func row(item: CoinPriceListEntry.Item) -> some View { + @ViewBuilder private func row(item: CoinItem) -> some View { HStack(spacing: .margin16) { icon(image: item.icon) @@ -133,9 +110,17 @@ struct CoinPriceListView: View { } HStack(spacing: .margin16) { - Text(item.name) - .font(.themeSubhead2) - .foregroundColor(.themeGray) + HStack(spacing: .margin4) { + if let rank = item.rank { + BadgeViewNew(text: rank) + } + + if let marketCap = item.marketCap { + Text(marketCap) + .font(.themeSubhead2) + .foregroundColor(.themeGray) + } + } Spacer() @@ -159,15 +144,4 @@ struct CoinPriceListView: View { .frame(width: .iconSize32, height: .iconSize32) } } - - private func title(sortType: SortType) -> LocalizedStringKey { - switch sortType { - case .highestCap, .unknown: return "sort_type.highest_cap" - case .lowestCap: return "sort_type.lowest_cap" - case .highestVolume: return "sort_type.highest_volume" - case .lowestVolume: return "sort_type.lowest_volume" - case .topGainers: return "sort_type.top_gainers" - case .topLosers: return "sort_type.top_losers" - } - } } diff --git a/UnstoppableWallet/Widget/CoinPriceList/CoinPriceListEntry.swift b/UnstoppableWallet/Widget/CoinPriceList/CoinPriceListEntry.swift index cead1231cd..2652bfb420 100644 --- a/UnstoppableWallet/Widget/CoinPriceList/CoinPriceListEntry.swift +++ b/UnstoppableWallet/Widget/CoinPriceList/CoinPriceListEntry.swift @@ -4,18 +4,31 @@ import WidgetKit struct CoinPriceListEntry: TimelineEntry { let date: Date - let mode: CoinPriceListMode let sortType: SortType let maxItemCount: Int - let items: [Item] + let items: [CoinItem] +} + +struct CoinItem { + let uid: String + let icon: Image? + let code: String + let marketCap: String? + let rank: String? + let price: String + let priceChange: String + let priceChangeType: PriceChangeType - struct Item { - let uid: String - let icon: Image? - let code: String - let name: String - let price: String - let priceChange: String - let priceChangeType: PriceChangeType + static func stub(index: Int) -> CoinItem { + CoinItem( + uid: "coin\(index)", + icon: nil, + code: "COD\(index)", + marketCap: "$1.23M", + rank: "\(index)", + price: "$1234", + priceChange: "1.23", + priceChangeType: .unknown + ) } } diff --git a/UnstoppableWallet/Widget/CoinPriceList/CoinPriceListProvider.swift b/UnstoppableWallet/Widget/CoinPriceList/CoinPriceListProvider.swift index 35a114280b..6a6a1ea592 100644 --- a/UnstoppableWallet/Widget/CoinPriceList/CoinPriceListProvider.swift +++ b/UnstoppableWallet/Widget/CoinPriceList/CoinPriceListProvider.swift @@ -3,8 +3,6 @@ import SwiftUI import WidgetKit struct CoinPriceListProvider: IntentTimelineProvider { - let mode: CoinPriceListMode - func placeholder(in context: Context) -> CoinPriceListEntry { let count: Int @@ -15,20 +13,9 @@ struct CoinPriceListProvider: IntentTimelineProvider { return CoinPriceListEntry( date: Date(), - mode: mode, sortType: .highestCap, maxItemCount: count, - items: (1 ... count).map { index in - CoinPriceListEntry.Item( - uid: "coin\(index)", - icon: nil, - code: "COD\(index)", - name: "Coin Name \(index)", - price: "$1234", - priceChange: "1.23", - priceChangeType: .unknown - ) - } + items: (1 ... count).map { CoinItem.stub(index: $0) } ) } @@ -61,13 +48,12 @@ struct CoinPriceListProvider: IntentTimelineProvider { switch sortType { case .highestCap, .lowestCap, .unknown: listType = .mcap - case .highestVolume, .lowestVolume: listType = .volume - case .topGainers, .topLosers: listType = .price + case .gainers, .losers: listType = .priceChange24h } switch sortType { - case .highestCap, .highestVolume, .topGainers, .unknown: listOrder = .desc - case .lowestCap, .lowestVolume, .topLosers: listOrder = .asc + case .highestCap, .gainers, .unknown: listOrder = .desc + case .lowestCap, .losers: listOrder = .asc } switch family { @@ -75,35 +61,22 @@ struct CoinPriceListProvider: IntentTimelineProvider { default: limit = 6 } - let coins: [Coin] - - switch mode { - case .topCoins: - coins = try await apiProvider.listCoins(type: listType, order: listOrder, limit: limit, currencyCode: currency.code) - case .watchlist: - let coinUids: [String]? = storage.value(for: AppWidgetConstants.keyFavoriteCoinUids) - - if let coinUids, !coinUids.isEmpty { - coins = try await apiProvider.listCoins(uids: coinUids, type: listType, order: listOrder, limit: limit, currencyCode: currency.code) - } else { - coins = [] - } - } + let coins = try await apiProvider.listCoins(type: listType, order: listOrder, limit: limit, currencyCode: currency.code) return CoinPriceListEntry( date: Date(), - mode: mode, sortType: sortType, maxItemCount: limit, items: coins.map { coin in - CoinPriceListEntry.Item( + CoinItem( uid: coin.uid, icon: coin.image, code: coin.code, - name: coin.name, + marketCap: coin.marketCap.flatMap { ValueFormatter.formatShort(currency: currency, value: $0) }, + rank: coin.rank.map { "\($0)" }, price: coin.formattedPrice(currency: currency), - priceChange: coin.formattedPriceChange, - priceChangeType: coin.priceChangeType + priceChange: coin.formattedPriceChange(), + priceChangeType: coin.priceChangeType() ) } ) diff --git a/UnstoppableWallet/Widget/Localizable.xcstrings b/UnstoppableWallet/Widget/Localizable.xcstrings index ccd199ddfa..31309dd918 100644 --- a/UnstoppableWallet/Widget/Localizable.xcstrings +++ b/UnstoppableWallet/Widget/Localizable.xcstrings @@ -1,6 +1,56 @@ { "sourceLanguage" : "en", "strings" : { + "%@number.billion" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@B" + } + } + } + }, + "%@number.million" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@M" + } + } + } + }, + "%@number.quadrillion" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@Q" + } + } + } + }, + "%@number.thousand" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@K" + } + } + } + }, + "%@number.trillion" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@T" + } + } + } + }, "single_coin_price.description" : { "extractionState" : "manual", "localizations" : { @@ -119,6 +169,16 @@ } } }, + "sort_type.gainers" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gainers" + } + } + } + }, "sort_type.highest_cap" : { "localizations" : { "de" : { @@ -177,60 +237,12 @@ } } }, - "sort_type.highest_volume" : { + "sort_type.losers" : { "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Höchstes Handelsvolumen" - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Highest Volume" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mayor volumen" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Plus grand volume" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "최대 거래량" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Maior volume" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Наибольший объем" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "En yüksek hacim" - } - }, - "zh" : { - "stringUnit" : { - "state" : "translated", - "value" : "最高交易量" + "value" : "Losers" } } } @@ -293,176 +305,12 @@ } } }, - "sort_type.lowest_volume" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Geringstes Handelsvolumen" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lowest Volume" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Menor volumen" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Plus faible volume" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "최소 거래량" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Menor volume" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Наименьший объем" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "En düşük hacim" - } - }, - "zh" : { - "stringUnit" : { - "state" : "translated", - "value" : "最低交易量" - } - } - } - }, - "sort_type.top_gainers" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Top-Gewinner" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Top Gainers" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mayores ganadores" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Plus grands gagnants" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "상위 수익 코인" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Maiores ganhadores" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Показывают рост" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "En çok kazananlar" - } - }, - "zh" : { - "stringUnit" : { - "state" : "translated", - "value" : "涨幅最大的股票" - } - } - } - }, - "sort_type.top_losers" : { + "sort_type.manual" : { "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Top-Verlierer" - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Top Losers" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mayores perdedores" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Plus grands perdants" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "하위 손실 코인" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Maiores perdedores" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Теряют в цене" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "En çok kaybedenler" - } - }, - "zh" : { - "stringUnit" : { - "state" : "translated", - "value" : "跌幅最大的股票" + "value" : "Manual" } } } diff --git a/UnstoppableWallet/Widget/Misc/ApiProvider.swift b/UnstoppableWallet/Widget/Misc/ApiProvider.swift index 0006b9ba8e..bd28ece00f 100644 --- a/UnstoppableWallet/Widget/Misc/ApiProvider.swift +++ b/UnstoppableWallet/Widget/Misc/ApiProvider.swift @@ -13,12 +13,6 @@ class ApiProvider { var headers = HTTPHeaders() headers.add(name: "widget", value: "true") - headers.add(name: "app_platform", value: "ios") - headers.add(name: "app_version", value: WidgetConfig.appVersion) - - if let appId = WidgetConfig.appId { - headers.add(name: "app_id", value: appId) - } if let apiKey = WidgetConfig.hsProviderApiKey { headers.add(name: "apikey", value: apiKey) @@ -54,7 +48,7 @@ class ApiProvider { func coinWithPrice(uid: String, currencyCode: String) async throws -> Coin { let parameters: Parameters = [ "uids": uid, - "fields": "uid,name,code,price,price_change_24h", + "fields": "uid,name,code,price,price_change_24h,price_change_1d", "currency": currencyCode.lowercased(), ] @@ -87,25 +81,40 @@ class ApiProvider { } enum ListType: String { - case price - case volume + case priceChange24h = "price_change_24h" + case priceChange1d = "price_change_1d" + case priceChange1w = "price_change_1w" + case priceChange1m = "price_change_1m" + case priceChange3m = "price_change_3m" case mcap } } struct Coin: ImmutableMappable { let uid: String - let name: String let code: String + let name: String + let marketCap: Decimal? + let rank: Int? let price: Decimal? let priceChange24h: Decimal? + let priceChange1d: Decimal? + let priceChange1w: Decimal? + let priceChange1m: Decimal? + let priceChange3m: Decimal? init(map: Map) throws { uid = try map.value("uid") - name = try map.value("name") code = try map.value("code") + name = try map.value("name") + marketCap = try? map.value("market_cap", using: Transform.stringToDecimalTransform) + rank = try? map.value("market_cap_rank") price = try? map.value("price", using: Transform.stringToDecimalTransform) priceChange24h = try? map.value("price_change_24h", using: Transform.stringToDecimalTransform) + priceChange1d = try? map.value("price_change_1d", using: Transform.stringToDecimalTransform) + priceChange1w = try? map.value("price_change_1w", using: Transform.stringToDecimalTransform) + priceChange1m = try? map.value("price_change_1m", using: Transform.stringToDecimalTransform) + priceChange3m = try? map.value("price_change_3m", using: Transform.stringToDecimalTransform) } } diff --git a/UnstoppableWallet/Widget/Misc/CoinPriceListMode.swift b/UnstoppableWallet/Widget/Misc/CoinPriceListMode.swift deleted file mode 100644 index fa326f48de..0000000000 --- a/UnstoppableWallet/Widget/Misc/CoinPriceListMode.swift +++ /dev/null @@ -1,20 +0,0 @@ -import SwiftUI - -enum CoinPriceListMode { - case topCoins - case watchlist - - var title: LocalizedStringKey { - switch self { - case .topCoins: return "top_coins.title" - case .watchlist: return "watchlist.title" - } - } - - var isWatchlist: Bool { - switch self { - case .watchlist: return true - default: return false - } - } -} diff --git a/UnstoppableWallet/Widget/Misc/Extensions.swift b/UnstoppableWallet/Widget/Misc/Extensions.swift index f321cec50b..233cf694c3 100644 --- a/UnstoppableWallet/Widget/Misc/Extensions.swift +++ b/UnstoppableWallet/Widget/Misc/Extensions.swift @@ -15,15 +15,25 @@ extension Coin { price.flatMap { ValueFormatter.format(currency: currency, value: $0) } ?? "n/a" } - var formattedPriceChange: String { - priceChange24h.flatMap { ValueFormatter.format(percentValue: $0) } ?? "n/a" + func formattedPriceChange(timePeriod: WatchlistTimePeriod = .day1) -> String { + priceChange(timePeriod: timePeriod).flatMap { ValueFormatter.format(percentValue: $0) } ?? "n/a" } - var priceChangeType: PriceChangeType { - guard let priceChange24h else { + func priceChangeType(timePeriod: WatchlistTimePeriod = .day1) -> PriceChangeType { + guard let priceChange = priceChange(timePeriod: timePeriod) else { return .unknown } - return priceChange24h >= 0 ? .up : .down + return priceChange >= 0 ? .up : .down + } + + private func priceChange(timePeriod: WatchlistTimePeriod) -> Decimal? { + switch timePeriod { + case .hour24: return priceChange24h + case .day1: return priceChange1d + case .week1: return priceChange1w + case .month1: return priceChange1m + case .month3: return priceChange3m + } } } diff --git a/UnstoppableWallet/Widget/Misc/ValueFormatter.swift b/UnstoppableWallet/Widget/Misc/ValueFormatter.swift index bf4aab165a..2028df5924 100644 --- a/UnstoppableWallet/Widget/Misc/ValueFormatter.swift +++ b/UnstoppableWallet/Widget/Misc/ValueFormatter.swift @@ -31,6 +31,18 @@ enum ValueFormatter { return maxCount } + private static func digitsAndValue(value: Decimal, basePow: Int) -> (Int, Decimal) { + let digits: Int + + switch value { + case pow(10, basePow) ..< (2 * pow(10, basePow + 1)): digits = 2 + case (2 * pow(10, basePow + 1)) ..< (2 * pow(10, basePow + 2)): digits = 1 + default: digits = 0 + } + + return (digits, value / pow(10, basePow)) + } + private static func transformedFull(value: Decimal, maxDigits: Int, minDigits: Int = 0) -> (value: Decimal, digits: Int) { let value = abs(value) let digits: Int @@ -62,9 +74,75 @@ enum ValueFormatter { return (value: value, digits: max(digits, minDigits)) } - private static func decorated(string: String, symbol: String? = nil, signValue: Decimal? = nil) -> String { + private static func transformedShort(value: Decimal, maxDigits: Int = Int.max) -> (value: Decimal, digits: Int, suffix: ((String) -> String)?, tooSmall: Bool) { + var value = abs(value) + var suffix: ((String) -> String)? + let digits: Int + var tooSmall = false + + switch value { + case 0: + digits = 0 + + case 0 ..< 0.0000_0001: + digits = 8 + value = 0.0000_0001 + tooSmall = true + + case 0.0000_0001 ..< 1: + let zeroCount = fractionZeroCount(value: value, maxCount: 8) + digits = min(maxDigits, zeroCount + 4, 8) + + case 1 ..< 1.01: + digits = 4 + + case 1.01 ..< 1.1: + digits = 3 + + case 1.1 ..< 20: + digits = 2 + + case 20 ..< 200: + digits = 1 + + case 200 ..< 19999.5: + digits = 0 + + case 19999.5 ..< edge(6): + (digits, value) = digitsAndValue(value: value, basePow: 3) + suffix = { String(localized: "\($0)number.thousand") } + + case edge(6) ..< edge(9): + (digits, value) = digitsAndValue(value: value, basePow: 6) + suffix = { String(localized: "\($0)number.million") } + + case edge(9) ..< edge(12): + (digits, value) = digitsAndValue(value: value, basePow: 9) + suffix = { String(localized: "\($0)number.billion") } + + case edge(12) ..< edge(15): + (digits, value) = digitsAndValue(value: value, basePow: 12) + suffix = { String(localized: "\($0)number.trillion") } + + default: + (digits, value) = digitsAndValue(value: value, basePow: 15) + suffix = { String(localized: "\($0)number.quadrillion") } + } + + return (value: value, digits: digits, suffix: suffix, tooSmall: tooSmall) + } + + private static func edge(_ power: Int) -> Decimal { + pow(10, power) - (pow(10, power - 3) / 2) + } + + private static func decorated(string: String, suffix: ((String) -> String)? = nil, symbol: String? = nil, signValue: Decimal? = nil, tooSmall: Bool = false) -> String { var string = string + if let suffix { + string = suffix(string) + } + if let symbol { string = "\(string) \(symbol)" } @@ -77,10 +155,14 @@ enum ValueFormatter { string = "\(sign)\(string)" } + if tooSmall { + string = "< \(string)" + } + return string } - private static func formattedCurrency(value: Decimal, digits: Int, code: String, symbol: String) -> String? { + private static func formattedCurrency(value: Decimal, digits: Int, code: String, symbol: String, suffix: ((String) -> String)? = nil) -> String? { currencyFormatter.currencyCode = code currencyFormatter.currencySymbol = symbol currencyFormatter.internationalCurrencySymbol = symbol @@ -95,7 +177,7 @@ enum ValueFormatter { return nil } - return pattern.replacingOccurrences(of: "1", with: decorated(string: string)) + return pattern.replacingOccurrences(of: "1", with: decorated(string: string, suffix: suffix)) } static func format(percentValue: Decimal, showSign: Bool = true) -> String? { @@ -119,4 +201,14 @@ enum ValueFormatter { return decorated(string: string, signValue: showSign ? value : nil) } + + static func formatShort(currency: Currency, value: Decimal, showSign: Bool = false) -> String? { + let (transformedValue, digits, suffix, tooSmall) = transformedShort(value: value) + + guard let string = formattedCurrency(value: transformedValue, digits: digits, code: currency.code, symbol: currency.symbol, suffix: suffix) else { + return nil + } + + return decorated(string: string, signValue: showSign ? value : nil, tooSmall: tooSmall) + } } diff --git a/UnstoppableWallet/Widget/Misc/WidgetConfig.swift b/UnstoppableWallet/Widget/Misc/WidgetConfig.swift index c2cca92370..d4f77dbc64 100644 --- a/UnstoppableWallet/Widget/Misc/WidgetConfig.swift +++ b/UnstoppableWallet/Widget/Misc/WidgetConfig.swift @@ -2,14 +2,6 @@ import Foundation import UIKit enum WidgetConfig { - static var appVersion: String { - Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String - } - - static var appId: String? { - UIDevice.current.identifierForVendor?.uuidString - } - static var marketApiUrl: String { (Bundle.main.object(forInfoDictionaryKey: "MarketApiUrl") as? String) ?? "" } diff --git a/UnstoppableWallet/Widget/SingleCoinPrice/SingleCoinPriceProvider.swift b/UnstoppableWallet/Widget/SingleCoinPrice/SingleCoinPriceProvider.swift index 8681a32d92..b4e64c538c 100644 --- a/UnstoppableWallet/Widget/SingleCoinPrice/SingleCoinPriceProvider.swift +++ b/UnstoppableWallet/Widget/SingleCoinPrice/SingleCoinPriceProvider.swift @@ -65,8 +65,8 @@ struct SingleCoinPriceProvider: IntentTimelineProvider { icon: coin.image, code: coin.code, price: coin.formattedPrice(currency: currency), - priceChange: coin.formattedPriceChange, - priceChangeType: coin.priceChangeType, + priceChange: coin.formattedPriceChange(), + priceChangeType: coin.priceChangeType(), chartPoints: chartPoints ) } diff --git a/UnstoppableWallet/Widget/TopCoinsWidget.swift b/UnstoppableWallet/Widget/TopCoinsWidget.swift index d20ffd4ae1..cdd531246b 100644 --- a/UnstoppableWallet/Widget/TopCoinsWidget.swift +++ b/UnstoppableWallet/Widget/TopCoinsWidget.swift @@ -7,13 +7,13 @@ struct TopCoinsWidget: Widget { IntentConfiguration( kind: AppWidgetConstants.topCoinsWidgetKind, intent: CoinPriceListIntent.self, - provider: CoinPriceListProvider(mode: .topCoins) + provider: CoinPriceListProvider() ) { entry in if #available(iOS 17.0, *) { - CoinPriceListView(entry: entry) + view(entry: entry) .containerBackground(.fill.tertiary, for: .widget) } else { - CoinPriceListView(entry: entry) + view(entry: entry) .background() } } @@ -26,4 +26,17 @@ struct TopCoinsWidget: Widget { .systemLarge, ]) } + + @ViewBuilder private func view(entry: CoinPriceListEntry) -> some View { + CoinListView(items: entry.items, maxItemCount: entry.maxItemCount, title: "top_coins.title", subtitle: title(sortType: entry.sortType)) + } + + private func title(sortType: SortType) -> LocalizedStringKey { + switch sortType { + case .highestCap, .unknown: return "sort_type.highest_cap" + case .lowestCap: return "sort_type.lowest_cap" + case .gainers: return "sort_type.gainers" + case .losers: return "sort_type.losers" + } + } } diff --git a/UnstoppableWallet/Widget/Watchlist/WatchlistEntry.swift b/UnstoppableWallet/Widget/Watchlist/WatchlistEntry.swift new file mode 100644 index 0000000000..198417fade --- /dev/null +++ b/UnstoppableWallet/Widget/Watchlist/WatchlistEntry.swift @@ -0,0 +1,10 @@ +import Foundation +import SwiftUI +import WidgetKit + +struct WatchlistEntry: TimelineEntry { + let date: Date + let sortBy: WatchlistSortBy + let maxItemCount: Int + let items: [CoinItem] +} diff --git a/UnstoppableWallet/Widget/Watchlist/WatchlistProvider.swift b/UnstoppableWallet/Widget/Watchlist/WatchlistProvider.swift new file mode 100644 index 0000000000..f5768f4823 --- /dev/null +++ b/UnstoppableWallet/Widget/Watchlist/WatchlistProvider.swift @@ -0,0 +1,115 @@ +import Foundation +import SwiftUI +import WidgetKit + +struct WatchlistProvider: TimelineProvider { + func placeholder(in context: Context) -> WatchlistEntry { + let count: Int + + switch context.family { + case .systemSmall, .systemMedium: count = 3 + default: count = 6 + } + + return WatchlistEntry( + date: Date(), + sortBy: .gainers, + maxItemCount: count, + items: (1 ... count).map { CoinItem.stub(index: $0) } + ) + } + + func getSnapshot(in context: Context, completion: @escaping (WatchlistEntry) -> Void) { + Task { + let entry = try await fetch(family: context.family) + completion(entry) + } + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + Task { + let entry = try await fetch(family: context.family) + + if let nextUpdate = Calendar.current.date(byAdding: DateComponents(minute: 15), to: Date()) { + let timeline = Timeline(entries: [entry], policy: .after(nextUpdate)) + completion(timeline) + } + } + } + + private func fetch(family: WidgetFamily) async throws -> WatchlistEntry { + let storage = SharedLocalStorage() + let priceChangeModeManager = PriceChangeModeManager(storage: storage) + let watchlistManager = WatchlistManager(storage: storage, priceChangeModeManager: priceChangeModeManager) + let currency = CurrencyManager(storage: storage).baseCurrency + let apiProvider = ApiProvider() + + let listType: ApiProvider.ListType + let listOrder: ApiProvider.ListOrder + let limit: Int + + switch watchlistManager.sortBy { + case .highestCap, .lowestCap: listType = .mcap + case .gainers, .losers, .manual: + switch watchlistManager.timePeriod { + case .hour24: listType = .priceChange24h + case .day1: listType = .priceChange1d + case .week1: listType = .priceChange1w + case .month1: listType = .priceChange1m + case .month3: listType = .priceChange3m + } + } + + switch watchlistManager.sortBy { + case .highestCap, .gainers, .manual: listOrder = .desc + case .lowestCap, .losers: listOrder = .asc + } + + switch family { + case .systemSmall, .systemMedium: limit = 3 + default: limit = 6 + } + + let coinUids: [String] + + switch watchlistManager.sortBy { + case .manual: + coinUids = Array(watchlistManager.coinUids.prefix(limit)) + default: + coinUids = watchlistManager.coinUids + } + + let coins: [Coin] + + if !coinUids.isEmpty { + let apiCoins = try await apiProvider.listCoins(uids: coinUids, type: listType, order: listOrder, limit: limit, currencyCode: currency.code) + + switch watchlistManager.sortBy { + case .manual: + coins = coinUids.compactMap { uid in apiCoins.first { $0.uid == uid } } + default: + coins = apiCoins + } + } else { + coins = [] + } + + return WatchlistEntry( + date: Date(), + sortBy: watchlistManager.sortBy, + maxItemCount: limit, + items: coins.map { coin in + CoinItem( + uid: coin.uid, + icon: coin.image, + code: coin.code, + marketCap: coin.marketCap.flatMap { ValueFormatter.formatShort(currency: currency, value: $0) }, + rank: coin.rank.map { "\($0)" }, + price: coin.formattedPrice(currency: currency), + priceChange: coin.formattedPriceChange(timePeriod: watchlistManager.timePeriod), + priceChangeType: coin.priceChangeType(timePeriod: watchlistManager.timePeriod) + ) + } + ) + } +} diff --git a/UnstoppableWallet/Widget/WatchlistWidget.swift b/UnstoppableWallet/Widget/WatchlistWidget.swift index 23c73a4d7b..2dbb96f5f7 100644 --- a/UnstoppableWallet/Widget/WatchlistWidget.swift +++ b/UnstoppableWallet/Widget/WatchlistWidget.swift @@ -4,16 +4,15 @@ import WidgetKit struct WatchlistWidget: Widget { var body: some WidgetConfiguration { - IntentConfiguration( + StaticConfiguration( kind: AppWidgetConstants.watchlistWidgetKind, - intent: CoinPriceListIntent.self, - provider: CoinPriceListProvider(mode: .watchlist) + provider: WatchlistProvider() ) { entry in if #available(iOS 17.0, *) { - CoinPriceListView(entry: entry) + view(entry: entry) .containerBackground(.fill.tertiary, for: .widget) } else { - CoinPriceListView(entry: entry) + view(entry: entry) .background() } } @@ -26,4 +25,18 @@ struct WatchlistWidget: Widget { .systemLarge, ]) } + + @ViewBuilder private func view(entry: WatchlistEntry) -> some View { + CoinListView(items: entry.items, maxItemCount: entry.maxItemCount, title: "watchlist.title", subtitle: title(sortBy: entry.sortBy)) + } + + private func title(sortBy: WatchlistSortBy) -> LocalizedStringKey { + switch sortBy { + case .manual: return "sort_type.manual" + case .highestCap: return "sort_type.highest_cap" + case .lowestCap: return "sort_type.lowest_cap" + case .gainers: return "sort_type.gainers" + case .losers: return "sort_type.losers" + } + } } diff --git a/UnstoppableWallet/Widget/de.lproj/AppWidget.strings b/UnstoppableWallet/Widget/de.lproj/AppWidget.strings index be5a18cd9c..3cc3564125 100644 --- a/UnstoppableWallet/Widget/de.lproj/AppWidget.strings +++ b/UnstoppableWallet/Widget/de.lproj/AppWidget.strings @@ -1,27 +1,39 @@ +"2puq80" = "Münzpreisliste"; + "6ebp42" = "Ausgewählte Münze"; + "8vJheC" = "Sortieren nach"; -"qHGyuo" = "Höchste Obergrenze"; -"tVjZ7W" = "Niedrigste Obergrenze"; -"t6AKQ5" = "Höchste Lautstärke"; -"C1LmDj" = "Geringste Lautstärke"; -"Rl3a0T" = "Top- Gewinner"; -"x1kK4T" = "Top Verlierer"; -"2puq80" = "Münzpreisliste"; -"Iodq3O-C1LmDj" = "Um sicherzustellen, dass ich das richtig verstehe, meintest du \"geringstes Handelsvolumen\"?"; -"Iodq3O-Rl3a0T" = "Um sicherzustellen, dass ich das richtig verstehe, meintest du 'Top-Gewinner'?"; +"Iodq3O-Rl3a0T" = "Um sicherzustellen, dass ich das richtig verstehe, meintest du 'Gewinner'?"; + "Iodq3O-qHGyuo" = "Um sicherzustellen, dass ich das richtig verstehe, meintest du 'Höchste Kapitalisierung'?"; -"Iodq3O-t6AKQ5" = "Um sicherzustellen, dass ich das richtig verstehe, meintest du 'Höchstes Volumen'?"; + "Iodq3O-tVjZ7W" = "Um sicherzustellen, dass ich das richtig verstehe, meintest du 'Niedrigste Kapitalisierung'?"; -"Iodq3O-x1kK4T" = "Um sicherzustellen, dass ich das richtig verstehe, meintest du 'Top-Verlierer'?"; + +"Iodq3O-x1kK4T" = "Um sicherzustellen, dass ich das richtig verstehe, meintest du 'Verlierer'?"; + "OkcYOj" = "Anzeigen des Münzpreises für ausgewählte Münze"; + +"Rl3a0T" = "Gewinner"; + "VXTxGF" = "Widget Münze"; + "ZBHf6A" = "Sortiertyp"; + "hrLfmt" = "Einzelpreis der Münze"; -"nj8Co9-C1LmDj" = "Es gibt ${count} Optionen, die 'Niedrigstes Volumen' entsprechen."; -"nj8Co9-Rl3a0T" = "Es gibt ${count} Optionen, die 'Top-Gewinner' entsprechen."; + +"nj8Co9-Rl3a0T" = "Es gibt ${count} Optionen, die 'Gewinner' entsprechen."; + "nj8Co9-qHGyuo" = "Es gibt ${count} Optionen, die 'Höchste Kapitalisierung' entsprechen."; -"nj8Co9-t6AKQ5" = "Es gibt ${count} Optionen, die 'Höchstes Volumen' entsprechen."; + "nj8Co9-tVjZ7W" = "Es gibt ${count} Optionen, die 'Niedrigste Kapitalisierung' entsprechen."; -"nj8Co9-x1kK4T" = "Es gibt ${count} Optionen, die 'Top-Verlierer' entsprechen."; + +"nj8Co9-x1kK4T" = "Es gibt ${count} Optionen, die 'Verlierer' entsprechen."; + +"qHGyuo" = "Höchste Obergrenze"; + "rCV9fg" = "Sehen Sie sich die Münzpreise für die Münzliste an"; + +"tVjZ7W" = "Niedrigste Obergrenze"; + +"x1kK4T" = "Verlierer"; diff --git a/UnstoppableWallet/Widget/en.lproj/AppWidget.strings b/UnstoppableWallet/Widget/en.lproj/AppWidget.strings index fcad31dce1..aef320a2c8 100644 --- a/UnstoppableWallet/Widget/en.lproj/AppWidget.strings +++ b/UnstoppableWallet/Widget/en.lproj/AppWidget.strings @@ -1,27 +1,40 @@ +"2puq80" = "Coin Price List"; + "6ebp42" = "Selected Coin"; + "8vJheC" = "Sort By"; -"qHGyuo" = "Highest Cap"; -"tVjZ7W" = "Lowest Cap"; -"t6AKQ5" = "Highest Volume"; -"C1LmDj" = "Lowest Volume"; -"Rl3a0T" = "Top Gainers"; -"x1kK4T" = "Top Losers"; -"2puq80" = "Coin Price List"; -"Iodq3O-C1LmDj" = "Just to confirm, you wanted ‘Lowest Volume’?"; -"Iodq3O-Rl3a0T" = "Just to confirm, you wanted ‘Top Gainers’?"; +"Iodq3O-Rl3a0T" = "Just to confirm, you wanted ‘Gainers’?"; + "Iodq3O-qHGyuo" = "Just to confirm, you wanted ‘Highest Cap’?"; -"Iodq3O-t6AKQ5" = "Just to confirm, you wanted ‘Highest Volume’?"; + "Iodq3O-tVjZ7W" = "Just to confirm, you wanted ‘Lowest Cap’?"; -"Iodq3O-x1kK4T" = "Just to confirm, you wanted ‘Top Losers’?"; + +"Iodq3O-x1kK4T" = "Just to confirm, you wanted ‘Losers’?"; + "OkcYOj" = "View coin price for selected coin"; + +"Rl3a0T" = "Gainers"; + "VXTxGF" = "Widget Coin"; + "ZBHf6A" = "Sort Type"; + "hrLfmt" = "Single Coin Price"; -"nj8Co9-C1LmDj" = "There are ${count} options matching ‘Lowest Volume’."; -"nj8Co9-Rl3a0T" = "There are ${count} options matching ‘Top Gainers’."; + +"nj8Co9-Rl3a0T" = "There are ${count} options matching ‘Gainers’."; + "nj8Co9-qHGyuo" = "There are ${count} options matching ‘Highest Cap’."; -"nj8Co9-t6AKQ5" = "There are ${count} options matching ‘Highest Volume’."; + "nj8Co9-tVjZ7W" = "There are ${count} options matching ‘Lowest Cap’."; -"nj8Co9-x1kK4T" = "There are ${count} options matching ‘Top Losers’."; + +"nj8Co9-x1kK4T" = "There are ${count} options matching ‘Losers’."; + +"qHGyuo" = "Highest Cap"; + "rCV9fg" = "See coin prices for coin list"; + +"tVjZ7W" = "Lowest Cap"; + +"x1kK4T" = "Losers"; + diff --git a/UnstoppableWallet/Widget/es.lproj/AppWidget.strings b/UnstoppableWallet/Widget/es.lproj/AppWidget.strings index 1831c535af..82ba591b05 100644 --- a/UnstoppableWallet/Widget/es.lproj/AppWidget.strings +++ b/UnstoppableWallet/Widget/es.lproj/AppWidget.strings @@ -1,27 +1,39 @@ +"2puq80" = "Lista de precios de criptomonedas"; + "6ebp42" = "Moneda seleccionada"; + "8vJheC" = "Ordenar por"; -"qHGyuo" = "Capitalización Mayor"; -"tVjZ7W" = "Capitalización Menor"; -"t6AKQ5" = "Mayor Volumen"; -"C1LmDj" = "Menor Volumen"; -"Rl3a0T" = "Máximos Ganadores"; -"x1kK4T" = "Máximos Perdedores"; -"2puq80" = "Lista de precios de criptomonedas"; -"Iodq3O-C1LmDj" = "Para confirmar, ¿quieres \"Menor Volumen\"?"; -"Iodq3O-Rl3a0T" = "Para confirmar, ¿quieres 'Principales Ganadores'?"; +"Iodq3O-Rl3a0T" = "Para confirmar, ¿quieres 'Ganadores'?"; + "Iodq3O-qHGyuo" = "Para confirmar, ¿querías 'Capitalización Mayor'?"; -"Iodq3O-t6AKQ5" = "Para confirmar, ¿querías 'Mayor Volumen'?"; + "Iodq3O-tVjZ7W" = "Para confirmar, ¿querías 'Capitalización Menor?"; -"Iodq3O-x1kK4T" = "Para confirmar, ¿querías 'Máximos Perdedores'?"; + +"Iodq3O-x1kK4T" = "Para confirmar, ¿querías 'Perdedores'?"; + "OkcYOj" = "Ver el precio de la moneda seleccionada"; + +"Rl3a0T" = "Ganadores"; + "VXTxGF" = "Widget de moneda"; + "ZBHf6A" = "Tipo de ordenamiento"; + "hrLfmt" = "Precio de una sola moneda"; -"nj8Co9-C1LmDj" = "Hay ${count} opciones que coinciden con 'Menor Volumen'."; -"nj8Co9-Rl3a0T" = "Hay ${count} opciones que coinciden con 'Principales Ganadores'."; + +"nj8Co9-Rl3a0T" = "Hay ${count} opciones que coinciden con 'Ganadores'."; + "nj8Co9-qHGyuo" = "Hay ${count} opciones que coinciden con 'Capitalización Mayor'."; -"nj8Co9-t6AKQ5" = "Hay ${count} opciones que coinciden con 'Mayor Volumen'."; + "nj8Co9-tVjZ7W" = "Hay ${count} opciones que coinciden con 'Capitalización Menor'."; -"nj8Co9-x1kK4T" = "Hay ${count} opciones que coinciden con 'Máximos Perdedores'."; + +"nj8Co9-x1kK4T" = "Hay ${count} opciones que coinciden con 'Perdedores'."; + +"qHGyuo" = "Cap Mayor"; + "rCV9fg" = "Ver precios de las monedas en la lista de monedas"; + +"tVjZ7W" = "Cap Menor"; + +"x1kK4T" = "Perdedores"; diff --git a/UnstoppableWallet/Widget/fr.lproj/AppWidget.strings b/UnstoppableWallet/Widget/fr.lproj/AppWidget.strings index 3cdd05a962..dd8e107d7d 100644 --- a/UnstoppableWallet/Widget/fr.lproj/AppWidget.strings +++ b/UnstoppableWallet/Widget/fr.lproj/AppWidget.strings @@ -1,27 +1,39 @@ +"2puq80" = "Liste des prix des cryptomonnaies"; + "6ebp42" = "Pièce sélectionnée"; -"8vJheC" = "Trier par"; -"qHGyuo" = "Cap. marché la plus forte"; -"tVjZ7W" = "Cap. marché la plus basse"; -"t6AKQ5" = "Volume le plus élevé"; -"C1LmDj" = "Volume le plus bas"; -"Rl3a0T" = "Les grands gagnants"; -"x1kK4T" = "Les gros perdants"; -"2puq80" = "Liste des prix des cryptomonnaies"; -"Iodq3O-C1LmDj" = "Pour confirmer, vous vouliez 'Volume le plus bas'?"; -"Iodq3O-Rl3a0T" = "Juste pour confirmer, vous vouliez 'Top Gainers'?"; +"8vJheC" = "Classer par"; + +"Iodq3O-Rl3a0T" = "Juste pour confirmer, vous vouliez 'Gagnants'?"; + "Iodq3O-qHGyuo" = "Juste pour confirmer, vous vouliez 'Capitalisation la plus élevée'?"; -"Iodq3O-t6AKQ5" = "Juste pour confirmer, vous vouliez 'Volume le plus élevé'?"; + "Iodq3O-tVjZ7W" = "Juste pour confirmer, vous vouliez 'Capitalisation la plus faible'?"; -"Iodq3O-x1kK4T" = "Juste pour confirmer, vous vouliez 'Les plus grands perdants'?"; + +"Iodq3O-x1kK4T" = "Juste pour confirmer, vous vouliez 'Perdants'?"; + "OkcYOj" = "Afficher le prix de la cryptomonnaie sélectionnée"; + +"Rl3a0T" = "Gagnants"; + "VXTxGF" = "Widget de la cryptomonnaie"; + "ZBHf6A" = "Type de tri"; + "hrLfmt" = "Prix d'une seule cryptomonnaie"; -"nj8Co9-C1LmDj" = "Il y a ${count} options correspondant à 'Volume le plus bas'."; -"nj8Co9-Rl3a0T" = "Il y a ${count} options correspondant à 'Top Gainers'."; + +"nj8Co9-Rl3a0T" = "Il y a ${count} options correspondant à 'Gagnants'."; + "nj8Co9-qHGyuo" = "Il y a ${count} options correspondant à 'Capitalisation la plus élevée'."; -"nj8Co9-t6AKQ5" = "Il y a ${count} options correspondant à 'Volume le plus élevé'."; + "nj8Co9-tVjZ7W" = "Il y a ${count} options correspondant à 'Capitalisation la plus faible'."; -"nj8Co9-x1kK4T" = "There are ${count} options matching ‘Top Losers’."; + +"nj8Co9-x1kK4T" = "Il y a ${count} options correspondant à 'Perdants'."; + +"qHGyuo" = "Cap. marché la plus forte"; + "rCV9fg" = "Voir les prix des cryptomonnaies de la liste"; + +"tVjZ7W" = "Cap. marché la plus basse"; + +"x1kK4T" = "Perdants"; diff --git a/UnstoppableWallet/Widget/ko.lproj/AppWidget.strings b/UnstoppableWallet/Widget/ko.lproj/AppWidget.strings index 288d607d2a..5a1b1b5d27 100644 --- a/UnstoppableWallet/Widget/ko.lproj/AppWidget.strings +++ b/UnstoppableWallet/Widget/ko.lproj/AppWidget.strings @@ -1,27 +1,39 @@ +"2puq80" = "동전 가격표"; + "6ebp42" = "선택한 코인"; + "8vJheC" = "정렬 기준"; -"qHGyuo" = "가장 높은 시가총액"; -"tVjZ7W" = "가장 낮은 시가총액"; -"t6AKQ5" = "가장 높은 거래량"; -"C1LmDj" = "가장 낮은 거래량"; -"Rl3a0T" = "최고 승자"; -"x1kK4T" = "최고 패자"; -"2puq80" = "동전 가격표"; -"Iodq3O-C1LmDj" = "확인용으로, '최저 거래량'을 원하셨나요?"; -"Iodq3O-Rl3a0T" = "확인용으로, '최고 승자'을 원하셨나요?"; +"Iodq3O-Rl3a0T" = "확인용으로, '상승자'을 원하셨나요?"; + "Iodq3O-qHGyuo" = "확인용으로, '가장 높은 시가총액'을 원하셨나요?"; -"Iodq3O-t6AKQ5" = "확인용으로, '가장 높은 거래량'을 원하셨나요?"; + "Iodq3O-tVjZ7W" = "확인용으로, '가장 낮은 시가총액'을 원하셨나요?"; -"Iodq3O-x1kK4T" = "확인용으로, '최고 패자'을 원하셨나요?"; + +"Iodq3O-x1kK4T" = "확인용으로, '하락자'을 원하셨나요?"; + "OkcYOj" = "선택한 코인의 가격 보기"; + +"Rl3a0T" = "이득자"; + "VXTxGF" = "위젯 코인"; + "ZBHf6A" = "정렬 유형"; + "hrLfmt" = "단일 코인 가격"; -"nj8Co9-C1LmDj" = "'최저 거래량'과 일치하는 옵션은 ${count} 개 있습니다."; -"nj8Co9-Rl3a0T" = "'최고 승자'과 일치하는 옵션은 ${count} 개 있습니다."; + +"nj8Co9-Rl3a0T" = "'상승자'과 일치하는 옵션은 ${count} 개 있습니다."; + "nj8Co9-qHGyuo" = "'가장 높은 시가총액' 과 일치하는 옵션은 ${count} 개 있습니다."; -"nj8Co9-t6AKQ5" = "'가장 높은 거래량'과 일치하는 옵션은 ${count} 개 있습니다."; + "nj8Co9-tVjZ7W" = "'가장 낮은 시가총액'과 일치하는 옵션은 ${count} 개 있습니다."; -"nj8Co9-x1kK4T" = "'최고 패자'과 일치하는 옵션은 ${count} 개 있습니다."; + +"nj8Co9-x1kK4T" = "'하락자'과 일치하는 옵션은 ${count} 개 있습니다."; + +"qHGyuo" = "가장 높은 시가총액"; + "rCV9fg" = "코인 목록의 가격을 확인하세요"; + +"tVjZ7W" = "가장 낮은 시가총액"; + +"x1kK4T" = "해자"; diff --git a/UnstoppableWallet/Widget/pt-BR.lproj/AppWidget.strings b/UnstoppableWallet/Widget/pt-BR.lproj/AppWidget.strings index 6901ca0f7a..f085f4c481 100644 --- a/UnstoppableWallet/Widget/pt-BR.lproj/AppWidget.strings +++ b/UnstoppableWallet/Widget/pt-BR.lproj/AppWidget.strings @@ -1,27 +1,39 @@ +"2puq80" = "Lista de Preços de Moedas"; + "6ebp42" = "Selecionar moeda"; + "8vJheC" = "Organizar por"; -"qHGyuo" = "Maior capitalização"; -"tVjZ7W" = "Menor capitalização"; -"t6AKQ5" = "Maior volume"; -"C1LmDj" = "Menor volume"; -"Rl3a0T" = "Maiores Altas"; -"x1kK4T" = "Maiores Quedas"; -"2puq80" = "Lista de Preços de Moedas"; -"Iodq3O-C1LmDj" = "Para confirmar, você queria 'Menor volume'?"; -"Iodq3O-Rl3a0T" = "Para confirmar, você queria 'Maiores Altas'?"; +"Iodq3O-Rl3a0T" = "Para confirmar, você queria 'Ganhadores'?"; + "Iodq3O-qHGyuo" = "Para confirmar, você queria 'Maior capitalização'?"; -"Iodq3O-t6AKQ5" = "Para confirmar, você queria 'Maior volume'?"; + "Iodq3O-tVjZ7W" = "Para confirmar, você queria 'Menor capitalização'?"; -"Iodq3O-x1kK4T" = "Para confirmar, você queria 'Maiores Quedas'?"; + +"Iodq3O-x1kK4T" = "Para confirmar, você queria 'Perdedores'?"; + "OkcYOj" = "Ver el precio de la moneda seleccionada"; + +"Rl3a0T" = "Ganhadores"; + "VXTxGF" = "Widget Moeda"; + "ZBHf6A" = "Tipo de Classificação"; + "hrLfmt" = "Preço de uma Única Moeda"; -"nj8Co9-C1LmDj" = "Há ${count} opções correspondentes a 'Menor volume'."; -"nj8Co9-Rl3a0T" = "Há ${count} opções correspondentes a 'Maiores Altas'."; + +"nj8Co9-Rl3a0T" = "Há ${count} opções correspondentes a 'Ganhadores'."; + "nj8Co9-qHGyuo" = "Há ${count} opções correspondentes a 'Maior capitalização'."; -"nj8Co9-t6AKQ5" = "Há ${count} opções correspondentes a 'Maior volume'."; + "nj8Co9-tVjZ7W" = "Há ${count} opções correspondentes a 'Menor capitalização'."; -"nj8Co9-x1kK4T" = "Há ${count} opções correspondentes a 'Maiores Quedas'."; + +"nj8Co9-x1kK4T" = "Há ${count} opções correspondentes a 'Perdedores'."; + +"qHGyuo" = "Maior capitalização"; + "rCV9fg" = "Ver preços das moedas na lista de moedas"; + +"tVjZ7W" = "Menor capitalização"; + +"x1kK4T" = "Perdedores"; diff --git a/UnstoppableWallet/Widget/ru.lproj/AppWidget.strings b/UnstoppableWallet/Widget/ru.lproj/AppWidget.strings index 5bbd0ea01f..71bddcae1e 100644 --- a/UnstoppableWallet/Widget/ru.lproj/AppWidget.strings +++ b/UnstoppableWallet/Widget/ru.lproj/AppWidget.strings @@ -1,27 +1,39 @@ +"2puq80" = "Список цен на токены"; + "6ebp42" = "Выбранный токен"; + "8vJheC" = "Сортировать"; -"qHGyuo" = "Наивысшая кап."; -"tVjZ7W" = "Наименьшая кап."; -"t6AKQ5" = "Наивысший объем"; -"C1LmDj" = "Наименьший объем"; -"Rl3a0T" = "Показывают рост"; -"x1kK4T" = "Теряют в цене"; -"2puq80" = "Список цен на токены"; -"Iodq3O-C1LmDj" = "Для подтверждения, вы хотели 'Наименьший объем'?"; "Iodq3O-Rl3a0T" = "Для подтверждения, вы хотели список \"Показывают рост\"?"; + "Iodq3O-qHGyuo" = "Для подтверждения, вы хотели список \"Наивысший капитал\"?"; -"Iodq3O-t6AKQ5" = "Для подтверждения, вы хотели список \"Наивысший объем\"?"; + "Iodq3O-tVjZ7W" = "Для подтверждения, вы хотели список \"Наименьший капитал\"?"; + "Iodq3O-x1kK4T" = "Для подтверждения, вы хотели список \"Теряют в цене\"?"; + "OkcYOj" = "Просмотр цены для выбранного токена"; + +"Rl3a0T" = "Gainers"; + "VXTxGF" = "Виджет токена"; + "ZBHf6A" = "Сортировка"; + "hrLfmt" = "Цена за один токен"; -"nj8Co9-C1LmDj" = "Существует ${count} вариантов, соответствующих запросу 'Наименьший объем'."; + "nj8Co9-Rl3a0T" = "Существует ${count} вариантов, соответствующих запросу 'Показывают рост'."; + "nj8Co9-qHGyuo" = "\"Существует ${count} вариантов, соответствующих запросу 'Наивысший капитал'."; -"nj8Co9-t6AKQ5" = "\"Существует ${count} вариантов, соответствующих запросу 'Наивысший объем'."; + "nj8Co9-tVjZ7W" = "Существует ${count} вариантов, соответствующих запросу 'Наименьший капитал'."; + "nj8Co9-x1kK4T" = "Существует ${count} вариантов, соответствующих запросу 'Теряют в цене'."; + +"qHGyuo" = "Наивысшей кап."; + "rCV9fg" = "Просмотреть цены токенов из списка"; + +"tVjZ7W" = "Наименьшей кап."; + +"x1kK4T" = "Losers"; diff --git a/UnstoppableWallet/Widget/tr.lproj/AppWidget.strings b/UnstoppableWallet/Widget/tr.lproj/AppWidget.strings index d968f77023..4f439b7647 100644 --- a/UnstoppableWallet/Widget/tr.lproj/AppWidget.strings +++ b/UnstoppableWallet/Widget/tr.lproj/AppWidget.strings @@ -1,27 +1,39 @@ +"2puq80" = "Para Birimi Fiyat Listesi"; + "6ebp42" = "Seçilen Koin"; + "8vJheC" = "Sırala"; -"qHGyuo" = "En Yüksek"; -"tVjZ7W" = "En Düşük"; -"t6AKQ5" = "En Yüksek HacIm"; -"C1LmDj" = "En Düşük HacIm"; -"Rl3a0T" = "En Kazançlılar"; -"x1kK4T" = "Çok Kaybedenler"; -"2puq80" = "Para Birimi Fiyat Listesi"; -"Iodq3O-C1LmDj" = "Sadece teyit etmek için, ‘En Düşük Hacim’i mi istediniz?"; -"Iodq3O-Rl3a0T" = "Sadece teyit etmek için, 'En Çok Kazananlar'ı mı istediniz?"; +"Iodq3O-Rl3a0T" = "Sadece teyit etmek için, 'Kazananlar' mı istediniz?"; + "Iodq3O-qHGyuo" = "Sadece teyit etmek için, 'En Yüksek Kap'ı mı istediniz?"; -"Iodq3O-t6AKQ5" = "Sadece teyit etmek için, 'En Yüksek Hacim'i mi istediniz?"; + "Iodq3O-tVjZ7W" = "Sadece doğrulamak için, 'En Düşük Kap'ı mı istiyordunuz?"; -"Iodq3O-x1kK4T" = "Sadece teyit etmek için, 'En Çok Kaybedenler'i mi istediniz?"; + +"Iodq3O-x1kK4T" = "Sadece teyit etmek için, 'Kaybedenler' mi istediniz?"; + "OkcYOj" = "Seçilen para birimi için fiyatı görüntüle"; + +"Rl3a0T" = "Kazanlar"; + "VXTxGF" = "Widget Para Birimi"; + "ZBHf6A" = "Sıralama Türü"; + "hrLfmt" = "Tekil Para Birimi Fiyatı"; -"nj8Co9-C1LmDj" = "'En Düşük Hacim' ile eşleşen ${count} seçenek var."; -"nj8Co9-Rl3a0T" = "‘En Çok Kazananlar’ ile eşleşen ${count} seçenek var."; + +"nj8Co9-Rl3a0T" = "‘Kazananlar’ ile eşleşen ${count} seçenek var."; + "nj8Co9-qHGyuo" = "‘En Yüksek Kap’ ile eşleşen ${count} seçenek var."; -"nj8Co9-t6AKQ5" = "‘En Yüksek Hacim’ ile eşleşen ${count} seçenek var."; + "nj8Co9-tVjZ7W" = "‘En Düşük Kap’ ile eşleşen ${count} seçenek var."; -"nj8Co9-x1kK4T" = "‘En Çok Kaybedenler’ ile eşleşen ${count} seçenek var."; + +"nj8Co9-x1kK4T" = "‘Kaybedenler’ ile eşleşen ${count} seçenek var."; + +"qHGyuo" = "En Yüksek Piyasa Değeri"; + "rCV9fg" = "Para listesi için para birimi fiyatlarını gör"; + +"tVjZ7W" = "En Düşük Piyasa Değeri"; + +"x1kK4T" = "Kaybedenler"; diff --git a/UnstoppableWallet/Widget/zh.lproj/AppWidget.strings b/UnstoppableWallet/Widget/zh.lproj/AppWidget.strings index 67298a5f19..9f9db79605 100644 --- a/UnstoppableWallet/Widget/zh.lproj/AppWidget.strings +++ b/UnstoppableWallet/Widget/zh.lproj/AppWidget.strings @@ -1,27 +1,39 @@ +"2puq80" = "数字货币价格列表"; + "6ebp42" = "已选硬币"; + "8vJheC" = "排序方式为"; -"qHGyuo" = "市值由高到低"; -"tVjZ7W" = "市值由低到高"; -"t6AKQ5" = "成交量由高到低"; -"C1LmDj" = "成交量由低到高"; -"Rl3a0T" = "涨幅榜"; -"x1kK4T" = "跌幅榜"; -"2puq80" = "数字货币价格列表"; -"Iodq3O-C1LmDj" = "只是确认一下,你想要'最低交易量'对吗?"; -"Iodq3O-Rl3a0T" = "只是确认一下,您想要‘涨幅最大’对吗?"; +"Iodq3O-Rl3a0T" = "只是确认一下,您想要‘赢家’对吗?"; + "Iodq3O-qHGyuo" = "只是确认一下,您想要‘最高市值’对吗?"; -"Iodq3O-t6AKQ5" = "只是确认一下,您想要‘最高交易量’对吗?"; + "Iodq3O-tVjZ7W" = "只是确认一下,您想要‘最低市值’对吗?"; -"Iodq3O-x1kK4T" = "只是确认一下,您想要‘最大跌幅’对吗?"; + +"Iodq3O-x1kK4T" = "只是确认一下,您想要‘家’对吗?"; + "OkcYOj" = "查看所选币种的价格"; + +"Rl3a0T" = "涨幅最大者"; + "VXTxGF" = "小部件币"; + "ZBHf6A" = "排序类型"; + "hrLfmt" = "单个币种价格"; -"nj8Co9-C1LmDj" = "有 ${count} 个选项符合“最低成交量”。"; -"nj8Co9-Rl3a0T" = "有 ${count} 个选项符合“涨幅最高”。"; + +"nj8Co9-Rl3a0T" = "有 ${count} 个选项符合'赢家'。"; + "nj8Co9-qHGyuo" = "有 ${count} 个选项符合“最高市值”。"; -"nj8Co9-t6AKQ5" = "有 ${count} 个选项符合“最高成交量”。"; -"nj8Co9-tVjZ7W" = "有 ${count} 个选项符合“市值最低”。"; -"nj8Co9-x1kK4T" = "有 ${count} 个选项符合“跌幅最大”。"; + +"nj8Co9-tVjZ7W" = "有 ${count} 个选项符合'市值最低'。"; + +"nj8Co9-x1kK4T" = "有 ${count} 个选项符合'家'。"; + +"qHGyuo" = "市值由高到低"; + "rCV9fg" = "查看币种列表的币价。"; + +"tVjZ7W" = "市值由低到高"; + +"x1kK4T" = "跌幅最大者"; diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 54d27e0e5f..c94fb2a9df 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -25,6 +25,8 @@ XCCONFIG_DEV_OPEN_SEA_API_KEY = ENV["XCCONFIG_DEV_OPEN_SEA_API_KEY"] XCCONFIG_DEV_TRONGRID_API_KEY = ENV["XCCONFIG_DEV_TRONGRID_API_KEY"] XCCONFIG_DEV_UNSTOPPABLE_DOMAINS_API_KEY = ENV["XCCONFIG_DEV_UNSTOPPABLE_DOMAINS_API_KEY"] XCCONFIG_DEV_ONE_INCH_API_KEY = ENV["XCCONFIG_DEV_ONE_INCH_API_KEY"] +XCCONFIG_DEV_ONE_INCH_COMMISSION = ENV["XCCONFIG_DEV_ONE_INCH_COMMISSION"] +XCCONFIG_DEV_ONE_INCH_COMMISSION_ADDRESS = ENV["XCCONFIG_DEV_ONE_INCH_COMMISSION_ADDRESS"] XCCONFIG_PROD_ETHERSCAN_API_KEY = ENV["XCCONFIG_PROD_ETHERSCAN_API_KEY"] XCCONFIG_PROD_ARBISCAN_API_KEY = ENV["XCCONFIG_PROD_ARBISCAN_API_KEY"] @@ -42,6 +44,8 @@ XCCONFIG_PROD_OPEN_SEA_API_KEY = ENV["XCCONFIG_PROD_OPEN_SEA_API_KEY"] XCCONFIG_PROD_TRONGRID_API_KEY = ENV["XCCONFIG_PROD_TRONGRID_API_KEY"] XCCONFIG_PROD_UNSTOPPABLE_DOMAINS_API_KEY = ENV["XCCONFIG_PROD_UNSTOPPABLE_DOMAINS_API_KEY"] XCCONFIG_PROD_ONE_INCH_API_KEY = ENV["XCCONFIG_PROD_ONE_INCH_API_KEY"] +XCCONFIG_PROD_ONE_INCH_COMMISSION = ENV["XCCONFIG_PROD_ONE_INCH_COMMISSION"] +XCCONFIG_PROD_ONE_INCH_COMMISSION_ADDRESS = ENV["XCCONFIG_PROD_ONE_INCH_COMMISSION_ADDRESS"] def delete_temp_keychain(name) delete_keychain( @@ -121,6 +125,8 @@ def apply_dev_xcconfig update_dev_xcconfig('trongrid_api_key', XCCONFIG_DEV_TRONGRID_API_KEY) update_dev_xcconfig('unstoppable_domains_api_key', XCCONFIG_DEV_UNSTOPPABLE_DOMAINS_API_KEY) update_dev_xcconfig('one_inch_api_key', XCCONFIG_DEV_ONE_INCH_API_KEY) + update_dev_xcconfig('one_inch_commission', XCCONFIG_DEV_ONE_INCH_COMMISSION) + update_dev_xcconfig('one_inch_commission_address', XCCONFIG_DEV_ONE_INCH_COMMISSION_ADDRESS) end def apply_prod_xcconfig(swap_enabled, donate_enabled) @@ -142,6 +148,8 @@ def apply_prod_xcconfig(swap_enabled, donate_enabled) update_prod_xcconfig('trongrid_api_key', XCCONFIG_PROD_TRONGRID_API_KEY) update_prod_xcconfig('unstoppable_domains_api_key', XCCONFIG_PROD_UNSTOPPABLE_DOMAINS_API_KEY) update_prod_xcconfig('one_inch_api_key', XCCONFIG_PROD_ONE_INCH_API_KEY) + update_prod_xcconfig('one_inch_commission', XCCONFIG_PROD_ONE_INCH_COMMISSION) + update_prod_xcconfig('one_inch_commission_address', XCCONFIG_PROD_ONE_INCH_COMMISSION_ADDRESS) end def deploy_production